【連載】Unity時代の3D入門 – 第6回「鏡面反射ライティング」

こんにちは、クライアントサイドエンジニアの矢野です。

第5回では、拡散反射ライティングについて書きました。
第6回では、物体のハイライトを表現するライティング、鏡面反射ライティングについて説明します。

鏡面反射とは

光が物体に当たると、光の一部が表面で反射し、残りは屈折しながら物体の中に入ります。

物体が平らであれば、反射した光は法線を挟んで反対側に進みます。
この光を鏡面反射光といいます。

また、鏡面反射光はスペキュラ(specular)とも呼ばれます

 

[原理] 反射の強さを表現する

前回の拡散反射と同じく、まずは反射の強さの表現方法を考えます。

人間が物体を見るとき、実際には「物体に反射した、光源からの光」を見ているという話を前回しました。

これを踏まえて考えると、鏡面反射が最も強くなるのは、光源の光の反射を直接見ることができる状況となります。
それはつまり光源からの光が反射した先にちょうど視点がある下図のような状況です。

つまり鏡面反射が最も強くなるのは、ある点から視点に向けた単位ベクトルとライトに向けた単位ベクトルを足し合わせたベクトルの向きが、法線の向きと一致している状態です。

また、この足し合わせたベクトルを正規化したものをハーフベクトルといいます。

ここまでの理解を一度まとめると、視点に向けた単位ベクトルとライトに向けた単位ベクトルを足し合わせた単位ベクトルの向きが法線と一致しているとき、鏡面反射光は最も強くなり、さらにそのベクトルをハーフベクトルと呼ぶので、つまり、ハーフベクトルが法線が一致する場合に鏡面反射光は最も強くなります。

ここで前回と同じように光の強さを0~1で表すことを考えると、法線とハーフベクトルの向きが同じときに最も強くなるため、1となります。

また、ベクトルの内積は、二つの単位ベクトルが同じ方向を向いているときに1となることを前回説明しました。

これらのことから、鏡面反射の強さは法線とハーフベクトルの内積として表現することができます。
求めたい値は0~1の数値であるため、前回と同様0未満の値は0に丸めます。

 

[シェーダ] 反射の強さを表現する

この計算をシェーダで表現します。

Shader "Custom/SpecularIntensity"
{
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                half3 normal : TEXCOORD1;
                half3 halfDir : TEXCOORD2;
            };
            
            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                // ハーフベクトルを求める
                half3 eyeDir = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz);
                o.halfDir = normalize(_WorldSpaceLightPos0.xyz + eyeDir);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col;
                // スペキュラの強さを求める
                col.rgb = max(0, dot(i.normal, i.halfDir));
                return col;
            }
            ENDCG
        }
    }
}

まず、頂点シェーダでハーフベクトルを求めています。

half3 eyeDir = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz);
o.halfDir = normalize(_WorldSpaceLightPos0.xyz + eyeDir);

_WorldSpaceCameraPos はUnityの定義済み変数であり、カメラの位置を取得できます。
ライト方向のベクトルと視点(カメラ)方向のベクトルを足し合わせて正規化することでハーフベクトルを求めています。

また、フラグメントシェーダでは、rgb値に法線とハーフベクトルとの内積を使って鏡面反射光の強さを代入しています。

col.rgb = max(0, dot(i.normal, i.halfDir));

ちなみに法線とハーフベクトルはフラグメントシェーダで正規化した方が正確な結果が得られますが、今回はモバイルを前提にしていることもあり、処理負荷のために誤差を許容して省略しています。

このシェーダを適当なオブジェクトに適用します。
Directional Lightの情報を取得するため、Hierarchy に Directional Light が置かれていることを確認してください。


このように反射光が適用されます。

 

[原理] 物体表面の粗さを表現する

次に物体表面の粗さを表現します。
物体表面にとても細かい凹凸があるとき、入射した光は様々な方向に反射します。
これを乱反射といいます。

この結果として、物体表面の反射光は粗いほど広範囲に薄く広がり、滑らかなほど局所的になります。

このような広がりを計算で表現するには、先ほど求めた鏡面反射の強さの値をn乗します。
xが0~1の範囲内の累乗のグラフは、べき指数nを大きくしていくにつれて小さい値をより小さくするため、局所的に光っている表現ができます。

 

[シェーダ] 物体表面の粗さを表現する

これをシェーダで記述します。

Shader "Custom/SpecularRoughness"
{
    Properties
    {
        // 鏡面反射の鋭さを示すプロパティを定義
        _Shininess ("Shininess", float) = 10
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                half3 normal : TEXCOORD1;
                half3 halfDir : TEXCOORD2;
            };

            half _Shininess;
            
            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                half3 eyeDir = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz);
                o.halfDir = normalize(_WorldSpaceLightPos0.xyz + eyeDir);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col;
                // _Shininessで累乗することにより物体の粗さを表現
                col.rgb = pow(max(0, dot(i.normal, i.halfDir)), _Shininess);
                return col;
            }
            ENDCG
        }
    }
}

まず、プロパティとしてべき指数を定義します。
この値は大きいほど鏡面反射光が局所的になる(物体が粗くない場合の表現に近づく)ため、_Shininess という名前にします。

 _Shininess ("Shininess", float) = 10

