GameTech/URP 渲染实战
源码 / Ch28/SimpleSSR.shader
Ch28/SimpleSSR.shader
// 案例 28.6:最简屏幕空间反射(SSR)。世界空间线性步进 + 二分细化,命中后从屏幕颜色里取反射色。
// 只对“朝上”的表面生效(模拟湿地面),用 ScreenEffectFeature 驱动,需要 Depth + Normal。
Shader "Cookbook/Ch28/SimpleSSR"
{
    Properties
    {
        _MaxSteps ("Max Steps", Range(8, 128)) = 64
        _StepSize ("Step Size (m)", Float) = 0.12
        _Thickness ("Thickness (m)", Float) = 0.3
        _F0 ("Fresnel F0", Range(0, 1)) = 0.25
        _Intensity ("Intensity", Range(0, 1)) = 1
        _MinNormalY ("Min Normal Y", Range(0, 1)) = 0.8
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
        ZWrite Off ZTest Always Cull Off Blend Off

        Pass
        {
            Name "SSR"
            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag
            #include "Assets/Cookbook/Ch28/ScreenSpaceCommon.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareNormalsTexture.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Random.hlsl"

            CBUFFER_START(UnityPerMaterial)
                float _MaxSteps, _StepSize, _Thickness, _F0, _Intensity, _MinNormalY;
            CBUFFER_END

            // 光线上一点 → (屏幕 uv, 光线点自己的观察深度, 该处场景的观察深度)
            float3 ProjectRay(float3 p, out float2 uv)
            {
                uv = ComputeNormalizedDeviceCoordinates(p, UNITY_MATRIX_VP);
                float rayEye = LinearEyeDepth(p, UNITY_MATRIX_V);
                float sceneEye = LinearEyeDepth(SampleSceneDepthLod(uv), _ZBufferParams);
                return float3(uv, rayEye - sceneEye);   // z>0:光线跑到了场景表面后面
            }

            bool TraceReflection(float3 originWS, float3 dirWS, float jitter, out float2 hitUV, out float hitFade)
            {
                hitUV = 0; hitFade = 0;
                float3 prev = originWS;
                int steps = (int)_MaxSteps;
                for (int i = 1; i <= steps; i++)
                {
                    float3 p = originWS + dirWS * (_StepSize * (i + jitter));
                    float2 uv;
                    float diff = ProjectRay(p, uv).z;
                    if (any(uv < 0) || any(uv > 1)) return false;          // 走出屏幕:屏幕里没有信息了
                    if (diff > 0 && diff < _Thickness)                      // 穿过了一个“有厚度”的表面
                    {
                        // 二分细化:在 prev 与 p 之间逼近交点,减少台阶感
                        float3 a = prev, b = p;
                        for (int k = 0; k < 5; k++)
                        {
                            float3 m = (a + b) * 0.5;
                            float2 muv;
                            if (ProjectRay(m, muv).z > 0) b = m; else a = m;
                        }
                        ProjectRay(b, hitUV);
                        // 越靠屏幕边缘、越接近最大步数,越淡出,避免硬切
                        float2 edge = smoothstep(0, 0.08, hitUV) * smoothstep(1, 0.92, hitUV);
                        hitFade = edge.x * edge.y * (1 - (float)i / steps);
                        return true;
                    }
                    prev = p;
                }
                return false;
            }

            half4 Frag(Varyings input) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
                float2 uv = input.texcoord;
                half4 color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_PointClamp, uv);
                float depth = SampleDeviceDepth(uv);
                if (IsSkyDepth(depth)) return color;

                float3 normalWS = normalize(SampleSceneNormals(uv));
                if (normalWS.y < _MinNormalY) return color;               // 只让“地面”反射

                float3 positionWS = ReconstructPositionWS(uv, depth);
                float3 viewDirWS = normalize(positionWS - _WorldSpaceCameraPos);
                float3 reflDirWS = reflect(viewDirWS, normalWS);

                float jitter = InterleavedGradientNoise(input.positionCS.xy, 0);
                float2 hitUV; float fade;
                if (!TraceReflection(positionWS + normalWS * 0.02, reflDirWS, jitter, hitUV, fade))
                    return color;

                half3 reflColor = SAMPLE_TEXTURE2D_X_LOD(_BlitTexture, sampler_LinearClamp, hitUV, 0).rgb;
                float NoV = saturate(dot(normalWS, -viewDirWS));
                float fresnel = _F0 + (1 - _F0) * pow(1 - NoV, 5);         // Schlick
                color.rgb = lerp(color.rgb, reflColor, saturate(fresnel * fade * _Intensity));
                return color;
            }
            ENDHLSL
        }
    }
}