URP 渲染实战 / 第 25 章 Renderer Feature 与 RTHandle / 案例 25.5
案例 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 里手动设置目标:
ConfigureTarget(mask, cameraDepth)告诉 Renderer:执行这个 Pass 之前,把颜色绑定为遮罩、深度绑定为相机深度。Renderer 记录下这次绑定,下一个 Pass 要用相机颜色时,它知道目标变了,会自动重新绑定——不需要我们手动恢复。ConfigureClear(ClearFlag.Color, Color.clear)只清颜色。千万不能写ClearFlag.All:那会把相机的深度缓冲清掉,后面的天空盒、透明物体全部出错。- 遮罩描述符的 MSAA 采样数沿用相机描述符。原因是颜色和深度一起绑定时,两者的采样数必须一致;相机开了 4x MSAA 时,相机深度也是 4x 的。采样这张多重采样遮罩时,Unity 会自动解析(resolve)。
- 格式用
GraphicsFormat.R8_UNorm:遮罩只需要一个通道 8 位,比相机颜色格式省 3/4 的带宽。
