GameTech/URP 渲染实战
源码 / shaders/Ch28/SimpleSSR.glsl
shaders/Ch28/SimpleSSR.glsl
// 案例 28.6:最简屏幕空间反射(SSR)。世界空间线性步进 + 二分细化,命中后从屏幕颜色里取反射色。
// 只对“朝上”的表面生效(模拟湿地面),用 ScreenEffectFeature 驱动,需要 Depth + Normal。
Shader "Cookbook/Ch28/SimpleSSR"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
        ZWrite Off ZTest Always Cull Off Blend Off

        Pass
        {
            Name "SSR"
            GLSLPROGRAM
            #include "Ch28/ScreenSpaceCommon.glsl"

            uniform float _MaxSteps, _StepSize, _Thickness, _F0, _Intensity, _MinNormalY;

            #ifdef FRAGMENT
            float InterleavedGradientNoise(vec2 pixCoord, int frameCount)
            {
                pixCoord += float(frameCount) * vec2(2.083, 4.867);
                return fract(52.9829189 * fract(dot(pixCoord, vec2(0.06711056, 0.00583715))));
            }

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

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

            vec4 Frag()
            {
                vec2 uv = texcoord;
                vec4 color = SamplePointClamp(_BlitTexture, uv);
                float depth = SampleDeviceDepth(uv);
                if (IsSkyDepth(depth)) return color;

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

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

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

                vec3 reflColor = textureLod(_BlitTexture, hitUV, 0.0).rgb;
                float NoV = saturate(dot(normalWS, -viewDirWS));
                float fresnel = _F0 + (1.0 - _F0) * pow(1.0 - NoV, 5.0);         // Schlick
                color.rgb = mix(color.rgb, reflColor, saturate(fresnel * fade * _Intensity));
                return color;
            }
            void main() { fragColor = Frag(); }
            #endif
            ENDGLSL
        }
    }
}