【Unity】【シェーダ】Reflection Probeの影響を受けるシェーダを書く

Reflection Probeを適用するシェーダの書き方です。

ここではReflection Probeの仕組みは割愛し、シェーダ内での取り扱いについてのみ書きます。
Reflection Probeについては別の記事で書きます(17.07.10 書きました -> 関連からリンク)。

キューブマップを取得する

まずはReflection Probeによってレンダリングされたキューブマップを取得してみます。

Unityではunity_SpecCube0というキューブマップが定義されており、次のように使われることが決まっています。

  • Reflection Probeの影響範囲内である場合にはReflection Probeによるキューブマップが入ってくる
  • それ以外の場合にはLighting WindowのEnvironment Reflectionで設定したキューブマップが入ってくる

上記を踏まえてシェーダを書いてみます。

Shader "ReflectionProbe"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }

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

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

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 worldPos : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
                float3 pos : TEXCOORD2;
            };
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldViewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
                half3 reflDir = reflect(-worldViewDir, i.worldNormal);
                
                // unity_SpecCube0はUnityで定義されているキューブマップ
                half4 refColor = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflDir, 0);

                // Reflection ProbeがHDR設定だった時に必要な処理
                refColor.rgb = DecodeHDR(refColor, unity_SpecCube0_HDR);
                return refColor;
            }
            ENDCG
        }
    }
}

unity_SpecCube0をサンプリングし、HDRのデコードをしているだけです。

結果は次のようになります。

f:id:halya_11:20180708174124p:plain
(Gameビューだと見づらかったのでSceneビューで)

赤と緑のCubeがSphereに正常に映り込んでいることが確認できます。

Box Projection

次にBox Projectionを適用します。

Shader "ReflectionProbe"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }

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

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

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 worldPos : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
            };

            // Box Projectionを考慮した反射ベクトルを取得
            float3 boxProjection(float3 normalizedDir, float3 worldPosition, float4 probePosition, float3 boxMin, float3 boxMax)
            {
                // GraphicsSettingsのReflection Probes Box Projectionが有効な場合のみtrue
               #if UNITY_SPECCUBE_BOX_PROJECTION
                    // Box Projectionが有効な場合はprobePosition.w > 0となる
                    if (probePosition.w > 0) {
                        float3 magnitudes = ((normalizedDir > 0 ? boxMax : boxMin) - worldPosition) / normalizedDir;
                        float magnitude = min(min(magnitudes.x, magnitudes.y), magnitudes.z);
                        normalizedDir = normalizedDir * magnitude + (worldPosition - probePosition);
                    }
               #endif

                return normalizedDir;
            }

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldViewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
                half3 reflDir = reflect(-worldViewDir, i.worldNormal);
                
                // 反射方向へのベクトルをBox Projectionにより変換する
                reflDir = boxProjection(reflDir, i.worldPos, unity_SpecCube0_ProbePosition, unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax);
                
                half4 refColor = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflDir, 0);
                refColor.rgb = DecodeHDR(refColor, unity_SpecCube0_HDR);
                return refColor;
            }
            ENDCG
        }
    }
}

Box Projectionの仕組みの説明はここでは割愛します(別記事書くかも)。

さてBox Projectionを有効化するには下記2つの設定が必要です。

  1. Graphics SettingsのReflection Probes Box Projectionにチェックを入れる
  2. Reflection ProbeのBox Projectionにチェックを入れる

コメントにも書いてありますが、上記1.2.が有効化されているかどうかは次のように取得するというUnityの決まりがあります。

  1. チェックが入っていたらUNITY_SPECCUBE_BOX_PROJECTIONがtrueとなる
  2. チェックが入っていたらunity_SpecCube0_ProbePosition.wが1となる

上記のソースコードはこれらを用いてBox Projectionが無効な場合と処理を分岐しています。

結果は次のようになります。

f:id:halya_11:20180708202910p:plain

Box Projectionが適用され、近くにあるオブジェクトもちゃんと映り込みました。

ブレンドを考慮する

最後にReflection Probe間のブレンドを適用してみます。

Shader "ReflectionProbe"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }

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

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

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 worldPos : TEXCOORD0;
                float3 worldNormal : TEXCOORD1;
            };

            // Box Projectionを考慮した反射ベクトルを取得
            float3 boxProjection(float3 normalizedDir, float3 worldPosition, float4 probePosition, float3 boxMin, float3 boxMax)
            {
               #if UNITY_SPECCUBE_BOX_PROJECTION
                    if (probePosition.w > 0) {
                        float3 magnitudes = ((normalizedDir > 0 ? boxMax : boxMin) - worldPosition) / normalizedDir;
                        float magnitude = min(min(magnitudes.x, magnitudes.y), magnitudes.z);
                        normalizedDir = normalizedDir* magnitude + (worldPosition - probePosition);
                    }
               #endif

                return normalizedDir;
            }

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                half3 worldViewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
                half3 reflDir = reflect(-worldViewDir, i.worldNormal);
                
                half3 reflDir0 = boxProjection(reflDir, i.worldPos, unity_SpecCube0_ProbePosition, unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax);
                half3 reflDir1 = boxProjection(reflDir, i.worldPos, unity_SpecCube1_ProbePosition, unity_SpecCube1_BoxMin, unity_SpecCube1_BoxMax);
                
                half4 refColor0 = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflDir0, 0);
                refColor0.rgb = DecodeHDR(refColor0, unity_SpecCube0_HDR);

                // SpecCube1のサンプラはSpecCube0のものを使う
                half4 refColor1 = UNITY_SAMPLE_TEXCUBE_SAMPLER_LOD(unity_SpecCube1, unity_SpecCube0, reflDir1, 0);
                refColor1.rgb = DecodeHDR(refColor1, unity_SpecCube1_HDR);

                // unity_SpecCube0_BoxMin.w にブレンド率が入ってくる
                return lerp(refColor1, refColor0, unity_SpecCube0_BoxMin.w);
            }
            ENDCG
        }
    }
}

2個目のキューブマップはunity_SpecCube1に入ってきます。
ただしこれにサンプラは含まれないため、 UNITY_SAMPLE_TEXCUBE_LODの代わりにNITY_SAMPLE_TEXCUBE_SAMPLER_LODを使用して、
サンプラとしてunity_SpecCube0を渡してサンプリングします。

またブレンド率はunity_SpecCube0_BoxMin.wに入ってくるという決まりがあるため、
これを使って二つのキューブマップの色を補間します。

結果は次のようになります。

f:id:halya_11:20180708212632g:plain

正常にブレンドされました。

関連

light11.hatenadiary.com