GameTech/URP 渲染实战

案例 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 去做:

  1. 把角色放到单独的层(本例用第 8 层),并从 Universal Renderer Data 的 Opaque Layer Mask 里去掉这一层——默认的不透明 Pass 不再画角色。
  2. Feature A(Character Visible):事件 AfterRenderingOpaques,过滤 Opaque 队列 + 角色层,不覆盖材质(用角色自己的 Lit),覆盖模板状态:Ref 1、Comp Always、Pass Replace。角色被正常画出来,同时在可见像素上写入 1。
  3. Feature B(Character Occluded):同样的事件和过滤条件,覆盖材质为 XRaySilhouette,覆盖深度状态为 Greater、不写深度,覆盖模板状态为 Ref 1、Comp NotEqual。
  4. 两个 Feature 的事件相同,URP 按它们在列表中的顺序入队,所以 A 一定先于 B 执行。

在 Inspector 里就是按上面的步骤在 Renderer Data 上点选(Add Renderer Feature › Render Objects)。配图脚本里用代码完成同样的设置:

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)。

图 16-14角色使用 URP Lit,未做任何修改。左:没有 Feature,墙后的半个身体看不见;中:两个 Feature 但不启用模板,被遮挡部分显示了轮廓色,但横在胸前的手臂上也叠了一层(那是手臂后面被手臂挡住的躯干);右:启用模板,只有被墙挡住的部分显示轮廓色
✓ RenderObjects 方案
不改角色材质,任何 Shader 都适用;可以按层批量开关;美术在 Inspector 里就能配置。
✗ 需要注意
角色被移出默认 Pass,依赖默认 Pass 的功能(例如 Depth Priming、部分调试视图)要单独验证;两个 Feature 各是一次额外的绘制遍历;要求模板缓冲可用(URP 的深度附件带 8 位模板)。
提示RenderObjects 还常用来做“第一人称武器不穿墙”(单独的层、清深度后最后画,或用 Custom Camera 设置改 FOV)、“只给某一层物体描边”等。更灵活的自定义绘制(自己的 DrawRenderers + FilteringSettings + ShaderTagId)放在第 25 章讲。