知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【UnityShader】法線マップ #40

前回の成果

雪を降らせた。

soramamenatan.hatenablog.com


今回やること

法線マップを勉強します。

blog.applibot.co.jp


事前準備

Scene上にPlaneを配置し、Cameraから見えるように回転させてください。

f:id:soramamenatan:20200216121958p:plain

こちらの画像はShaderのプロパティのNormalMapにアタッチしてください。

f:id:soramamenatan:20200216142618p:plain

【連載】Unity時代の3D入門 – 第9回「ノーマルマッピング」 – てっくぼっと!:より引用


ソースコード

Shader "Unlit/Normalmap" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _NormalMap ("Normal map", 2D) = "bump" {}
        _Shininess ("Shininess", Range(0.0, 1.0)) = 0.078125
    }
    SubShader {
        Tags { "Queue"="Geometry" "RenderType"="Opaque"}

        Pass {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #include "UnityCG.cginc"

            #pragma vertex vert
            #pragma fragment frag

            float4 _LightColor0;
            sampler2D _MainTex;
            sampler2D _NormalMap;
            half _Shininess;

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

            struct v2f {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                half3 lightDir : TEXCOORD1;
                half3 viewDir : TEXCOORD2;
            };

            v2f vert(appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv  = v.texcoord.xy;
                TANGENT_SPACE_ROTATION;
                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));
                o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));
                return o;
            }

            float4 frag(v2f i) : COLOR {
                i.lightDir = normalize(i.lightDir);
                i.viewDir = normalize(i.viewDir);
                half3 halfDir = normalize(i.lightDir + i.viewDir);
                half4 tex = tex2D(_MainTex, i.uv);
                half3 normal = UnpackNormal(tex2D(_NormalMap, i.uv));
                half4 diff = saturate(dot(normal, i.lightDir)) * _LightColor0;
                half3 spec = pow(max(0, dot(normal, halfDir)), _Shininess * 128.0) * _LightColor0.rgb * tex.rgb;
                fixed4 color;
                color.rgb  = tex.rgb * diff + spec;
                return color;
            }
            ENDCG
        }
    }
}


法線マップとは

テクスチャ画像の情報を使い、平板なモデルの表面に凹凸があるかのように見せる手法。陰影情報が記録されたRGB画像を用いて、モデル表面の法線(normal)を変化させることで凹凸を表現する。

ノーマルマッピング | CG用語辞典 | CGWORLD Entry.jp:より引用

とのことです。

法線マップは青紫色(下記画像右)のようなもので、適応すると(下記画像左)凹凸を表現できます。

f:id:soramamenatan:20200216121702j:plain

法線マップをオブジェクト空間へ変換しシェーディング │ 空の缶詰:より引用


では、Shaderの解説に移ります。


TANGENT_SPACE_ROTATION

これは接空間への3*3の行列を生成するマクロです。
定義は以下となっています。

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

このマクロを解説するために必要な接空間から説明します。


接空間

これは3Dオブジェクトの各点で定義させるベクトル空間のことです。
下記の画像の灰色の半透明の平面を基準とした座標空間です。
そして、この平面の法線と該当の点の法線は一致します。

f:id:soramamenatan:20200216124110p:plain

接ベクトル空間 - Wikipedia:より引用


接空間を使用する理由

下記の画像の青軸は、3Dオブジェクトの表面の法線を表しています。
一般的なシェーディングでは、この法線を使用して陰影をつけていきます。
しかし、これは各点毎に固定された値を持っているので、凹凸を表現することができません
そこで、法線マップを使用して凹凸を表現するのですが、法線マップの存在する空間は接空間です。
ですので、接空間を使用します。

f:id:soramamenatan:20200216124640p:plain

法線マップと接空間 - しゅみぷろ:より引用


空間の変換

接空間を使用してシェーディングを行うためには、オブジェクトの法線マップの値とライトの方向ベクトルを同じ空間に変換する必要があります。
やり方としては、

  • 法線マップをライトやカメラの座標空間に変換
  • ライトやカメラを接空間に変換

