SceneKitの系 / magicien 

人間てぇものは、物事を二つに分類したがる嫌いがあるようでして、「私の彼氏が理系で」「俺の彼女が肉食系で」なんて言ったりするわけでございます。そうやって分類することで、物事をちょっぴり分かった気がするんでしょうな。3Dグラフィックスライブラリなんてわけのわからないものに出くわしたときは、「おや、これは何系かな?」と言っておけば、「おっ、お客さん通だねぇ」なんてことになるわけでございます。


左手系・右手系

3Dのライブラリを分類するとすれば、まずは「左手系」「右手系」だろう。
X軸を右、Y軸を上とした時に、Z軸が手前に来るのが右手系、Z軸が奥に行くのが左手系ということらしい。電磁気学とかだとここら辺が重要かもしれないけれど、3Dだと正直どうでも良い。
OpenGLは右手系、DirectX、MMD、Unityは左手系。SceneKitは内部でOpenGLを使うことがあるので右手系になっている。

行列

SceneKitでは、SCNMatrix4という4x4行列用の構造体がある。
各成分は、m11、m12、...、m44 という名前になっている。配列ではないので、ループで一気に計算、というわけにはいかない。配列にしたらしたでやれ列優先だ、行優先だという話になるので、これはこれで良いと思う。
新APIだと、simd系と相互変換できるようになったので、計算が必要な場合は、一度simdに変換すると良いだろう。
let m1: SCNMatrix4 = ...
let m2: SCNMatrix4 = ...
let m3: SCNMatrix4 = ...
let answer = SCNMatrix4Mult( SCNMatrix4Mult( m1, m2 ), m3 )

let f1 = float4x4(m1).transpose   // float4x4(m1) とすると、行と列が逆になるので転置する
let f2 = float4x4(m2).transpose
let f3 = float4x4(m3).transpose
let f = f1 * f2 * f3

let answer2 = SCNMatrix4( f.transpose )   // answerとanswer2の結果はほぼ一致する

if( Float( m1.m23 ) == f1.cmatrix.columns.2.y ){
    // 上記 if文は概ねtrueになる。が、計算精度の問題で一致しないこともあるだろう。
    // float4x4の場合、列は0123、行はxyzwでアクセスする。列・行の順でわかりづらい。
    // SCNMatrix4.m23 は CGFloat 型なので Float 型にキャストしてから比較している。型の違いは「色」の項目で。
}

回転

3Dで一番厄介なのが、この回転である。どの軸をどの順番でどの方向に回すかで挙動が変わる。各ライブラリがその個性を遺憾無く発揮する場であり、3Dデータ・APIの相互変換を難しくする主因となっている。

SceneKitの場合、SCNNodeの回転を設定するには、変換行列である transformを直接設定する方法以外に、rotation、orientation、eulerAngles の3つの変数を設定する方法がある。どれか一つを変更すれば、他の変数も全て変更される。

rotation: SCNVector4 回転軸(xyz)と回転量(w、ラジアン)を指定
orientation: SCNVector4 いわゆるクォータニオン
eulerAngles: SCNVector3 xyz各軸の回転量をそれぞれラジアンで指定
回転方向は、回転軸の+方向に対して右回り。

回転行列はこんな感じ。

※rotationのxyz軸は大きさ1に正規化される。 \( rotation = (x, y, z, w) \Rightarrow \left( \begin{array}{cccc} x^2 (1-\cos w) + \cos w & xy(1-\cos w) + z\sin w & xz(1-\cos w) - y\sin w & 0 \\ yx(1-\cos w) - z\sin w & y^2(1-\cos w) + \cos w & yz(1-\cos w) + x\sin w & 0 \\ zx(1-\cos w) + y\sin w & zy(1-\cos w) - x\sin w & z^2 (1-\cos w) + \cos w & 0\\ 0 & 0 & 0 & 1 \end{array} \right) \)

\( eulerAngles = (x, 0, 0) \Rightarrow \left( \begin{array}{cccc} 1 & 0 & 0 & 0 \\ 0 & \cos x & \sin x & 0 \\ 0 & -\sin x & \cos x & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \)

\( eulerAngles = (0, y, 0) \Rightarrow \left( \begin{array}{cccc} \cos y & 0 & -\sin y & 0 \\ 0 & 1 & 0 & 0 \\ \sin y & 0 & \cos y & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \)

\( eulerAngles = (0, 0, z) \Rightarrow \left( \begin{array}{cccc} \cos z & \sin z & 0 & 0 \\ -\sin z & \cos z & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \)

eulerAnglesのxyz回転を全て指定した場合は、z軸・y軸・x軸の順番で回転する。
\( eulerAngles = (x, y, z) \Rightarrow \left( \begin{array}{cccc} 1 & 0 & 0 & 0 \\ 0 & \cos x & \sin x & 0 \\ 0 & -\sin x & \cos x & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \left( \begin{array}{cccc} \cos y & 0 & -\sin y & 0 \\ 0 & 1 & 0 & 0 \\ \sin y & 0 & \cos y & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \left( \begin{array}{cccc} \cos z & \sin z & 0 & 0 \\ -\sin z & \cos z & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \)

orientationはrotationとほぼ同じ。関係は次の通り。

\( rotation.xyz = normalize( orientation.xyz ) \)
\( rotation.w = 2 \arccos (orientation.w) \)
※\( \arccos (orientation.w) \) が NaN の場合は、\( rotation.w = 0 \)


ちなみに、MMDのモーションデータの場合、モデルモーションはクォータニオン、カメラモーションはオイラー角で保存されている。MMDのクォータニオンからSceneKitのクォータニオンへの変換は、xとyの符号を反転すれば良い。MMDのオイラー角をSceneKitのオイラー角で表すとすれば、Z軸・-X軸・-Y軸の順で回転するのと同等。

macOS版とiOS版の両方を作る場合は、色にも気をつけないといけない。

SCNMaterialで色を設定する場合、macOSでは、NSColor、iOS/tvOS/watchOSではUIColorを使う。インタフェースはほぼ同じなので、
#if os(macOS)
    typealias Color = NSColor
#else
    typealias Color = UIColor
#endif
という感じで共通化して使ってもいいかもしれない。このとき注意が必要なのは、NSColor・UIColor共通でRGBA成分を表すのに使われているCGFloatという型である。CGFloatは、macOSだと64bit(Double)、iOSだと32bit(Float)になっている。なので、値を代入する時に型変換が必要になったりならなかったりして煩わしい。
実は自前でtypealiasを作らなくても、CGColorやSKColorといった共通クラスが用意されているけれど、CGFloatの差異は吸収されないので煩わしい。
元の型がFloatでもDoubleでもCGFloatにキャストするよう気を付ければ、コンパイラから文句を言われずに済むが、代入のたびにCGFloatとか書かないといけないのがやっぱり煩わしい。

shaderModifiersでシェーダを書く場合、materialに設定した色とシェーダで扱う色で数値が異なるので要注意。シェーダに渡される値は、materialに設定した色を sRGB→RGB 変換したものになるので、数値が小さく(暗く)なる。

2017/06/19(Mon) 02:15:57