SceneKit
行列
Swift

SceneKitで行列を乗算する時の注意点

SceneKitの座標変換において、行列を右からかければいいのか左からかければいいのか混乱したのでメモします。

ABとBAは違う

A

=

\begin{pmatrix}
1 & 2\\
3 & 4\\
\end{pmatrix}

\quad

B

=

\begin{pmatrix}
5 & 6\\
7 & 8\\
\end{pmatrix}

上のようなABがあったときにABとBAではアウトプットが異なります。

AB

=

\begin{pmatrix}
1 & 2\\
3 & 4\\
\end{pmatrix}

\begin{pmatrix}
5 & 6\\
7 & 8\\
\end{pmatrix}

=

\begin{pmatrix}
19 & 22\\
43 & 50\\
\end{pmatrix}


BA

=

\begin{pmatrix}
5 & 6\\
7 & 8\\
\end{pmatrix}

\begin{pmatrix}
1 & 2\\
3 & 4\\
\end{pmatrix}

=

\begin{pmatrix}
23 & 34\\
31 & 46\\
\end{pmatrix}

あるAという行列に対してBを左からかけるか右からかけるかで合成してできる行列が異なるのです。

ベクトルと行列をかける

3Dを扱う時には、ベクトルに対して行列をかけて座標変換をします。座標変換とは回転、拡大縮小、投影、移動などの処理です。

この時にベクトルを行ベクトルで示すか列ベクトルで示すかによって、行列を左からかけるのか右からかけるのかが変わってきます。これは書き方のルールです。行ベクトルの場合は右からかけるし、列ベクトルの場合は左からかけます。

ある(x, y)というベクトルに

A

=

\begin{pmatrix}
1 & 2\\
3 & 4\\
\end{pmatrix}

というAの変換行列をかける時、行ベクトルで記す場合と列ベクトルで記す場合の変換後の値を見てください。

行ベクトルの場合

\begin{pmatrix}
x & y\\
\end{pmatrix}

\begin{pmatrix}
1 & 2\\
3 & 4\\
\end{pmatrix}

=

\begin{pmatrix}
x + 3y & 2x + 4y\\
\end{pmatrix}

列ベクトルの場合

\begin{pmatrix}
1 & 2\\
3 & 4\\
\end{pmatrix}

\begin{pmatrix}
x \\
y
\end{pmatrix}

=

\begin{pmatrix}
x + 2y \\
3x + 4y
\end{pmatrix}

このようにアウトプットが異なるので、行ベクトルで計算しているのか列ベクトルで計算しているのかは重大な問題です。

SceneKitはどっちなのか

simd_float4x4の4行目の上から3つが平行移動のxyzを示すことが分かっているので、

\begin{pmatrix}
1 & 0 & 0 & Δx \\
0 & 1 & 0 & Δy \\
0 & 0 & 1 & Δz \\
0 & 0 & 0 & 1 
\end{pmatrix}

\begin{pmatrix}
x \\
y \\
z \\
1
\end{pmatrix}


=

\begin{pmatrix}
x + Δx \\
y + Δy \\
z + Δz \\
1
\end{pmatrix}

上のように列ベクトルで基本的に考えればいいんじゃないか?と思うかもしれません。

しかし、残念ながら一概には言えません。

SCNMatrix4のSCNMatrix4Multを使った行列は右から左にかけていく列ベクトルみたいな挙動ですが、simd_float4x4を使って乗算するときは左から右にかける行ベクトルみたいな挙動をします。

それぞれ見ていきましょう。

SCNMatrix4Multの計算

ベクトルvAという変換をして、その後Bという変換をする時には、

B

A

v

と考えます。

だからfunc SCNMatrix4Mult(_ a: SCNMatrix4, _ b: SCNMatrix4) -> SCNMatrix4で行列を合成するときは、左のaが後にやる方、右のbが先にやる方、という風に考えて合成します。列ベクトルの挙動です。

しかしsimd_float4x4は逆です。

simd_float4x4の計算

ベクトルvAという変換をして、その後Bという変換をする時には、

v

A

B

と考えます。

時系列にそって左からかけていく感じです。行ベクトルの挙動です。

例:あるSCNCameraのtransformをx軸回りに-90度回転させる

実験してみました。

  • v...現在のベクトル(多くは原点のベクトル)
  • A...cameraのtransform
  • B...x軸回りに-90度回転

