2020年12月3日木曜日

Unityシェーダでのベクトルと行列の掛け算の仕組み

ややこしいUnityの座標系の確認方法」シリーズ. UnityのC#スクリプトからCgのシェーダにカメラの姿勢など行列やベクトルを受け渡すようなプログラムを書く場合は,仕様違いで頭がこんがらがってくる. 右手座標系なのか左手座標系なのか?ベクトルと行列の掛け算は,列ベクトルなのか行ベクトルなのか? マニュアルやブログに記載はあるものの正確に書かれていなくて結局よく分からない. 今夏は,実際にプログラムを書いて結果を見てどちらなのかを確認する. 分かりにくいマニュアルや信頼できないブログは一切読まない(読むのが苦手..).

Cgでのベクトルと行列の演算について

C#とCgの間をいきなり考えると分からないことだらけなので, まずはCgの中だけでの演算について確認する. Cgには行列matとベクトルvecを掛け算したい場合にmul(mat, vec)という関数が使える. これが[行ベクトル][行列]の積なのか,[行列][列ベクトル]なのかを確認する.


実験1

具体的にはこんな場合.Cgのフラグメントシェーダに行列とベクトルの演算を書いて結果の色で確認する.

fixed4 frag (v2f i) : SV_Target
{
    float4 vec = float4(1, 0, 0, 1);
    float4x4 mat;
    mat._11_21_31_41 = float4(1, 0, 0, 0);
    mat._12_22_32_42 = float4(1, 0, 0, 0);
    mat._13_23_33_43 = float4(1, 0, 0, 0);
    mat._14_24_34_44 = float4(0, 0, 0, 1);
    vec = mul(mat, vec);

    return vec;
}

何が難しいんだと感じる人は,色んな言語を完全にマスターしている達人か, この言語の結果だけ知っていてこれが当たり前だと思い込んでいる初心者プログラマ. 中級プログラマは,これまでの経験から色んな可能性を考えてしまって逆に混乱するはず.

というのも

float4 vec = float4(1, 0, 0, 1);

が \begin{equation} \left[ \begin{array}{cccc} 1 & 0 & 0 & 1 \end{array} \right] \end{equation} の意味なのか \begin{equation} \left[ \begin{array}{c} 1\\ 1 \\ 0 \\ 1 \end{array} \right] \end{equation} の意味なのか分からない.さらに,

mat._11_21_31_41 = float4(1, 0, 0, 0);

の代入が \begin{equation} \left[ \begin{array}{cccc} 1 & & & \\ 0 & & & \\ 0 & & & \\ 0 & & & \\ \end{array} \right] \end{equation} の意味なのか \begin{equation} \left[ \begin{array}{cccc} 1 & 0 & 0 & 0 \\ & & & \\ & & & \\ & & & \\ \end{array} \right] \end{equation} の意味なのかも分からない.

予想1:白になる

「(1)かつ(3)の場合」OR「(2)かつ(4)の場合」は白になるはず.なぜならそれぞれを計算すると

\[ \left[ \begin{array}{cccc} 1 & 0 & 0 & 1 \end{array} \right] \left[ \begin{array}{cccc} 1 & 1 & 1 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ \end{array} \right] = \left[ \begin{array}{cccc} 1 & 1 & 1 & 1 \end{array} \right] \ \ \ {\rm OR}\ \ \ \left[ \begin{array}{cccc} 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ \end{array} \right] \left[ \begin{array}{c} 1 \\ 0 \\ 0 \\ 1 \end{array} \right] = \left[ \begin{array}{c} 1 \\ 1 \\ 1 \\ 1 \end{array} \right] \]

となる.つまり,どちらの場合でも,(R,G,B,A)=(1, 1, 1, 1)となるので白が表示されるはず.

予想2:赤になる

「(2)かつ(3)の場合」OR「(1)かつ(4)の場合」は赤になるはず.なぜならそれぞれを計算すると

\[ \left[ \begin{array}{cccc} 1 & 0 & 0 & 1 \end{array} \right] \left[ \begin{array}{cccc} 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ \end{array} \right] = \left[ \begin{array}{cccc} 1 & 0 & 0 & 1 \end{array} \right] \ \ \ {\rm OR}\ \ \ \left[ \begin{array}{cccc} 1 & 1 & 1 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ \end{array} \right] \left[ \begin{array}{c} 1 \\ 0 \\ 0 \\ 1 \end{array} \right] = \left[ \begin{array}{c} 1 \\ 0 \\ 0 \\ 1 \end{array} \right] \]

となる.つまり,どちらの場合でも,(R,G,B,A)=(1, 0, 0, 1)となるので赤が表示されるはず.

結果

結果はだった.ということは,「(2)かつ(3)」か「(1)かつ(4)」なのだけど,これはどちらでもいい. どちらかだと勝手に思い込んでも一貫している限りは本質的に等価で計算上は全く問題ないから. 行列もベクトルも,ソースコード上での要素の並びと「(2)かつ(3)の場合」の数式が一致する.

\[ \left[ \begin{array}{cccc} 1 & 0 & 0 & 1 \end{array} \right] \left[ \begin{array}{cccc} 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ \end{array} \right] = \left[ \begin{array}{cccc} 1 & 0 & 0 & 1 \end{array} \right] \]

自分は,列ベクトル派なので,ソースコード上の行列の要素の並びとイメージとが転置してで分かりにくい. そんな人は次の例を見て欲しい.

ちなみにシェーダの結果は再生ボタンを幼くても編集画面で即座に反映される. C#スクリプトにオブジェクトの色を指定するコードを書いていると, 編集画面ではシェーダが指定する色が表示され, 再生とともにC#で指定した色に変わる. シェーダのコードにエラーが有る場合はオブジェクトの色がピンク色(R,G,B)=(1,0,1)になる.


実験2

ちなみに行列の初期化にはもう一つ方法がある.この場合はどうなるのか?

fixed4 frag (v2f i) : SV_Target
{
    float4 vec = float4(1, 0, 0, 1);
    float4x4 mat;
    mat[0] = float4(1, 0, 0, 0);
    mat[1] = float4(1, 0, 0, 0);
    mat[2] = float4(1, 0, 0, 0);
    mat[3] = float4(0, 0, 0, 1);
    vec = mul(mat, vec);

    return vec;
}

結果はになる.この場合は自分もソースコード上の行列の要素の並びと頭の中のイメージが一致した. ベクトルはソースコード上では行ベクトルでも,脳内では無条件で列ベクトルと変換するのでこれで問題ない.

\[ \left[ \begin{array}{cccc} 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ \end{array} \right] \left[ \begin{array}{c} 1 \\ 0 \\ 0 \\ 1 \end{array} \right] = \left[ \begin{array}{c} 1 \\ 1 \\ 1 \\ 1 \end{array} \right] \]