GameTech/URP 渲染实战
源码 / Ch23/ContactShadowPass.shader
Ch23/ContactShadowPass.shader
// 案例 23.9:接触阴影的全屏步进 Pass(由 ContactShadowFeature 调用)。
Shader "Cookbook/Ch23/ContactShadowPass"
{
    SubShader
    {
        Tags { "RenderPipeline"="UniversalPipeline" }
        Pass
        {
            Name "ContactShadows"
            ZTest Always ZWrite Off Cull Off

            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Random.hlsl"
            #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"

            float4 _ContactParams;    // x: 追踪长度  y: 步数  z: 厚度  w: 起点偏移
            float4 _ContactLightDir;  // xyz: 指向光源的方向 L   w: 强度

            half Frag(Varyings input) : SV_Target
            {
                float2 uv = input.texcoord;
                float deviceDepth = SampleSceneDepth(uv);
            #if UNITY_REVERSED_Z
                if (deviceDepth <= 0.0) return 1;              // 天空:不处理
            #else
                if (deviceDepth >= 1.0) return 1;
                deviceDepth = deviceDepth * 2.0 - 1.0;
            #endif
                // 1) 由深度重建这个像素的世界坐标
                float3 positionWS = ComputeWorldSpacePosition(uv, deviceDepth, UNITY_MATRIX_I_VP);

                int   steps     = (int)_ContactParams.y;
                float stepLen   = _ContactParams.x / _ContactParams.y;
                float thickness = _ContactParams.z;
                float3 L = _ContactLightDir.xyz;

                // 2) 用交错梯度噪声抖动起点,把条带状走样变成细噪点(软阴影 / TAA 会再把它抹平)
                float jitter = InterleavedGradientNoise(uv * _ScreenSize.xy, 0);
                float3 p = positionWS + L * (_ContactParams.w + stepLen * jitter);

                half occlusion = 0;
                [loop] for (int i = 0; i < steps; i++)
                {
                    p += L * stepLen;
                    // 3) 把采样点投回屏幕,读那里的场景深度
                    float4 positionCS = TransformWorldToHClip(p);
                    float2 sampleUV = ComputeNormalizedDeviceCoordinates(p, UNITY_MATRIX_VP);
                    if (any(sampleUV < 0) || any(sampleUV > 1)) break;

                    float sceneEyeDepth  = LinearEyeDepth(SampleSceneDepth(sampleUV), _ZBufferParams);
                    float sampleEyeDepth = positionCS.w;           // 透视投影下 w 就是观察空间深度
                    float delta = sampleEyeDepth - sceneEyeDepth;  // > 0:采样点在可见表面之后

                    // 4) 在表面之后、但又没“穿透”太深 → 光线被这块表面挡住了
                    if (delta > 0.002 && delta < thickness)
                    {
                        // 离起点越远的遮挡越弱,避免接触阴影末端出现硬边
                        occlusion = 1.0 - (float)i / steps;
                        break;
                    }
                }
                return 1.0 - occlusion * _ContactLightDir.w;
            }
            ENDHLSL
        }
    }
}