というベクトルと行列があった時にやりたいことは

行ベクトルにおける

v

A

B

という計算です。

以下はPlaygroundで実験した結果です。

simd_float4x4もSCNMatrix4も全て行ベクトルだと仮定して計算して見た結果以下のようになりました。

import SceneKit

// あるカメラtransform
let transform = simd_float4x4([0.88068247, -0.015991557, 0.47343692, 0.0],
                              [0.41722444, 0.49946916, -0.7592457, 0.0],
                              [-0.22432563, 0.86618406, 0.44654584, 0.0],
                              [0.003490514, -0.063046105, -0.08170649, 1.0000001])
let mat = SCNMatrix4(transform)

// x軸を中心とした-90度回転
let rotateMat = SCNMatrix4MakeRotation(-.pi/2, 1, 0, 0)
let rotateSimd = matrix_float4x4(rotateMat)


// ① 手計算
let output1 = simd_float4x4(transform.columns.0,
                            transform.columns.2 * -1.0,
                            transform.columns.1,
                            transform.columns.3)
print(output1)

// ② simd_float4x4の乗算
let output2 = transform * rotateSimd
print(output2)

// ③ SCNMatrix4Rotateを使った乗算
let mult3 = SCNMatrix4Rotate(mat, -.pi/2, 1, 0, 0)
let output3 = matrix_float4x4(mult3)
print(output3)

// ④ SCNMatrix4Multを使った乗算
let mult4 = SCNMatrix4Mult(mat, rotateMat)
let output4 = matrix_float4x4(mult4)
print(output4)

// ⑤ SCNMatrix4Multを使った乗算  ④と逆
let mult5 = SCNMatrix4Mult(rotateMat, mat)
let output5 = matrix_float4x4(mult5)
print(output5)

/*
 ①
 simd_float4x4([[0.88068247, -0.015991557, 0.47343692, 0.0)], [0.22432563, -0.86618406, -0.44654584, -0.0)], [0.41722444, 0.49946916, -0.7592457, 0.0)], [0.003490514, -0.063046105, -0.08170649, 1.0000001)]])
 ②
 simd_float4x4([[0.88068235, -0.015991556, 0.47343686, 0.0)], [0.22432564, -0.86618394, -0.44654587, 0.0)], [0.41722438, 0.4994692, -0.7592456, 0.0)], [0.003490514, -0.063046105, -0.08170649, 1.0000001)]])
 ③
 simd_float4x4([[0.88068235, 0.4734369, 0.01599159, 0.0)], [0.41722438, -0.7592456, -0.4994692, 0.0)], [-0.2243256, 0.44654587, -0.86618394, 0.0)], [0.0034905134, -0.08170649, 0.06304609, 1.0000001)]])
 ④
 simd_float4x4([[0.88068235, 0.4734369, 0.01599159, 0.0)], [0.41722438, -0.7592456, -0.4994692, 0.0)], [-0.2243256, 0.44654587, -0.86618394, 0.0)], [0.0034905134, -0.08170649, 0.06304609, 1.0000001)]])
 ⑤
 simd_float4x4([[0.88068235, -0.015991556, 0.47343686, 0.0)], [0.22432564, -0.86618394, -0.44654587, 0.0)], [0.41722438, 0.4994692, -0.7592456, 0.0)], [0.003490514, -0.063046105, -0.08170649, 1.0000001)]])
*/

// 結果:①②⑤が一緒、 ③④が一緒(①②⑤とは逆の乗算)

①②と③④ではアウトプットが違うことがわかるでしょうか?
やっぱり乗算の方向がsimd_float4x4とSCNMatrix4Multで逆です。

まとめ

とても紛らわしいですね。

しかし、まとめると基本的にはBAvと考えておいて、simd_float4x4で行列を掛け算するときだけvABと考えれば良いようです。

左から右に進んで行った方が時系列がわかりやすいので、SceneKitで行列を乗算するときはSCNMatrix4Multではなくsimd_float4x4で行おう。

参考文献

https://developer.apple.com/documentation/scenekit/1409697-scnmatrix4mult
https://developer.apple.com/documentation/accelerate/simd/working_with_matrices

関連話題のstack overflow

https://stackoverflow.com/a/43258320/10339551