夜風のMixedReality

xRと出会って変わった人生と出会った技術を書き残すためのGeekなHoloRangerの居場所

ゼロから始めるUnityShader開発 第五章 ノーマルマップ その②

本日はShader勉強枠です。

前回の記事はこちらになります。

redhologerbera.hatenablog.com

〇ノーマルマップとは

 ノーマルマップはローポリゴンでありながら凸凹間を再現するために使用されるテクスチャとそれを用いた実装です。

 ノーマルマップを使用することで複雑な質感を再現できるため、多くのアプリケーションやゲームで採用されています。

  例えば次の画像ではノーマルマップがあることで筋肉感を再現しています。

使用した画像は次のようになります。

 

 このようにノーマルマップを使用している場合とそうではない場合で質感が大きく変わることがわかります。

〇ノーマルマップの実装

 ノーマルマップはタンジェント(Tangent)と呼ばれるものがキーワードになっています。

 筆者の理解ですので厳密にはより複雑な定義があると思われますが、3DCGのメッシュの向き=法線をノーマルと呼びます。

 ノーマルマップを使用するためにはメッシュごとではなく描画される際のピクセルとノーマルマップのテクセル(テクスチャのピクセル)を紐づけるためにモデルのピクセルごとの法線を求める必要があります。

 このことをタンジェント(Tangent)と呼びます。

〇構造体の定義

構造体は次のようになります。

   struct appdata
            {
                float4 vertex : POSITION;
                half3 normal: NORMAL;//法線
                float2 uv : TEXCOORD0;//ノーマルマップを使用するためのUV
                half4 tangent :TANGENT;//追加
            };
            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 normalWS : TEXCOORD1;//ワールド法線
                float2 uv : TEXCOORD0;//ノーマルマップのUV
                half3 tangentX : COLOR3; //ノーマルマップのタンジェント
                half3 tangentY : COLOR4;//ノーマルマップのタンジェント
                half3 tangentZ : COLOR5;//ノーマルマップのタンジェント
            };

ここではGraphicsToolsStandardShaderの実装にのっとって成分ごとのtangentを定義しています。(tangentX,tangentY,tangentZ)

〇頂点シェーダーの処理

頂点シェーダーで行う必要があるのは次のような処理です。

・頂点座標を処理

・メッシュの法線を取得

・ワールド座標の法線を取得

・ノーマルマップ用のUVを取得

・ピクセルごとの向き=タンジェントを取得

次のようになります。

  v2f vert(appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);
                //ライトの当たる向きを計算するため面の法線を取得
                VertexNormalInputs normal = GetVertexNormalInputs(v.normal);
                o.normalWS = normal.normalWS;
                o.uv = v.uv;
                //ワールド空間の Tangent へ変換
                half3 worldTangent = TransformObjectToWorldDir(v.tangent.xyz);
                half tangentSign = v.tangent.w * unity_WorldTransformParams.w;
                //ワールド空間のBitangentへ変換
                half3 worldBitangent = cross(o.normalWS, worldTangent) * tangentSign;
                o.tangentX = half3(worldTangent.x, worldBitangent.x, o.normalWS.x);
                o.tangentY = half3(worldTangent.y, worldBitangent.y, o.normalWS.y);
                o.tangentZ = half3(worldTangent.z, worldBitangent.z, o.normalWS.z);
                return o;
            }

ここで、worldBitangentはワールド座標のタンジェントとワールド座標の法線の外積になっています。

各成分のtangentにはワールド座標のTangent成分、Bitangent成分、ワールド法線成分の各成分が格納されています。

〇フラグメントシェーダー

フラグメントシェーダーで行う処理は次のようなものです

・ライトの取得

・NormalMapの展開

・Tangentに適応

float4 frag(v2f i, bool facing : SV_IsFrontFace) : SV_Target
            {
                float4 col = _MainColor;
                //Light.hlslで提供されるUnityのライトを取得する関数
                Light lt = GetMainLight();

                //NormalMapの実装
                half3 worldNormal;
                half3 tangentNormal = UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, i.uv));
                worldNormal.x = dot(i.tangentX, tangentNormal);
                worldNormal.y = dot(i.tangentY, tangentNormal);
                worldNormal.z = dot(i.tangentZ, tangentNormal);
                worldNormal = normalize(worldNormal) * (facing ? 1.0 : -1.0);

                //ライトの向きを計算
                float strength = max(0., dot(lt.direction, worldNormal));
                float4 lightColor = float4(lt.color, 1);
                return col * lightColor * strength;
            }

 NormalMapはUnpackNormal()というノーマルマップ専用のサンプリング関数を用いています。

UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, i.uv))

通常のサンプリング同様一度uv座標にのっとって展開しています。

タンジェントの取得法としてtangentとノーマルマップからサンプリングしたデータを内積しています。

 これを正規化し、単位ベクトルに戻したものに面の向きをかけたものがピクセルごとの向きになります。

 つまり、疑似的に面を凸凹させています。

 lightの向きで影が付くようになっていますが、メッシュの面ではなくノーマルマップを適応した疑似的な面で影をつけることで細かいdetailを再現しています。

 本日は以上です。

〇コード一覧

Shader "Unlit/SimpleNormalMap"
{
    Properties
    {
        _MainColor("MainColor",color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
        _NormalMap("NormalMap",2D) = "bump" {}
    }
    SubShader
    {
        Tags
        {
            "RenderType"="Opaque"
        }
        LOD 100

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include  "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"


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

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 normalWS : TEXCOORD1;
                float2 uv : TEXCOORD0;
                half3 tangentX : COLOR3; //ノーマルマップのタンジェント
                half3 tangentY : COLOR4;
                half3 tangentZ : COLOR5;
            };


            float4 _MainColor;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);
                //面の法線を取得、ライトの当たる向きを計算
                VertexNormalInputs normal = GetVertexNormalInputs(v.normal);
                o.normalWS = normal.normalWS;
                o.uv = v.uv;
                //ワールド空間の Tangent へ変換
                half3 worldTangent = TransformObjectToWorldDir(v.tangent.xyz);
                half tangentSign = v.tangent.w * unity_WorldTransformParams.w;
                //ワールド空間のBitangent(従法線)へ変換
                half3 worldBitangent = cross(o.normalWS, worldTangent) * tangentSign;
                o.tangentX = half3(worldTangent.x, worldBitangent.x, o.normalWS.x);
                o.tangentY = half3(worldTangent.y, worldBitangent.y, o.normalWS.y);
                o.tangentZ = half3(worldTangent.z, worldBitangent.z, o.normalWS.z);
                return o;
            }

            TEXTURE2D(_NormalMap);
            SAMPLER(sampler_NormalMap);

            float4 frag(v2f i, bool facing : SV_IsFrontFace) : SV_Target
            {
                float4 col = _MainColor;
                //Light.hlslで提供されるUnityのライトを取得する関数
                Light lt = GetMainLight();

                //NormalMapの実装
                half3 worldNormal;
                half3 tangentNormal = UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, i.uv));
                worldNormal.x = dot(i.tangentX, tangentNormal);
                worldNormal.y = dot(i.tangentY, tangentNormal);
                worldNormal.z = dot(i.tangentZ, tangentNormal);
                worldNormal = normalize(worldNormal) * (facing ? 1.0 : -1.0);

                //ライトの向きを計算
                float strength = max(0., dot(lt.direction, worldNormal));
                float4 lightColor = float4(lt.color, 1);
                return col * lightColor * strength;
            }
            ENDHLSL
        }
    }
}