GameTech/URP 渲染实战

案例 25.5只给某个 Layer 画一张遮罩进阶

目标:把第 8 层的物体画成纯白剪影,写进一张单通道遮罩 RT。支持两种模式:“透视”(被别的物体挡住的部分也写入)和“只写可见部分”。这张遮罩是案例 25.6 描边的输入,也可以用来做选中高亮、局部后处理等。

原理:遮罩 RT 和相机同尺寸;绘制时绑定遮罩做颜色、相机深度做深度。用 RenderStateBlock 在 C# 里切换深度测试:透视模式 ZTest Always,否则 ZTest LEqual——物体自己已经在不透明 Pass 里写过深度,等于自己时通过;被别的物体挡住的部分深度更大,不通过。替换材质的 Shader 只输出 1,并且 ZWrite Off,保证绝不修改相机深度:

Assets/Cookbook/Ch25/LayerMaskSolid.shader
// 案例 25.5:DrawRenderers 的 Override Material——把物体画成纯白剪影,写进遮罩 RT。
Shader "Cookbook/Ch25/LayerMaskSolid"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
        Pass
        {
            Name "Mask"
            ZWrite Off      // 绝不改动相机深度;深度比较方式由 C# 的 RenderStateBlock 决定
            Cull Back

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes { float4 positionOS : POSITION; };
            struct Varyings { float4 positionCS : SV_POSITION; };

            Varyings vert(Attributes v)
            {
                Varyings o;
                o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
                return o;
            }

            half4 frag(Varyings i) : SV_Target { return 1.0; }
            ENDHLSL
        }
    }
}
// 案例 25.5:DrawRenderers 的 Override Material——把物体画成纯白剪影,写进遮罩 RT。
Shader "Cookbook/Ch25/LayerMaskSolid"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
        Pass
        {
            Name "Mask"
            ZWrite Off      // 绝不改动相机深度;深度比较方式由 C# 的 RenderStateBlock 决定
            Cull Back

            GLSLPROGRAM
            #ifdef VERTEX
            void main()
            {
                // 对应 HLSL 的 TransformObjectToHClip。这里拆成“物体→世界→裁剪”两步,和 Lit 的顶点着色器
                // (GetVertexPositionInputs)运算顺序一致,保证算出的深度逐位相同,LEqual 测试不会闪烁
                gl_Position = TransformWorldToHClip(TransformObjectToWorld(positionOS.xyz));
            }
            #endif
            #ifdef FRAGMENT
            void main() { fragColor = vec4(1.0); }
            #endif
            ENDGLSL
        }
    }
}
Assets/Cookbook/Ch25/LayerOutlineFeature.cs(节选:mask-pass)
class MaskPass : ScriptableRenderPass
{
    // 只画原材质里有这些 LightMode 的物体(URP 前向渲染用到的三个标签)
    static readonly List<ShaderTagId> s_ShaderTags = new List<ShaderTagId>
    {
        new ShaderTagId("UniversalForward"),
        new ShaderTagId("UniversalForwardOnly"),
        new ShaderTagId("SRPDefaultUnlit"),
    };

    readonly Material m_Material;
    FilteringSettings m_Filtering;
    RenderStateBlock m_StateBlock;
    public RTHandle mask;           // 遮罩 RT(自己分配)
    public RTHandle cameraDepth;    // 借用相机深度做遮挡测试

    public MaskPass(Material material)
    {
        m_Material = material;
        profilingSampler = new ProfilingSampler("Ch25 Layer Mask");
        m_Filtering = new FilteringSettings(RenderQueueRange.all);
    }

    public void Setup(Settings s)
    {
        m_Filtering.layerMask = s.layerMask;
        // 用 RenderStateBlock 覆盖材质里的深度状态:透视模式 ZTest Always,否则 LEqual
        m_StateBlock = new RenderStateBlock(RenderStateMask.Depth)
        {
            depthState = new DepthState(false, s.seeThrough ? CompareFunction.Always : CompareFunction.LessEqual)
        };
    }

    public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
    {
        // 遮罩与相机同尺寸、同 MSAA(要和相机深度一起绑定,两者采样数必须一致),单通道 8 位即可
        var desc = renderingData.cameraData.cameraTargetDescriptor;
        desc.graphicsFormat = GraphicsFormat.R8_UNorm;
        desc.depthBufferBits = 0;
        RenderingUtils.ReAllocateIfNeeded(ref mask, desc, FilterMode.Bilinear,
            TextureWrapMode.Clamp, name: "_Ch25LayerMask");

        // 告诉 URP:这个 Pass 画到 mask 上,深度用相机深度;开始前只清颜色(深度绝不能清)
        ConfigureTarget(mask, cameraDepth);
        ConfigureClear(ClearFlag.Color, Color.clear);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        var cmd = CommandBufferPool.Get();
        using (new ProfilingScope(cmd, profilingSampler))
        {
            // 先把命令缓冲里已有的命令提交,再调用 context 级别的 DrawRenderers
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();

            var drawing = CreateDrawingSettings(s_ShaderTags, ref renderingData, SortingCriteria.CommonOpaque);
            drawing.overrideMaterial = m_Material;          // 所有物体都换成纯白材质
            drawing.overrideMaterialPassIndex = 0;
            context.DrawRenderers(renderingData.cullResults, ref drawing, ref m_Filtering, ref m_StateBlock);
        }
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }

    public void Dispose()
    {
        mask?.Release();
        mask = null;
    }
}
maskPass(fr) {
  const E = fr.engine, gl = fr.gl, s = this.settings;
  // 遮罩与相机同尺寸、同 MSAA(要和相机深度一起绑定,两者采样数必须一致),单通道 8 位即可
  const rt = E.rt('ch25:_Ch25LayerMask', fr.width, fr.height, { color: 'R8', depth: null, samples: fr.msaa, renderbuffers: true });
  // ConfigureTarget(mask, cameraDepth):把相机的深度缓冲挂到遮罩的 FBO 上(借用,不清除)
  gl.bindFramebuffer(gl.FRAMEBUFFER, rt.fbo);
  gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, fr.cameraTarget.depthRB);
  E.bindTarget(rt);
  // ConfigureClear(ClearFlag.Color, Color.clear):只清颜色,深度绝不能清
  gl.colorMask(true, true, true, true);
  gl.clearColor(0, 0, 0, 0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  // RenderStateBlock 覆盖深度状态:透视模式 ZTest Always,否则 LEqual;不写深度
  const pass = this.maskMaterial.shader.passes[0];
  fr.drawRenderers({
    queue: 'all', lightModes: SHADER_TAGS, layerMask: layerMask(s.layerMask),
    overrideMaterial: this.maskMaterial, overridePass: 0,
    state: { ...pass.state, zwrite: 'Off', ztest: s.seeThrough ? 'Always' : 'LEqual' },
  });
  E.glc.resolve(rt, { color: true });
  this.mask = rt.texture;
}

这里和案例 25.1 最大的不同是用了 ConfigureTarget 与 ConfigureClear,而不是在 Execute 里手动设置目标:

图 25-9左:场景,红球前面挡了一块板;中:透视模式的遮罩(ZTest Always),被挡住的红球也完整写入;右:只写可见部分(ZTest LEqual),红球只剩露出板子上沿的一小块