の2つがあります。 前者の場合、法線マップはピクセルシェーダーで取得することになり、座標の変換処理がピクセル処理時に毎回行われます。
後者の場合、頂点シェーダーでライトやカメラのベクトルを変換するだけで良いので、前者に比べて計算量が少ないです。
ですので、今回は後者を使用します。


従法線

従法線とは、

空間曲線上の1点で接触平面に垂直な直線

従法線(じゅうほうせん)とは - コトバンク:より引用 のことです。

なぜ従法線が必要なのかは、長くなってしまうので以下のサイト様を参考にしてください。

marupeke296.com


この従法線を求めるためには、法線と接ベクトルの外積で出すことができるのでこちらのソースコードで求められます。

float3 binormal = cross( v.normal, v.tangent.xyz ) * v.tangent.w;


接空間への変換

接空間は、

  • 接ベクトル
  • 従法線
  • 法線

の3つのベクトルを行列にすることによって求めることができます。

各ベクトルのイメージ
名前 英語名 軸の色
接ベクトル Tangent 赤色
従法線 Binormal 緑色
法線 Normal 青色

f:id:soramamenatan:20200216124640p:plain

法線マップと接空間 - しゅみぷろ:より引用

以上から、TANGENT_SPACE_ROTATIONマクロの中身が理解できました。


接空間を理解するために参考にしたサイト様

esprog.hatenablog.com

qiita.com


ライトとカメラの方向ベクトルを接空間に変換

TANGENT_SPACE_ROTATIONマクロにより、rotation変数に3*3の行列が入っているので、以下のソースコードで接空間に変換できます。

o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));


ObjSpaceLightDir

ライトのオブジェクト空間での方向ベクトルを取得できます。


ObjSpaceViewDir

カメラのオブジェクト空間での方向ベクトルを取得できます。


UnpackNormal

テクスチャに0~1で書き込まれている法線の情報を-1~1の値に変換するものです。
以下定義です。

inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(UNITY_NO_DXT5nm)
    return packednormal.xyz * 2 - 1;
#else
    return UnpackNormalDXT5nm(packednormal);
#endif
}


diff,spec

この2つの変数は、拡散反射光(diffuse)と鏡面反射光(spacular)のことです。
詳しい解説は次回の記事で行います。


結果

Planeに凹凸が出れば完成です。

f:id:soramamenatan:20200216142941p:plain


ソースコードにコメントを付与

Shader "Unlit/Normalmap" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _NormalMap ("Normal map", 2D) = "bump" {}
        _Shininess ("Shininess", Range(0.0, 1.0)) = 0.078125
    }
    SubShader {
        Tags { "Queue"="Geometry" "RenderType"="Opaque"}

        Pass {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #include "UnityCG.cginc"

            #pragma vertex vert
            #pragma fragment frag

            float4 _LightColor0;
            sampler2D _MainTex;
            sampler2D _NormalMap;
            half _Shininess;

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

            struct v2f {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                half3 lightDir : TEXCOORD1;
                half3 viewDir : TEXCOORD2;
            };

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

            float4 frag(v2f i) : COLOR {
                i.lightDir = normalize(i.lightDir);
                i.viewDir = normalize(i.viewDir);
                half3 halfDir = normalize(i.lightDir + i.viewDir);
                half4 tex = tex2D(_MainTex, i.uv);
                half3 normal = UnpackNormal(tex2D(_NormalMap, i.uv));
                // 拡散反射光
                half4 diff = saturate(dot(normal, i.lightDir)) * _LightColor0;
                // 鏡面反射光
                half3 spec = pow(max(0, dot(normal, halfDir)), _Shininess * 128.0) * _LightColor0.rgb * tex.rgb;
                fixed4 color;
                color.rgb  = tex.rgb * diff + spec;
                return color;
            }
            ENDCG
        }
    }
}

今回はこれで以上となります。
ここまでご視聴ありがとうございました!