URP 渲染实战 / 第 16 章 透明、混合与模板测试 / 案例 16.9
案例 16.9用 RenderObjects 显示被遮挡部分的轮廓色进阶
目标:不修改角色的任何材质和 Shader(角色用的是 URP 自带的 Lit),只通过 URP 自带的 Render Objects Renderer Feature,实现案例 16.7 的效果。
原理:RenderObjects 是 URP 自带的一个 Renderer Feature,能在指定的时机(RenderPassEvent)把指定层(Layer)、指定队列的物体再画一遍,并可以覆盖材质、深度状态和模板状态。先看它在 URP 14 源码中暴露的设置字段:
// 摘自 URP 14 Runtime/RendererFeatures/RenderObjects.cs(RenderObjectsSettings,节选) public string passTag = "RenderObjectsFeature"; public RenderPassEvent Event = RenderPassEvent.AfterRenderingOpaques; public FilterSettings filterSettings = new FilterSettings(); // RenderQueueType、LayerMask、PassNames public Material overrideMaterial = null; public int overrideMaterialPassIndex = 0; public OverrideMaterialMode overrideMode = OverrideMaterialMode.Material; // None / Material / Shader public bool overrideDepthState = false; public CompareFunction depthCompareFunction = CompareFunction.LessEqual; public bool enableWrite = true; public StencilStateData stencilSettings = new StencilStateData(); // overrideStencilState、stencilReference、Comp、passOperation…
方案用两个 Feature 配合,思路和案例 16.7 完全相同,只是“写模板”和“画剪影”都交给 Feature 去做:
- 把角色放到单独的层(本例用第 8 层),并从 Universal Renderer Data 的 Opaque Layer Mask 里去掉这一层——默认的不透明 Pass 不再画角色。
- Feature A(Character Visible):事件 AfterRenderingOpaques,过滤 Opaque 队列 + 角色层,不覆盖材质(用角色自己的 Lit),覆盖模板状态:Ref 1、Comp Always、Pass Replace。角色被正常画出来,同时在可见像素上写入 1。
- Feature B(Character Occluded):同样的事件和过滤条件,覆盖材质为 XRaySilhouette,覆盖深度状态为 Greater、不写深度,覆盖模板状态为 Ref 1、Comp NotEqual。
- 两个 Feature 的事件相同,URP 按它们在列表中的顺序入队,所以 A 一定先于 B 执行。
在 Inspector 里就是按上面的步骤在 Renderer Data 上点选()。配图脚本里用代码完成同样的设置:
Assets/Cookbook/Ch16/Editor/Shots16.cs(节选:ro-setup)
// 1) 默认的不透明 Pass 不再画角色层,交给下面两个 Feature s.Renderer.opaqueLayerMask = ~(1 << CharacterLayer); // 2) 第一个 Feature:正常画角色,同时把模板写成 1(标记“角色可见的像素”) var visible = s.Feature<RenderObjects>(); visible.settings.passTag = "Character Visible"; visible.settings.Event = RenderPassEvent.AfterRenderingOpaques; visible.settings.filterSettings.RenderQueueType = RenderQueueType.Opaque; visible.settings.filterSettings.LayerMask = 1 << CharacterLayer; visible.settings.overrideMode = RenderObjects.RenderObjectsSettings.OverrideMaterialMode.None; visible.settings.stencilSettings.overrideStencilState = s.Panel == 2; visible.settings.stencilSettings.stencilReference = 1; visible.settings.stencilSettings.stencilCompareFunction = CompareFunction.Always; visible.settings.stencilSettings.passOperation = StencilOp.Replace; // 3) 第二个 Feature:用剪影材质再画一遍,深度测试改成 Greater、不写深度, // 并且只在模板 ≠ 1 的地方画(排除角色自己挡住自己的情况) var occluded = s.Feature<RenderObjects>(); occluded.settings.passTag = "Character Occluded"; occluded.settings.Event = RenderPassEvent.AfterRenderingOpaques; occluded.settings.filterSettings.RenderQueueType = RenderQueueType.Opaque; occluded.settings.filterSettings.LayerMask = 1 << CharacterLayer; occluded.settings.overrideMode = RenderObjects.RenderObjectsSettings.OverrideMaterialMode.Material; occluded.settings.overrideMaterial = silhouette; occluded.settings.overrideMaterialPassIndex = 0; occluded.settings.overrideDepthState = true; occluded.settings.depthCompareFunction = CompareFunction.Greater; occluded.settings.enableWrite = false; occluded.settings.stencilSettings.overrideStencilState = s.Panel == 2; occluded.settings.stencilSettings.stencilReference = 1; occluded.settings.stencilSettings.stencilCompareFunction = CompareFunction.NotEqual; occluded.settings.stencilSettings.passOperation = StencilOp.Keep; // 4) Feature 在 OnEnable 时就用默认设置 Create 过一次,改完设置要再 Create 一次 visible.Create(); occluded.Create();
代码最后的 Create() 很关键。ScriptableRendererFeature 在 OnEnable 和 OnValidate 里都会调用 Create(),而 RenderObjects 的 Create() 会根据当时的 settings 构造 RenderObjectsPass,把层、事件、深度/模板状态都“烘焙”进 Pass 对象。用 ScriptableObject.CreateInstance 新建 Feature 时,OnEnable 已经用默认设置调用过一次 Create,之后在代码里改 settings 不会自动生效——必须手动再调一次(在 Inspector 里改不需要,因为会触发 OnValidate)。

✓ RenderObjects 方案
不改角色材质,任何 Shader 都适用;可以按层批量开关;美术在 Inspector 里就能配置。✗ 需要注意
角色被移出默认 Pass,依赖默认 Pass 的功能(例如 Depth Priming、部分调试视图)要单独验证;两个 Feature 各是一次额外的绘制遍历;要求模板缓冲可用(URP 的深度附件带 8 位模板)。提示RenderObjects 还常用来做“第一人称武器不穿墙”(单独的层、清深度后最后画,或用 Custom Camera 设置改 FOV)、“只给某一层物体描边”等。更灵活的自定义绘制(自己的
DrawRenderers + FilteringSettings + ShaderTagId)放在第 25 章讲。