Open5

HLSLシェーダーの魔導書から ShaderLab への実装移植(ライティング編)

しおむすびしおむすび

HLSLシェーダーの魔導書の4章のサンプルコードをもとに、ライティング処理を Unity(ShaderLab) で再実装してみる。
出発点は Sample_04_02 の状態 (Lambert拡散反射モデルが実装された状態)で、そこに鏡面反射や環境光を追加していく。

この状態からスタート。

Shader "Day3/Lambert"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        LOD 100

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

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

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

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert(appdata v)
            {
                v2f o;

                o.vertex = UnityObjectToClipPos(v.vertex);  // MVP変換
                // 頂点法線をピクセルシェーダーに渡す
                o.normal = UnityObjectToWorldNormal(v.normal); // 法線を回転させる
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                // ディレクションライトのデータを作成する
                float3 ligDirection = normalize(_WorldSpaceLightPos0.xyz);

                // ピクセルの法線とライトの方向の内積を計算する
                float t = dot(i.normal, ligDirection);

                // 内積の結果が0以下なら0にする
                t = max(0.0f, t);

                // ピクセルが受けているライトの光を求める
                fixed3 ligColor = _LightColor0.xyz; // #include "Lighting.cginc" にて定義
                fixed3 diffuseLig = ligColor * t;

                float4 finalColor = tex2D(_MainTex, i.uv);

                // 最終出力カラーに光を乗算する
                finalColor.xyz *= diffuseLig;

                return finalColor;
            }
            ENDCG
        }
    }
}

全体のコードは以下にまとめていく。
https://github.com/tsukumaru/GraphicsAcademy/tree/main/Assets/day3/Shaders

しおむすびしおむすび

鏡面反射の実装

Sample_04_03 のコードを参考にしながら、さっそく鏡面反射を実装していく。

そのまま実装していき、フラグメントシェーダーを以下のように変更。

            fixed4 frag(v2f i) : SV_Target
            {
                // ディレクションライトのデータを作成する
                float3 ligDirection = normalize(_WorldSpaceLightPos0.xyz);
                // ピクセルが受けているライトの光を求める
                fixed3 ligColor = _LightColor0.xyz; // #include "Lighting.cginc" にて定義

                /*** 拡散反射 ***/

                float t = dot(i.normal, ligDirection);
                t = max(0.0f, t);

                fixed3 diffuseLig = ligColor * t;

+                /*** 鏡面反射 ***/
+
+               // 反射ベクトルを求める
+                float3 refVec = reflect(ligDirection, i.normal);
+
+                // 光が当たったサーフェイスから視点に伸びるベクトルを求める
+                // 視点にはカメラのワールド座標を使用。
+                float3 toEye = normalize(_WorldSpaceCameraPos - i.worldPos);
+
+                // 鏡面反射の強さを求める
+                t = dot(refVec, toEye);
+                t = max(0.0f, t);
+
+                t = pow(t, 5.0f);
+
+                // 鏡面反射光を求める
+                float3 specularLig = ligColor * t;
+
+                // 拡散反射光と鏡面反射光を足し算して、最終的な光を求める
+                float3 lig = diffuseLig + specularLig;

                float4 finalColor = tex2D(_MainTex, i.uv);

                // 最終出力カラーに光を乗算する
-                finalColor.xyz *= diffuseLig;
+                finalColor.xyz *= lig;

                return finalColor;
            }

基本的にはサンプルコードをそのまま持ってきたが、視点に _WorldSpaceCameraPos を使うというところで少し調べた。

拡散反射のみの場合との比較gif は以下の通り。手前が拡散反射のみで、奥が鏡面反射を追加したバージョン。
カメラの位置に応じて、少し明るくなっているのがわかる。

しおむすびしおむすび

反射ベクトルの求め方について

鏡面反射は、ライトベクトルが反射したベクトルがどのくらいカメラ(目)に入るかで求めることができる。
反射ベクトルは今回 reflect という組み込み関数を使って対応したが、本来は以下の計算式で求められる。

R = L + 2 * (-N・L) * N

これがどういう意味なのか、あまり腹落ちしていなかったので調べてみた。
すると以下の記事がかなりわかりやすかった。
https://qiita.com/edo_m18/items/b145f2f5d2d05f0f29c9


(記事内から引用)

(記事内から引用)

