【Unity】【シェーダ】フラグメントシェーダで接空間<->ワールド空間変換

フラグメントシェーダで接空間<->ワールド空間変換する方法です。

課題

たとえばノーマルマップを適用した法線に対してキューブマッピングをすることを考えます。

まずは普通に、フラグメントシェーダでノーマルマップから接空間における法線を取得します。
これをキューブマッピングするわけですが、キューブマップをサンプリングする関数にはワールド空間の法線が必要です。
つまりフラグメントシェーダで接空間からワールド空間に座標変換する必要があります。

以下、このようなケースの解決方法を解説します。

シェーダ

まずはシェーダです。

Shader "TangentToWorldFragment"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag

           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex       : POSITION;
                half3 normal        : NORMAL;
                half4 tangent       : TANGENT;
            };

            struct v2f
            {
                float4 pos          : SV_POSITION;
                half3 normal        : TEXCOORD2;
                half4 tangent       : TEXCOORD3;
                half3 binormal      : TEXCOORD4;
            };

            v2f vert (appdata v)
            {
                v2f o           = (v2f)0;
                o.pos           = UnityObjectToClipPos(v.vertex);

                // 各ベクトルをワールド空間で求める
                // binormalはtangentのwとunity_WorldTransformParams.wを掛ける(Unityの決まり)
                o.binormal      = normalize(cross(v.normal.xyz, v.tangent.xyz) * v.tangent.w * unity_WorldTransformParams.w);
                o.normal        = UnityObjectToWorldNormal(v.normal);
                o.tangent       = mul(unity_ObjectToWorld, v.tangent.xyz);
                o.binormal      = mul(unity_ObjectToWorld, o.binormal);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 方法①
                // ワールド空間 > 接空間の変換行列を作り、
                // Transposeして接空間 > ワールド空間の変換行列にする
                half3x3 tangentToWorldMatrix    = transpose(half3x3(i.tangent.xyz, i.binormal, i.normal));

                // 方法②
                // 接空間のベクトルをワールド空間に変換するだけなら行列をつくらずこのように計算したほうが早い
                half3 someVector;
                half3 worldNormal           = (i.tangent * someVector.x) + (i.binormal * someVector.y) + (i.normal * someVector.z);

                return 1;
            }
            ENDCG
        }
    }
}

フラグメントシェーダで座標変換をするために、まずは頂点シェーダでワールド座標の法線、接線、従法線を求めてフラグメントシェーダに渡しています。

フラグメントシェーダでは、先ほど求めたそれら用いて変換行列を作成します。
ここについてはベクトルの「変換後の空間の3軸のベクトルから行列を作ると変換行列になる」という性質を使っています。
また今回の場合、これを転置することで逆行列が求められます。
この行列を使えばフラグメントシェーダで接空間<->ワールド空間の変換ができます。

ただし、接空間 -> ワールド空間変換だけで反対方向の変換をしないのであれば、
方法②のほうが処理負荷が低いのでお勧めです。

ノーマルマッピング+キューブマッピング

これの実用例として、下記の記事でノーマルマッピングとキューブマッピングを組み合わせた実装を紹介しています。

light11.hatenadiary.com

関連

light11.hatenadiary.com

light11.hatenadiary.com

参考

catlikecoding.com