したかみ ぶろぐ

Unity成分多め

3D数学の復習と実践(回転とスケーリング)

始めに

部活での勉強会にて「Quaternion理解している人いますか?」って質問で誰も手を上げる人がいませんでした。かくいう私も理解してませんでした。

ってことで5月6月ぐらいに勉強してブログでまとめようとしましたが、マークダウンで数式を書くのが面倒くさ過ぎてずっとやってませんでした。

しかし、前回GPUインスタンシングを勉強した際にシェーダー内で座標や回転を求めていたので覚悟を決めてブログにまとめていこうと思います。

今回参考にするのは以下のサイトと参考書です。

実例で学ぶゲーム3D数学

実例で学ぶゲーム3D数学

docs.google.com

準備

3D数学の学習はUnityのシェーダーを使用しながら確認していきたいと思います。

理由としては

  • Unity C#は3D数学を知らなくても座標、回転をいじれる
  • Shaderでは行列計算を使用しないとそれらを行えない
  • 今回作成したシェーダーをGPUインスタンシングに流用したい



実験用シェーダーを作成

行列をいじいじするためのシェーダーをとりあえず作ってCubeにはっつけます。

シェーダーでテキトーに行列の値を決めてそれを頂点座標と掛け合わせます。


\begin{pmatrix}
V_x \\ V_y \\ V_z  \end{pmatrix}
=
\begin{pmatrix} 
V_x.x & V_x.y & V_x.z \\
V_y.x & V_y.y & V_y.z \\
V_z.x & V_z.y & V_z.z \\
\end{pmatrix}


