GameTech/URP 渲染实战

案例 25.3不写 C#:Full Screen Pass Renderer Feature入门

目标:学会使用 URP 14 自带的 FullScreenPassRendererFeature,并为它手写两个兼容的 Shader:一个读屏幕颜色的“夜视仪”,一个读深度图的“距离雾”。

原理:这个 Feature 的源码只有两百多行(位于 Runtime/RendererFeatures 目录下的 FullScreenPassRendererFeature.cs),做的事和案例 25.1 几乎一样:如果勾选了 fetchColorBuffer,先把相机颜色拷一份到 _FullscreenPassColorCopy,然后用你的材质在相机颜色上画一个全屏三角形。关键的执行部分如下(摘自 URP 14 源码):

private static void ExecuteMainPass(CommandBuffer cmd, RTHandle sourceTexture, Material material, int passIndex)
{
    s_SharedPropertyBlock.Clear();
    if (sourceTexture != null)
        s_SharedPropertyBlock.SetTexture(ShaderPropertyId.blitTexture, sourceTexture);       // _BlitTexture
    // We need to set the "_BlitScaleBias" uniform for user materials with shaders relying on core Blit.hlsl to work
    s_SharedPropertyBlock.SetVector(ShaderPropertyId.blitScaleBias, new Vector4(1, 1, 0, 0));
    cmd.DrawProcedural(Matrix4x4.identity, material, passIndex, MeshTopology.Triangles, 3, 1, s_SharedPropertyBlock);
}

源码注释说得很清楚:它把拷贝出来的颜色绑定为 _BlitTexture,并设置 _BlitScaleBias,专门为了让“基于 core Blit.hlsl 的用户 Shader”能直接工作。这正是我们手写 Shader 的方式——和案例 25.1 的 Shader 完全同构,Vert 直接用 Blit.hlsl 的。Shader Graph 的 Fullscreen 目标生成的也是同样的约定,所以手写版和 Shader Graph 版可以互换。

Inspector 上的几个设置:

字段含义
Injection Point只有三个选项:Before Rendering Transparents、Before Rendering Post Processing、After Rendering Post Processing(枚举值直接映射到对应的 RenderPassEvent)
Fetch Color Buffer是否先拷贝相机颜色作为 _BlitTexture。只做“叠加/混合”而不读屏幕时可以关掉,省一次全屏拷贝
Requirements需要的输入:Depth、Normal、Color、Motion。它直接传给 ConfigureInput
Pass Material / Pass Index用哪个材质的哪个 Pass
Bind Depth Stencil Attachment绘制时是否同时绑定相机深度/模板,打开后材质里的 ZTest、Stencil 才有效

夜视仪的片元着色器:亮度放大并压缩、哈希噪点、扫描线、圆形镜筒暗角,全部是逐像素的独立计算:

Assets/Cookbook/Ch25/FullscreenNightVision.shader(节选:frag)
half4 Frag(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    float2 uv = input.texcoord;
    half3 c = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv).rgb;

    // 亮度放大(模拟像增强管),再压一条曲线防止大片死白
    half luma = dot(c, half3(0.2126, 0.7152, 0.0722)) * _Gain;
    luma = luma / (1.0 + luma);

    // 颗粒噪点:按像素坐标取哈希,_Time 让它在运行时闪动(截图时 _Time≈0)
    float2 pixel = uv * _ScreenParams.xy;
    half grain = Hash12(pixel + frac(_Time.y * 13.0) * 517.0) - 0.5;
    luma = saturate(luma + grain * 0.12);

    // 扫描线:每 3 像素一条暗线
    half scan = 1.0 - _ScanlineStrength * step(2.0, fmod(pixel.y, 3.0));

    // 圆形镜筒暗角(按宽高比校正成正圆)
    float2 d = (uv - 0.5) * float2(_ScreenParams.x / _ScreenParams.y, 1.0);
    half vignette = smoothstep(_VignetteRadius, _VignetteRadius - 0.25, length(d));

    half3 result = _Tint.rgb * luma * scan * vignette;
    return half4(result, 1.0);
}
vec4 Frag()
{
    vec2 uv = texcoord;
    vec3 c = texture(_BlitTexture, uv).rgb;

    // 亮度放大(模拟像增强管),再压一条曲线防止大片死白
    float luma = dot(c, vec3(0.2126, 0.7152, 0.0722)) * _Gain;
    luma = luma / (1.0 + luma);

    // 颗粒噪点:按像素坐标取哈希,_Time 让它在运行时闪动(截图时 _Time≈0)
    vec2 pixel = uv * _ScreenParams.xy;
    float grain = Hash12(pixel + fract(_Time.y * 13.0) * 517.0) - 0.5;
    luma = saturate(luma + grain * 0.12);

    // 扫描线:每 3 像素一条暗线(pixel.y 为正,mod 与 HLSL 的 fmod 结果相同)
    float scan = 1.0 - _ScanlineStrength * step(2.0, mod(pixel.y, 3.0));

    // 圆形镜筒暗角(按宽高比校正成正圆)
    vec2 d = (uv - 0.5) * vec2(_ScreenParams.x / _ScreenParams.y, 1.0);
    float vignette = smoothstep(_VignetteRadius, _VignetteRadius - 0.25, length(d));

    vec3 result = _Tint.rgb * luma * scan * vignette;
    return vec4(result, 1.0);
}

