知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【UnityShader】Cascade Shadow Maps【3】 #114

はじめに

前回、CSMsのスクリプト部分について解説しました。
今回は、シェーダー部分について解説します。

深度を渡すシェーダー

Shader "CustomShadow/Caster"
{
    SubShader
    {
        Tags { "RenderType" = "Opaque" }

        CGINCLUDE
        #include "UnityCG.cginc"
        struct v2f
        {
            float4 pos : SV_POSITION;
            float2 depth:TEXCOORD0;
        };

        uniform float _gShadowBias;
        v2f vert (appdata_full v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.pos.z += _gShadowBias;
            o.depth = o.pos.zw;
            return o;
        }

        fixed4 frag (v2f i) : COLOR
        {
            // 正規化デバイス空間に
            float depth = i.depth.x / i.depth.y;

            // プラットフォーム毎の違いを吸収
            // shaderの言語がGLSLならtrue
        #if defined (SHADER_TARGET_GLSL)
            // (-1, 1)-->(0, 1)
            depth = depth * 0.5 + 0.5;
            // 深度値が反転しているか
            // DirectX 11, DirectX 12, PS4, Xbox One, Metal: 逆方向
        #elif defined (UNITY_REVERSED_Z)
            //(1, 0)-->(0, 1)
            depth = 1 - depth;
        #endif
            return depth;
        }
        ENDCG

        Pass
        {
            Cull front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }
    }
}

影を受け取るシェーダー

Shader "CustomShadow/Receiver"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }

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

            CGPROGRAM
            #include "UnityCG.cginc"

            struct v2f
            {
                float4 pos : SV_POSITION;
                float4 worldPos : TEXCOORD0;
                float eyeZ : TEXCOORD1;
            };

            uniform float4x4 _gWorldToShadow;
            uniform sampler2D _gShadowMapTexture;
            uniform float4 _gShadowMapTexture_TexelSize;

            uniform float4 _gLightSplitsNear;
            uniform float4 _gLightSplitsFar;
            uniform float4x4 _gWorld2Shadow[4];

            uniform sampler2D _gShadowMapTexture0;
            uniform sampler2D _gShadowMapTexture1;
            uniform sampler2D _gShadowMapTexture2;
            uniform sampler2D _gShadowMapTexture3;

            uniform float _gShadowStrength;

            v2f vert(appdata_full v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.eyeZ = o.pos.w;
                return o;
            }

            // Cascadeの重みを色として表示
            fixed4 GetCascadeWeights(float z)
            {
                fixed4 zNear = float4(z >= _gLightSplitsNear);
                fixed4 zFar = float4(z < _gLightSplitsFar);
                // 仮に2番目の視錐台の場合、(0,1,0,0)
                fixed4 weights = zNear * zFar;
                return weights;
            }

            // 影の計算
            float CulcShadow(float4x4 w2Shadow, float4 wPos, fixed cascadeWeight, sampler2D shadowMapTex)
            {
                // WVP行列に変換し、0~1の範囲に
                float4 shadowCoord = mul(w2Shadow, wPos);
                shadowCoord.xy /= shadowCoord.w;
                shadowCoord.xy = shadowCoord.xy * 0.5 + 0.5;

                float4 sampleDepth = tex2D(shadowMapTex, shadowCoord.xy);

                // 深度
                float depth = shadowCoord.z / shadowCoord.w;
                #if defined (SHADER_TARGET_GLSL)
                    depth = depth * 0.5 + 0.5;
                #elif defined (UNITY_REVERSED_Z)
                    depth = 1 - depth;
                #endif

                float shadow = sampleDepth < depth ? _gShadowStrength : 1;
                // どの分割した視錐台のものの影かを返す
                return shadow * cascadeWeight;
            }

            // ShadowTextureのサンプリング
            float4 SampleShadowTexture(float4 wPos, fixed4 cascadeWeights)
            {
                float cascadeShadow =
                    CulcShadow(_gWorld2Shadow[0], wPos, cascadeWeights[0], _gShadowMapTexture0) +
                    CulcShadow(_gWorld2Shadow[1], wPos, cascadeWeights[1], _gShadowMapTexture1) +
                    CulcShadow(_gWorld2Shadow[2], wPos, cascadeWeights[2], _gShadowMapTexture2) +
                    CulcShadow(_gWorld2Shadow[3], wPos, cascadeWeights[3], _gShadowMapTexture3);

                return cascadeShadow;// * cascadeWeights;
            }

            fixed4 frag (v2f i) : COLOR0
            {
                fixed4 weights = GetCascadeWeights(i.eyeZ);
                fixed4 col = SampleShadowTexture(i.worldPos, weights);
                return col;
            }

            #pragma vertex vert
            #pragma fragment frag
            #pragma fragmentoption ARB_precision_hint_fastest
            ENDCG
        }
    }
}