Shader "Unlit/Test_3DMath"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _XVector("X Vector", Vector) = (1, 0, 0, 0)
        _YVector("Y Vector", Vector) = (0, 1, 0, 0)
        _ZVector("Z Vector", Vector) = (0, 0, 1, 0)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _XVector;
            float4 _YVector;
            float4 _ZVector;

            v2f vert (appdata v)
            {
                v2f o;

                float4x4 mat = float4x4(
                    _XVector,
                    _YVector,
                    _ZVector,
                    float4(0, 0, 0, 1));
        o.vertex = mul(v.vertex, mat);
                o.vertex = UnityObjectToClipPos(o.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

f:id:vxd-naoshi-19961205-maro:20191130021240p:plain


マテリアルのプロパティは次のようになります。

f:id:vxd-naoshi-19961205-maro:20191130021420p:plain



プロパティをいじいじしてみる

テキトーに X Vector の X の値を変更するとオブジェクトのX軸での大きさが変わります。

f:id:vxd-naoshi-19961205-maro:20191130022207p:plain
Vector Xの値を(2, 0, 0, 0)に変更してみる。 transformのScaleを (2, 1, 1) に設定した場合と同じような結果になりました。


次にZ Vector の値を (0, 0, 0, 0) に設定するとXY平面のQuadみたくなりました。

f:id:vxd-naoshi-19961205-maro:20191130023110p:plain


最後にすべての値をテキトーに設定するとえらいこっちゃになります。

f:id:vxd-naoshi-19961205-maro:20191130023804p:plain


行列って理解しないとえげつない形を生み出してしまうパワーを持ってると感じます. . .



行列の意味を考えてみる

先程の例で設定した値ってどんな意味があるのってなるので勉強していきます。(解説で間違っている個所があるかもしれません!)

ここでは簡単に2次元で考えます。 先程のZ Vectorの値を (0, 0, 0, 0) に設定した状態から始めていきます。


オブジェクトには軸が存在します。 赤い矢印がX軸、緑の矢印がY軸になります。

f:id:vxd-naoshi-19961205-maro:20191130030610p:plain


この2つの軸をベクトルで表すと X軸 = (1, 0)、Y軸 = (0, 1) になります。これを行列で表現すると次のようになります。(軸行列とする)

f:id:vxd-naoshi-19961205-maro:20191130144940p:plain


ではこの行列の値を掛け算で変えていきます。 行列の掛け算は次のようになります。


\begin{pmatrix} a & b \\ c & d \end{pmatrix}\times \begin{pmatrix} e & f \\ g & h 
\end{pmatrix}
=
\begin{pmatrix} 
a \times e + b \times g & a \times f + b \times h \\
c \times e + d \times g & c \times f + d \times h \end{pmatrix}


因みに、軸行列との掛け算は掛けられる行列と等しくなります。


\begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}\times \begin{pmatrix} a & b \\ c & d 
\end{pmatrix}
=
\begin{pmatrix} 
1 \times a + 0 \times c & 1 \times b + 0 \times d \\
0 \times a + 1 \times c & 0 \times b + 1 \times d \end{pmatrix} \\\
=
\begin{pmatrix} a & b \\ c & d \end{pmatrix}


ってことで、X VectorがX軸のベクトル、Y VectorがY軸のベクトルになります。



では、X Vectorに (2, 0, 0, 0) を代入した場合を考えてみます。

X軸のベクトルが2倍になるのでX軸線上に伸びます。

f:id:vxd-naoshi-19961205-maro:20191130150332p:plain


X Vectorに (1, -1, 0, 0) を代入した場合は次のようにX軸が右下方向を向くので平行四辺形のような形になります。

f:id:vxd-naoshi-19961205-maro:20191130151208p:plain


最後にX Vectorに(0.8, -0.7)、Y Vectorに(0.5, 1)を代入したときは次のようになります。

f:id:vxd-naoshi-19961205-maro:20191130152447p:plain


いくつかの例を試して、行列のそれぞれの行が軸ベクトルを表していることが分かりました。



2次元での回転

軸ベクトルをいじいじして回転を表現します。

回転は次のように表現されます。

f:id:vxd-naoshi-19961205-maro:20191130155412p:plain


これをベクトルで表現すると


R(θ) = 
\begin{pmatrix} cosθ & sinθ \\ -sinθ & cosθ \end{pmatrix}


これをシェーダーで実装します。 このシェーダーでも分かり易いよう2Dにしています。

            v2f vert (appdata v)
            {
                v2f o;

                float4x4 mat = float4x4(
                    float4(cos(_theta), sin(_theta), 0, 0),
                    float4(-sin(_theta), cos(_theta), 0, 0),
                    float4(0, 0, 0, 0),
                    float4(0, 0, 0, 1));
                o.vertex = mul(v.vertex, mat);

                o.vertex = UnityObjectToClipPos(o.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }


結果、このように回転が出来るシェーダーが出来ました。

f:id:vxd-naoshi-19961205-maro:20191130165333g:plain



3次元の回転

では2次の回転を3次元に応用していきます。

X軸、Y軸、Z軸での回転行列は次のようになります。  


R_x(θ) = 
\begin{pmatrix} 
1 & 0 & 0 \\ 
0 & cosθ & sinθ \\ 
0 & -sinθ & cosθ \end{pmatrix}


R_y(θ) = 
\begin{pmatrix} 
cos\theta & 0 & -sin\theta \\ 
0 & 1 & 0 \\ 
sin\theta & 0 & cosθ \end{pmatrix}


R_z(θ) = 
\begin{pmatrix} 
cos\theta & sin\theta & 0 \\ 
-sin\theta & cos\theta & 0 \\ 
0 & 0 & 1 \end{pmatrix}


これらの回転行列をシェーダーで実装します。

    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ThetaX ("Theta X", float) = 0
        _ThetaY ("Theta Y", float) = 0
        _ThetaZ ("Theta Z", float) = 0
    }
. . . . . 
. . . . .
. . . . .
            float _ThetaX;
            float _ThetaY;
            float _ThetaZ;

            v2f vert (appdata v)
            {
                v2f o;

                _ThetaX = radians(_ThetaX);
                _ThetaY = radians(_ThetaY);
                _ThetaZ = radians(_ThetaZ);

                float sinX = sin(_ThetaX);
                float cosX = cos(_ThetaX);

                float4x4 rotX = float4x4(
                    float4(1, 0, 0, 0),
                    float4(0, cosX, sinX, 0),
                    float4(0, -sinX, cosX, 0),
                    float4(0, 0, 0, 1));

                float sinY = sin(_ThetaY);
                float cosY = cos(_ThetaY);

                float4x4 rotY = float4x4(
                    float4(cosY, 0, -sinY, 0),
                    float4(0, 1, 0, 0),
                    float4(sinY, 0, cosY, 0),
                    float4(0, 0, 0, 1));

                float sinZ = sin(_ThetaZ);
                float cosZ = cos(_ThetaZ);

                float4x4 rotZ = float4x4(
                    float4(cosZ, sinZ, 0, 0),
                    float4(-sinZ, cosZ, 0, 0),
                    float4(0, 0, 1, 0),
                    float4(0, 0, 0, 1));

                v.vertex = mul(v.vertex, rotX);
                v.vertex = mul(v.vertex, rotY);
                v.vertex = mul(v.vertex, rotZ);

                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }


結果が以下のようになりました。 シェーダー内でオブジェクトを回転することが出来ました。

f:id:vxd-naoshi-19961205-maro:20191130193557g:plain



(追記)スケーリング

回転の解説に一生懸命になりすぎて、スケーリング(拡大縮小)について解説するのを失念しておりました。

軸ベクトルの大きさを変えることでスケーリングを行えます。 スケーリングの際は以下の行列を掛けることで出来ます。


S(s_x, s_y, s_z) = 
\begin{pmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & s_z  \end{pmatrix}


この機能もシェーダーに追加します。

            v2f vert (appdata v)
            {
                v2f o;

                float4x4 scale = float4x4(
                    float4(_ScaleX, 0, 0, 0),
                    float4(0, _ScaleY, 0, 0),
                    float4(0, 0, _ScaleZ, 0),
                    float4(0, 0, 0, 1));

                                       . . . . . .
                                       . . . . . .
            v.vertex = mul(v.vertex, scale);
            v.vertex = mul(v.vertex, rotX);
            v.vertex = mul(v.vertex, rotY);
            v.vertex = mul(v.vertex, rotZ);
                                      . . . . . . . .


注意ですが、スケーリング → 回転の順で行わなければ可笑しなことになります

実験でやってみましたが凄いことになりました。

f:id:vxd-naoshi-19961205-maro:20191202000138g:plain



最後に

今回は回転を主なテーマとして扱いました。

行列を勉強した際、「何を表してんだこいつ. . . 」ってなり苦手意識が強かったです。(授業でも赤点取って追試を受けた)

それでもUnityとシェーダーを使って勉強するうちに行列の理解と凄さを実感しました。

次回はシェーダーでオブジェクトの平行移動についてまとめていく予定です。


追記

3D数学について学んだことをまとめました。

次回 shitakami.hatenablog.com


次々回 shitakami.hatenablog.com


さらその次

shitakami.hatenablog.com