距离雾需要深度图:include URP 的 DeclareDepthTexture.hlsl,用 SampleSceneDepth 读原始深度,再用 LinearEyeDepth(raw, _ZBufferParams) 转成观察空间的线性距离(单位:米)。_ZBufferParams 已经考虑了 Reversed-Z,所以这段代码在各平台上通用:

Assets/Cookbook/Ch25/FullscreenDepthFog.shader(节选:frag)
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_LinearClamp, uv);

    // 原始深度 → 观察空间线性深度(米)。Reversed-Z 的差异由 _ZBufferParams 处理
    float rawDepth = SampleSceneDepth(uv);
    float eyeDepth = LinearEyeDepth(rawDepth, _ZBufferParams);

    // 指数雾:f = 1 - e^(-density * 距离)
    float dist = max(eyeDepth - _FogStart, 0.0);
    half fog = 1.0 - exp(-_FogDensity * dist);
    color.rgb = lerp(color.rgb, _FogColor.rgb, fog);
    return color;
}
vec4 Frag()
{
    vec2 uv = texcoord;
    vec4 color = texture(_BlitTexture, uv);

    // 原始深度 → 观察空间线性深度(米)。Reversed-Z 的差异由 _ZBufferParams 处理
    float rawDepth = SampleSceneDepth(uv);
    float eyeDepth = LinearEyeDepth(rawDepth, _ZBufferParams);

    // 指数雾:f = 1 - e^(-density * 距离)
    float dist = max(eyeDepth - _FogStart, 0.0);
    float fog = 1.0 - exp(-_FogDensity * dist);
    color.rgb = mix(color.rgb, _FogColor.rgb, fog);
    return color;
}

用代码配置这个 Feature(Inspector 里的操作完全对应):

Assets/Cookbook/Ch25/Editor/Ch25Shots.cs(节选:shot-fullscreen)
[BookShot("ch25-fullscreen", W = 640, H = 400, Panels = 3)]
static void FullScreen(Shot s)
{
    Stage(s);
    // 加一排远处的柱子,让深度雾有层次
    for (int i = 0; i < 6; i++)
        s.Cylinder(s.Lit(new Color(0.8f, 0.78f, 0.72f), 0, 0.3f), new Vector3(-5f + i * 2f, 1.5f, 4f + i * 2.5f), 1f)
            .transform.localScale = new Vector3(0.6f, 1.5f, 0.6f);
    if (s.Panel == 0) return;

    var f = s.Feature<FullScreenPassRendererFeature>();
    if (s.Panel == 1)
    {
        f.passMaterial = s.Mat("Cookbook/Ch25/FullscreenNightVision");
        f.injectionPoint = FullScreenPassRendererFeature.InjectionPoint.AfterRenderingPostProcessing;
        f.fetchColorBuffer = true;                               // 需要读屏幕颜色
        f.requirements = ScriptableRenderPassInput.None;
    }
    else
    {
        f.passMaterial = s.Mat("Cookbook/Ch25/FullscreenDepthFog");
        f.injectionPoint = FullScreenPassRendererFeature.InjectionPoint.BeforeRenderingPostProcessing;
        f.fetchColorBuffer = true;
        f.requirements = ScriptableRenderPassInput.Depth;        // 需要 _CameraDepthTexture
    }
}
图 25-7左:原图;中:夜视仪(After Rendering Post Processing,Fetch Color Buffer 打开);右:距离雾(Before Rendering Post Processing,Requirements = Depth),远处的柱子逐渐隐入雾中
提示Requirements 勾了 Depth,URP 才保证 _CameraDepthTexture 在这个时间点之前已经生成(必要时会插入 Depth Prepass 或 Copy Depth)。如果忘了勾,在 URP Asset 打开了 Depth Texture 的项目里效果看起来也“正常”,换到一个关闭了 Depth Texture 的平台配置就全错——这是典型的“在我电脑上没问题”。

Full Screen Pass 的局限也很明显:只能单 Pass、只有三个插入点、不能分配自己的中间 RT、不能读 Volume。多 Pass 的模糊、Bloom,以及要接 Volume 的后处理,都需要像第 26 章那样自己写 Feature。