解説

深度を渡すシェーダー

頂点シェーダー

v2f vert (appdata_full v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.pos.z += _gShadowBias;
    o.depth = o.pos.zw;
    return o;
}

スクリプトから渡してきた_gShadowBiasをクリップ座標のZに加算することでシャドウアクネを防いでいます。
また、このシェーダーは深度を渡すだけですので、ZWのみをdepthに入れています。

フラグメントシェーダー

fixed4 frag (v2f i) : COLOR
{
    // 正規化デバイス空間に
    float depth = i.depth.x / i.depth.y;

    // プラットフォーム毎の違いを吸収
    // shaderの言語がGLSLならtrue
#if defined (SHADER_TARGET_GLSL)
    // (-1, 1)-->(0, 1)
    depth = depth * 0.5 + 0.5;
    // 深度値が反転しているか
    // DirectX 11, DirectX 12, PS4, Xbox One, Metal: 逆方向
#elif defined (UNITY_REVERSED_Z)
    //(1, 0)-->(0, 1)
    depth = 1 - depth;
#endif
    return depth;
}

Z値をWで除算することにより、正規化デバイス空間へと変換しています。
Z値はプラットフォームによって異なるのでその違いを吸収しています。

影を受け取るシェーダー

頂点シェーダー

特に何もしていないので、割愛させて頂きます。

フラグメントシェーダー

fixed4 frag (v2f i) : COLOR0
{
    fixed4 weights = GetCascadeWeights(i.eyeZ);
    fixed4 col = SampleShadowTexture(i.worldPos, weights);
    return col;
}
影の重み
// Cascadeの重みを色として表示
fixed4 GetCascadeWeights(float z)
{
    fixed4 zNear = float4(z >= _gLightSplitsNear);
    fixed4 zFar = float4(z < _gLightSplitsFar);
    // 仮に2番目の視錐台の場合、(0,1,0,0)
    fixed4 weights = zNear * zFar;
    return weights;
}

UnityObjectToClipPos()で取得したクリップ空間のw成分を引数としてどのシャドウマップを使用するかを決めています。

w成分はクリップ空間においてz成分の逆ベクトルとなっています。
クリップ空間は右手座標系ですので、カメラの前方にあるオブジェクトのz成分が負になっています。
そこでz成分の逆ベクトルであるw成分を使用することによって正の値を求めています。


例えば、カメラのNearが0.3でFarが100のときの各値は以下となります。

Near

f:id:soramamenatan:20220101090019p:plain

Far

f:id:soramamenatan:20220101090027p:plain


そして、w成分が仮に30だとすると

fiexd4 zNear = (1, 1, 1, 0);
fiexd4 zFar = (0, 0, 1, 1);

// (0, 0, 1, 0)
fixed4 weights = zNear * zFar;

となるので3番目のシャドウマップが使われることになります。

参考

https://answers.unity.com/questions/1364663/z-and-w-output-of-unityobjecttoclippos.html

影の計算
// 影の計算
float CulcShadow(float4x4 w2Shadow, float4 wPos, fixed cascadeWeight, sampler2D shadowMapTex)
{
    // WVP行列に変換し、0~1の範囲に
    float4 shadowCoord = mul(w2Shadow, wPos);
    shadowCoord.xy /= shadowCoord.w;
    shadowCoord.xy = shadowCoord.xy * 0.5 + 0.5;

    float4 sampleDepth = tex2D(shadowMapTex, shadowCoord.xy);

    // 深度
    float depth = shadowCoord.z / shadowCoord.w;
    #if defined (SHADER_TARGET_GLSL)
        depth = depth * 0.5 + 0.5;
    #elif defined (UNITY_REVERSED_Z)
        depth = 1 - depth;
    #endif

    float shadow = sampleDepth < depth ? _gShadowStrength : 1;
    // どの分割した視錐台のものの影かを返す
    return shadow * cascadeWeight;
}

シャドウマップのサンプリングを行う関数となります。

スクリプト側から渡されたVP行列を元にシャドウマップのサンプリングを行い、重みに応じて描画するかの有無を決めています。

結果

影が描画されれば成功です。

f:id:soramamenatan:20220101092030p:plain

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