そしてこれを先ほど求めた鏡面反射の強さの値に累乗します。

col.rgb = pow(max(0, dot(i.normal, i.halfDir)), _Shininess);

_Shininess を20としてこのシェーダを適用すると、以下のようなレンダリング結果が得られます。

Shininess の値を大きくすることで、先ほどよりも反射が局所的になったことがわかります。

 

[原理] 物体の色を表現する

次に物体の色について考えます。

拡散反射光は物体内部で一部の色が吸収されることにより色がつくことを前回学びました。
これに対して鏡面反射光は物体の内部に入らない光であるため、基本的には着色されません。

しかし、物質が金属である場合には、光の一部が吸収されて金や銅のように着色されます。
話が逸れるので詳しくは説明しませんが、これは金属の化学結合が非金属とは異なっており、原子に束縛されない自由電子を持っているためです。

計算としては拡散反射のときと同様、色を定義して乗算するだけです。

 

[シェーダ] 物体の色を表現する

シェーダは次のように変更します。

Shader "Custom/SpecularObjectColor"
{
    Properties
    {
        _Shininess ("Shininess", float) = 10
        // 反射の色
        _SpecColor ("Specular Color (RGB)", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                half3 normal : TEXCOORD1;
                half3 halfDir : TEXCOORD2;
            };

            half _Shininess;
            half4 _SpecColor;
            
            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                half3 eyeDir = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz);
                o.halfDir = normalize(_WorldSpaceLightPos0.xyz + eyeDir);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col;
                // 反射の色を乗算
                col.rgb = pow(max(0, dot(i.normal, i.halfDir)), _Shininess) * _SpecColor.rgb;
                return col;
            }
            ENDCG
        }
    }
}

反射の色をプロパティで定義した後、フラグメントシェーダでその値を乗算しています。

col.rgb = pow(max(0, dot(i.normal, i.halfDir)), _Shininess) * _SpecColor.rgb;

色を赤に設定した場合のレンダリング結果は以下の通りとなります。

 

[原理] ライトの色を表現する

ライトの色に関しては前回の拡散反射の場合と変わりません。
上で求めた色にライトの色を乗算することでライトを表現します。

 

[シェーダ] ライトの色を表現する

シェーダは次のように変更します。

Shader "Custom/SpecularLightColor"
{
    Properties
    {
        _Shininess ("Shininess", float) = 10
        _SpecColor ("Specular Color (RGB)", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                half3 normal : TEXCOORD1;
                half3 halfDir : TEXCOORD2;
            };

            half _Shininess;
            half4 _SpecColor;
            // ライトの色を取得するために変数を定義
            half4 _LightColor0;
            
            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                half3 eyeDir = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz);
                o.halfDir = normalize(_WorldSpaceLightPos0.xyz + eyeDir);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col;
                // ライトの色を乗算
                col.rgb = pow(max(0, dot(i.normal, i.halfDir)), _Shininess) * _SpecColor.rgb * _LightColor0.rgb;
                return col;
            }
            ENDCG
        }
    }
}

上記ではまず前回の拡散反射と同様 _LightColor0 を定義することでライトの色を取得しています。
そしてフラグメントシェーダでそれを乗算することでライトの色を適用します。

例えば反射色を白にした状態でライトの色を赤にすると、レンダリング結果は以下の通りとなります。

 

拡散反射光と合成する

最後に、今回作成した鏡面反射光のシェーダを、前回作成した拡散反射のものと合成します。

Shader "Custom/DiffuseAndSpecular"
{
    Properties
    {
        _MainTex ("Main Texture (RGB)", 2D) = "white" {}
        _Color ("Diffuse Tint (RGB)", Color) = (1, 1, 1, 1)
        _SpecColor ("Specular Color (RGB)", Color) = (1, 1, 1, 1)
        _Ambient ("Ambient Color (RGB)", Color) = (0, 0, 0, 0)
        _Shininess ("Shininess", float) = 10
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                half2 uv : TEXCOORD0;
                half3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                half3 normal: TEXCOORD1;
                half3 halfDir : TEXCOORD2;
            };

            sampler2D _MainTex;
            half4 _MainTex_ST;
            half4 _Color;
            half4 _SpecColor;
            half4 _Ambient;
            half4 _LightColor0;
            half _Shininess;
            
            v2f vert (appdata v)
            {
                v2f o = (v2f)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                half3 eyeDir = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz);
                o.halfDir = normalize(_WorldSpaceLightPos0.xyz + eyeDir);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                half3 diff = tex2D(_MainTex, i.uv) * _Color * max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _LightColor0.rgb + _Ambient.rgb;
                half3 spec = pow(max(0, dot(i.normal, i.halfDir)), _Shininess) * _LightColor0.rgb * _SpecColor.rgb;

                fixed4 col;
                col.rgb = saturate(diff + spec);
                return col;
            }
            ENDCG
        }
    }
}

このシェーダを適用すると、レンダリング結果は以下のようになります。

 

まとめ

今回は以下のことを行いました。

  • 鏡面反射光の取り扱い
  • 拡散反射のシェーダとの合成

次回は周囲の環境の映り込みを表現するキューブマッピングを解説する予定です。

 

バックナンバー