上記の図を参考に、一から説明してみると、

  • 反射ベクトルは、ライトベクトルの始点を衝突点に変換した後、入射した面を基準に対象移動させたもの。 ( ここまでで R = F + ?? になる)
  • 移動する距離は、面と反射ベクトルの終点との距離がわかれば2倍すればよく、それを求めるために法線をa倍したものを使う。(ここまでで R = F + 2 * a * N になる)
  • aは、2番目の図でいうところの、F と P と a でできる直角三角形から求められる。(ここでは F も N も正規化して大きさが 1 になっている前提)
    • 三角関数を使って、 a = |F|cosΘ = cosΘ
    • 内積は始点をそろえる必要があるので、F を -F にすると、-N・F = |N||F|cosΘ = cosΘ
    • なので、 a = cosΘ = -N・F
  • これでようやく R = L + 2 * (-N・L) * N の形になる (F は L と同じ意味)

ここまで考えてようやく理解することができた

しおむすびしおむすび

環境光の実装

鏡面反射が実装できたので、次は環境光を追加する。
Sample_04_04 のコードを参考に実装する。

ソースコードは以下の通り。一律で底上げしているだけ。

            fixed4 frag(v2f i) : SV_Target
            {
                // ディレクションライトのデータを作成する
                float3 ligDirection = normalize(_WorldSpaceLightPos0.xyz);
                // ピクセルが受けているライトの光を求める
                fixed3 ligColor = _LightColor0.xyz; // #include "Lighting.cginc" にて定義

                /*** 拡散反射 ***/

                float t = dot(i.normal, ligDirection);
                t = max(0.0f, t);

                fixed3 diffuseLig = ligColor * t;

                /*** 鏡面反射 ***/

                float3 refVec = reflect(ligDirection, i.normal);
                float3 toEye = normalize(_WorldSpaceCameraPos - i.worldPos);

                t = dot(refVec, toEye);
                t = max(0.0f, t);

                t = pow(t, 5.0f);

                float3 specularLig = ligColor * t;

                // 拡散反射光と鏡面反射光を足し算して、最終的な光を求める
                float3 lig = diffuseLig + specularLig;
                
+                /*** 環境光 ***/
+                lig += 0.3;

                float4 finalColor = tex2D(_MainTex, i.uv);

                // 最終出力カラーに光を乗算する
                finalColor.xyz *= lig;

                return finalColor;
            }

比較gifは以下。一番奥が環境光をいれたもので、はっきり顔がわかるくらいに明るくなっていることがわかる。

しおむすびしおむすび

鏡面反射と環境光をテクスチャから指定できるようにする

ついでに、鏡面反射と環境光の度合いをテクスチャから指定できるにしてみる。(スペキュラマップとAOマップに対応させる)

サンプルは、スペキュラマップが Sample_06_02 、AOマップが Sample_06_03 に該当する。

ここはさほど難しくなく、以下のようにそれぞれを tex2D で読み込み、乗算することで実装できた。

            fixed4 frag(v2f i) : SV_Target
            {
                // ディレクションライトのデータを作成する
                float3 ligDirection = normalize(_WorldSpaceLightPos0.xyz);
                // ピクセルが受けているライトの光を求める
                fixed3 ligColor = _LightColor0.xyz; // #include "Lighting.cginc" にて定義

                /*** 拡散反射 ***/

                float t = dot(i.normal, ligDirection);
                t = max(0.0f, t);

                fixed3 diffuseLig = ligColor * t;

                /*** 鏡面反射 ***/

                float3 refVec = reflect(ligDirection, i.normal);
                float3 toEye = normalize(_WorldSpaceCameraPos - i.worldPos);

                t = dot(refVec, toEye);
                t = max(0.0f, t);

                t = pow(t, 5.0f);

                float3 specularLig = ligColor * t;

+                half specPower = tex2D(_Specular, i.uv).r;
+                specularLig *= specPower;

                // 拡散反射光と鏡面反射光を足し算して、最終的な光を求める
                float3 lig = diffuseLig + specularLig;
                
                /*** 環境光 ***/
                half ambient = 0.3;
+                half ambientPower = tex2D(_AO, i.uv).r;
+                ambient *= ambientPower;

                lig += ambient;

                float4 finalColor = tex2D(_MainTex, i.uv);

                // 最終出力カラーに光を乗算する
                finalColor.xyz *= lig;

                return finalColor;
            }

これをアタッチしたマテリアルを Cube にアタッチして、適当なマップを適用してみると、以下のようになる。