🌈

そろそろShaderをやるパート57 行列を理解する(回転編)

2022/02/26に公開

そろそろShaderをやります

そろそろShaderをやります。そろそろShaderをやりたいからです。
パート100までダラダラ頑張ります。10年かかってもいいのでやります。
100記事分くらい学べば私レベルの初心者でもまあまあ理解できるかなと思っています。

という感じでやってます。

※初心者がメモレベルで記録するので
 技術記事としてはお力になれないかもしれません。

下準備

下記参考
そろそろShaderをやるパート1 Unite 2017の動画を見る(基礎知識~フラグメントシェーダーで色を変える)

デモ

頂点シェーダーを行列の計算式で回転させています。

行列の旨味の理解は下記記事でまとめました。
【参考リンク】:そろそろShaderをやるパート56 行列を理解する(平行移動編)

回転の理屈

まずは2次元座標における回転の理屈を考えます。

図を基に解説します。
OPとx軸の成す角をαとします。成す角と回転を分かり易くするために
円を補助線として作図しています。円の半径をrとします。

この時点で座標Pは三角関数を用いてx,yそれぞれを下記の式で表せます。

\begin{cases}\ x=rcosα \\\ y=rsinα \end{cases}

次にQの位置までPを回転させたシチュエーションを考えます。

この時、座標Qは三角関数を用いてx',y'それぞれを下記の連立方程式で表せます。

\begin{cases}\ x'=rcos(α+θ) \\\ y'=rsin(α+θ) \end{cases}

加法定理を用いると下記のように式を変形できます。

\begin{cases}\ x'=r(cosαcosθ-sinαsinθ) \\\ y'=r(sinαcosθ+cosαsinθ) \end{cases}

展開して、座標Pのx,yの値を代入してみます。

\begin{cases}\ x'=rcosαcosθ-rsinαsinθ \\\ y'=rsinαcosθ+rcosαsinθ \end{cases}
\begin{cases}\ x'=xcosθ-ysinθ \\\ y'=ycosθ+xsinθ \end{cases}

回転後の座標Q(x',y')を座標P(x,y)で表すことができました。

行列への変換

ここまで来れば行列に直すのは簡単です。
先ほどの最終的な計算結果を行列に直したものが下記です。

\begin{pmatrix} x' \\ y' \\ \end{pmatrix} = \begin{pmatrix} cosθ & -sinθ \\ sinθ & cosθ \\ \end{pmatrix} \begin{pmatrix} x \\ y \\ \end{pmatrix}

あとはこれを前回記事で書いた同時座標系で扱い、
アフィン変換のメリットを受けやすい形へ変換します。

\begin{pmatrix} x' \\ y' \\ 1\\ \end{pmatrix} = \begin{pmatrix} cosθ & -sinθ & 0 \\ sinθ & cosθ & 0 \\ 0 & 0 & 1\\ \end{pmatrix} \begin{pmatrix} x \\ y \\ 1 \\ \end{pmatrix}

これで完成です。あとはこの変換を三次元で取り扱うだけです。

  • X軸を中心にθ度回転させる場合
\begin{pmatrix} x' \\ y' \\ z' \\ 1\\ \end{pmatrix} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & cosθ & -sinθ & 0 \\ 0 & sinθ & cosθ & 0 \\ 0 & 0 & 0 & 1\\ \end{pmatrix} \begin{pmatrix} x \\ y \\ z \\ 1 \\ \end{pmatrix}
  • Y軸を中心にθ度回転させる場合
\begin{pmatrix} x' \\ y' \\ z' \\ 1\\ \end{pmatrix} = \begin{pmatrix} cosθ & 0 & sinθ & 0 \\ 0 & 1 & 0 & 0 \\ -sinθ & 0 & cosθ & 0 \\ 0 & 0 & 0 & 1\\ \end{pmatrix} \begin{pmatrix} x \\ y \\ z \\ 1 \\ \end{pmatrix}
  • Z軸を中心にθ度回転させる場合
\begin{pmatrix} x' \\ y' \\ z' \\ 1\\ \end{pmatrix} = \begin{pmatrix} cosθ & -sinθ & 0 & 0 \\ sinθ & cosθ & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1\\ \end{pmatrix} \begin{pmatrix} x \\ y \\ z \\ 1 \\ \end{pmatrix}

Shaderサンプル

Shader "Custom/MatrixRotation"
{
    Properties
    {
        //メインテクスチャー
        [NoScaleOffset]_MainTex ("Texture", 2D) = "white" {}
        //キーワードのEnumを定義できる なぜか変数名が大文字でないと変更が反映されなかった
        [KeywordEnum(X,Y,Z)] _AXIS("Axis",Int) = 0
        _Rotation("Rotation",Range(-6.28,6.28)) = 0
    }
    SubShader
    {
        //両面描画
        Cull off

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            //ここでシェーダーキーワードを定義する
            #pragma multi_compile _AXIS_X _AXIS_Y _AXIS_Z

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            sampler2D _MainTex;
            float _Rotation;

            v2f vert(appdata v)
            {
                v2f o;
                // 回転行列を作る
                half c = cos(_Rotation);
                half s = sin(_Rotation);

                #ifdef _AXIS_X
                
                //X軸中心の回転 定義したキーワードで判定
                half4x4 rotateMatrixX = half4x4(1, 0, 0, 0,
                                               0, c, -s, 0,
                                               0, s, c, 0,
                                               0, 0, 0, 1);
                v.vertex = mul(rotateMatrixX, v.vertex);

                #elif _AXIS_Y

                //Y軸中心の回転 定義したキーワードで判定
                half4x4 rotateMatrixY = half4x4(c, 0, s, 0,
                                               0, 1, 0, 0,
                                               -s, 0, c, 0,
                                               0, 0, 0, 1);
                v.vertex = mul(rotateMatrixY, v.vertex);

                #elif _AXIS_Z

                //Z軸中心の回転 定義したキーワードで判定
                half4x4 rotateMatrixZ = half4x4(c, -s, 0, 0,
                                               s, c, 0, 0,
                                               0, 0, 1, 0,
                                               0, 0, 0, 1);
                v.vertex = mul(rotateMatrixZ, v.vertex);

                #endif

                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                //テクスチャーのサンプリング
                fixed4 tex_color = tex2D(_MainTex, i.uv);
                return tex_color;
            }
            ENDCG
        }
    }
}

先ほどの回転行列を頂点シェーダーに反映させています。

KeywordEnum

MaterialPropertyDrawerという"MaterialのプロパティのInspector上での表示"を
拡張するための機能が存在します。

KeywordEnumはUnityに組み込みで実装されており、キーワード列挙型で任意のキーワードをInspectorに表示できます。

以下のように使用します。

[KeywordEnum(X,Y,Z)] _AXIS("Axis",Int) = 0

#pragma multi_compile _AXIS_X _AXIS_Y _AXIS_Z

#ifdef _AXIS_X
    //ここに_AXIS_Xの場合の処理を書く       
#endif

私の環境だけかもしれませんが、シェーダーキーワードに小文字が含まれていると正しく値が変更されませんでした。

参考リンク

シェーダーでトグル・列挙型のプロパティを使う
MVP行列による座標変換について

Discussion