【Unity】【シェーダ】フレネル反射の実装

フレネル反射について簡単にまとめます。

フレネル反射

UnityのStandardシェーダで非金属かつ滑らかな物体を作ると、エッジの部分が明るく見えます。

f:id:halya_11:20180618221502p:plain

これはエッジの部分の反射率がほかの部分よりも高いので周りの景色が移りこんでいるためです。

このように視点と法線が直角になる部分(エッジ)ほど反射率が大きくなる現象をフレネル反射といって、
現実でも水やプラスチックなどの物体でわかりやすく見られる物理現象です。

数式で表す

フレネル反射率は次の近似式で表されます。

f:id:halya_11:20180618222646p:plain
フレネルの式 - Wikipedia より

F0は正面から見たときの反射率です。
この値は、水やプラスチックなどフレネル反射が目立つ物体だと0.02~0.05あたりと低い値を示し、金属は1に近くなります。

またcosθは視点へのベクトルと法線との内積として求められるので下記のようになります。

f:id:halya_11:20180618222620p:plain

シェーダ1 フレネル反射

まずフレネル反射率を返すシェーダを書いてみます。

Shader "FresnelReflection"
{
    Properties
    {
        [PowerSlider(0.1)] _F0 ("F0", Range(0.0, 1.0)) = 0.02
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

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

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

            struct v2f
            {
                float4 vertex : SV_POSITION;
                half vdotn : TEXCOORD1;
            };

            float _F0;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                half3 viewDir = normalize(ObjSpaceViewDir(v.vertex));
                o.vdotn = dot(viewDir, v.normal.xyz);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                half fresnel = _F0 + (1.0h - _F0) * pow(1.0h - i.vdotn, 5);
                return fresnel;
            }
            ENDCG
        }
    }
}

F0は調整できるようにしてみました。
描画結果は次のようになります。

f:id:halya_11:20180618224039p:plain

シェーダ2 キューブマップ

フレネル反射は環境光の反射として扱われるのでキューブマップと組み合わせてみます。

Shader "FresnelReflection"
{
    Properties
    {
        [PowerSlider(0.1)] _F0 ("F0", Range(0.0, 1.0)) = 0.02
        _CubeMap ("Cube Map", Cube) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

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

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

            struct v2f
            {
                float4 vertex : SV_POSITION;
                half vdotn : TEXCOORD1;
                half3 reflDir : TEXCOORD2;
            };

            UNITY_DECLARE_TEXCUBE(_CubeMap);
            float _F0;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                half3 viewDir = normalize(ObjSpaceViewDir(v.vertex));
                o.vdotn = dot(viewDir, v.normal.xyz);
                o.reflDir = mul(unity_ObjectToWorld, reflect(-viewDir, v.normal.xyz));
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                half fresnel = _F0 + (1.0h - _F0) * pow(1.0h - i.vdotn, 5);
                return UNITY_SAMPLE_TEXCUBE(_CubeMap, i.reflDir) * fresnel;
            }
            ENDCG
        }
    }
}

キューブマップをサンプリングしてフレネル反射率を乗算しているだけです。
他のライティング計算もしないといまいちですが、結果は次のようになります。

f:id:halya_11:20180618225034p:plain

参考

フレネルの式 - Wikipedia

キューブマップはこちらから
NoEmotion HDRs