🌈

そろそろShaderをやるパート56 行列を理解する(平行移動編)

2022/02/26に公開

そろそろShaderをやります

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

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

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

下準備

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

デモ

頂点シェーダーを行列の計算式を用いて移動させています。

行列

下記記事が非常に参考になりました。
【参考リンク】:MVP行列による座標変換について

行列の旨味を理解する上で連立方程式の例が最高にわかりやすかったです。

プリンを1個とヨーグルトを2個のセットAがあります。 プリンを2個とヨーグルトを3個のセットBがあります。 セットAを2個とセットBを1個買ったら960円でした。 セットAを2個とセットBを2個買ったら1400円でした。 プリンとヨーグルトはそれぞれ何円でしょう。 セットの値段に割引はないものとします。

これを連立方程式で表すと下記のようになります。

\begin{cases}\ x'=1x+2y \\\ y'=2x+3y \end{cases}
\begin{cases}\ 960=2x'+1y'\\\ 1400=2x'+2y' \end{cases}

そしてこの連立方程式を行列で表すとそれぞれ下記のようになります。

\begin{pmatrix} x' \\ y' \\ \end{pmatrix} = \begin{pmatrix} 1 & 2 \\ 2 & 3 \\ \end{pmatrix} \begin{pmatrix} x \\ y \\ \end{pmatrix}
\begin{pmatrix} 960 \\ 1400 \\ \end{pmatrix} = \begin{pmatrix} 2 & 1 \\ 2 & 2 \\ \end{pmatrix} \begin{pmatrix} x' \\ y' \\ \end{pmatrix}

x'とy'が一致しているので代入してみます。

\begin{pmatrix} 960 \\ 1400 \\ \end{pmatrix} = \begin{pmatrix} 2 & 1 \\ 2 & 2 \\ \end{pmatrix} \begin{pmatrix} 1 & 2 \\ 2 & 3 \\ \end{pmatrix} \begin{pmatrix} x \\ y \\ \end{pmatrix}

このように、複雑な式をひとまとめにすることができます。
これが行列の掛け算の強みとのことです。

今回の記事で平行移動について触れますが、回転や拡大縮小も同様に行列で表現が可能です。
行列同士での乗算により面倒な計算を一気に行えるようになります。

移動の理屈

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

図を基に解説します。
以下はPをQの位置に移動させた際の図です。

P(x,y)をそれぞれTx,Ty分移動した座標がQ(x',y')です。
これを式に示すと以下のようになります。

\begin{cases}\ x'=x + T_x \\\ y'=y + T_y \end{cases}

この連立方程式を先ほどの行列の説明で行ったように、
乗算のみで構成された行列の計算式に変換したいところですが、このままでは不可能です。
項(+ Tx+ Tyの箇所)が増えたことにより乗算のみで構成された行列の計算式で表せなくなっています。

\begin{pmatrix} x' \\ y' \\ \end{pmatrix} = \begin{pmatrix} 1 & 0 \\ 0 & 1 \\ \end{pmatrix} \begin{pmatrix} x \\ y \\ \end{pmatrix} + \begin{pmatrix} T_x \\ T_y \\ \end{pmatrix}

これでは"行列同士での乗算により面倒な計算を一気に行える"という旨味が失われてしまいます。

同次座標

この問題に対処すべく、同次座標を用います。

同次座標はn次元の座標に1次元追加することで表されます。
平行移動、回転、拡大縮小などの変換(アフィン変換と言います)を行う上で必要な
"乗算のみで構成された行列"を導き出すために役立ちます。

下記図は"xy平面の成す2次元座標Pにz軸方向の次元を付与した同時座標"を図示したものです。
P'はPの同一直線上にあり、Pをz'/z倍した座標となっています。

P、P'それぞれの同次座標と直交座標(1次元追加する前の座標)は以下のような関係性を示します。
追加した(n+1)次元目の数値で各座標が除算されています。

同次座標 直交座標
P(x,y,z) (x/z,y/z)
P'(xz',yz',z') (x,y)

わかりづらいので取り得る実数を当てはめたものが下記です。
いずれも直交座標においては同じ値を示していることがわかります。
これが同次座標の特性です。

同次座標 直交座標
(6,4,2) (3,2)
(3,2,1) (3,2)

加えて、追加した(n+1)次元目の実数に1を用いることで、
同次座標から"追加した(n+1)次元目"を除いた値に
変化がないことが上記の表からわかります。

すなわち、同次座標の計算結果をそのまま次の行列の計算に利用することができます。

平行移動行列

原理が理解できたのでいよいよ平行移動を行列を用いて式に落とし込みます。

先ほどの二次元座標での移動を示した連立方程式は以下です。

\begin{cases}\ x'=x + T_x \\\ y'=y + T_y \end{cases}

これに同次座標を用いて行列で表すと下記のようになります。
これが二次元座標の平行移動行列です。

\begin{pmatrix} x' \\ y' \\ 1 \end{pmatrix} = \begin{pmatrix} 1 & 0 & T_x \\ 0 & 1 & T_y \\ 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} x \\ y \\ 1 \end{pmatrix}

元の連立方程式に戻せるか試しに計算してみました。

\begin{pmatrix} x' \\ y' \\ 1 \end{pmatrix} = \begin{pmatrix} x + T_x \\ y + T_y\\ 1 \end{pmatrix}

増やした次元の実数は1なので、同次座標と直交座標の関係性から値はそのままです。
結果最初の連立方程式の値となりました。

\begin{cases}\ x'=x + T_x \\\ y'=y + T_y \end{cases}

ここまでで、行列を用いて平行移動を計算できていることが確認できました。

あとはこの変換を下記のように三次元で取り扱うだけです。

\begin{pmatrix} x' \\ y' \\ z' \\ 1 \end{pmatrix} = \begin{pmatrix} 1 & 0 & 0 & T_x \\ 0 & 1 & 0 & T_y \\ 0 & 0 & 1 & T_z \\ 0 & 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix}

Shaderサンプル

Shader "Custom/MatrixMove"
{
    Properties
    {
        [NoScaleOffset]_MainTex ("Texture", 2D) = "white" {}
        _MoveX("MoveX",Range(-0.5,0.5)) = 0
        _MoveY("MoveY",Range(-0.5,0.5)) = 0
        _MoveZ("MoveZ",Range(-0.5,0.5)) = 0
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            sampler2D _MainTex;
            float _MoveX;
            float _MoveY;
            float _MoveZ;

            v2f vert(appdata v)
            {
                v2f o;
                //移動のための行列を計算
                half4x4 moveMatrix = half4x4(1, 0, 0, _MoveX,
                                             0, 1, 0, _MoveY,
                                             0, 0, 1, _MoveZ,
                                             0, 0, 0, 1);
                v.vertex = mul(moveMatrix, v.vertex);
                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
        }
    }
}

先ほどの平行移動行列を頂点シェーダーに反映させています。
行列の掛け算にはmul(Multipleの"マル"らしい)を使用します。
行列乗算を行う際に使用する関数です。

参考リンク

カメラの位置・姿勢推定0 同次座標・斉次座標の導入
同次座標の説明と3次元座標と相互変換する方法
同次座標系(Homogeneous Coordinates)
同次座標 ( 斉次座標 ) とは
【Unity】同次座標系をマスターしよう -「w」の正体まで徹底図解!

Discussion