🌈

そろそろShaderをやるパート76 ノーマルマッピング

2022/08/31に公開

そろそろShaderをやります

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

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

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

下準備

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

デモ

反射に応じて平面であるはずのPlaneに凹凸があるように見える表現です。

ノーマルマップは以下サイトからお借りしました。
【参考リンク】:【Unity】ノーマルマッピング

Shaderサンプル

Shader "Custom/NormalMap"
{
    Properties
    {
        //ここに書いたものがInspectorに表示される
        _MainColor("MainColor",Color) = (1,1,1,1)
        _Reflection("Reflection", Range(0, 10)) = 1
        _Specular("Specular", Range(0, 10)) = 1
        _NormalMap ("Normal map", 2D) = "bump" {}
    }
    SubShader
    {
        Tags
        {
            "RenderType"="Opaque"
            "LightMode"="ForwardBase"
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal: NORMAL;
                float4 tangent : TANGENT;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 lightDir : TEXCOORD1;
                float3 viewDir : TEXCOORD2;
            };

            //変数の宣言 Propertiesで定義した名前と一致させる
            float4 _MainColor;
            float _Reflection;
            float _Specular;
            sampler2D _NormalMap;

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

                //接空間の行列を取得
                TANGENT_SPACE_ROTATION;
                //ライトの方向ベクトルを接空間に変換
                o.lightDir = normalize(mul(rotation, ObjSpaceLightDir(v.vertex)));
                //カメラの方向ベクトルを接空間に変換
                o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                //ノーマルマップから法線を取得
                float3 normal  = UnpackNormal(tex2D(_NormalMap, i.uv));
                //ライトベクトルと法線ベクトルから反射ベクトルを計算
                float3 refVec = reflect(-i.lightDir, normal);
                //反射ベクトルと視線ベクトルの内積を計算
                float dotVR = dot(refVec, i.viewDir);
                //0以下は利用しないように内積の値を再計算
                dotVR = max(0, dotVR);
                dotVR = pow(dotVR, _Reflection);
                float3 specular = _LightColor0.xyz * _Specular;
                //内積を補間値として塗分け
                float4 finalColor = lerp(_MainColor, float4(specular, 1), dotVR);
                return finalColor;
            }
            ENDCG
        }
    }
}

反射の処理については、以前書いたPhong鏡面反射と同様の仕組みを組み込んでいます。
【参考リンク】:そろそろShaderをやるパート74 Phong鏡面反射

接空間

ノーマルマップを利用するうえで必要なのが、接空間です。
接空間とはテクセルの位置に接する平面を基準とした座標空間のことです。
【引用元】:法線マッピングの要、「接空間」

テクスチャに焼き付けられた凹凸の情報は、
接空間上において各テクセルの法線はどこを向いているか、を表します。
それらを読みだして凹凸を疑似的に再現する計算を行います。

TANGENT_SPACE_ROTATION

接空間へ変換するための行列を計算するマクロです。

内部では以下の計算を行っています。

// Declares 3x3 matrix 'rotation', filled with tangent space basis
#define TANGENT_SPACE_ROTATION \
    float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
    float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )

利用する際はマクロに定義された変数名正しいセマンティクスの指定が必要です。

float3 normal: NORMAL;
float4 tangent : TANGENT;

マクロの計算結果であるrotationを利用して
視線ベクトルとライトベクトルを接空間に変換します。

//ライトの方向ベクトルを接空間に変換
o.lightDir = normalize(mul(rotation, ObjSpaceLightDir(v.vertex)));
//カメラの方向ベクトルを接空間に変換
o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));

ObjSpaceLightDirとObjSpaceViewDirはそれぞれ正規化されていない値を返すため、
normalizeで正規化する必要があります。

参考リンク

Unityのシェーダーでノーマルマップを実装するには
法線マップをオブジェクト空間へ変換しシェーディング
【連載】Unity時代の3D入門 – 第9回「ノーマルマッピング」
[Unity] バンプマッピング(法線マッピング)をやってみる
法線マップと接空間

Discussion