URP 渲染实战 / 第 25 章 Renderer Feature 与 RTHandle / 案例 25.2
案例 25.2生命周期探针与 RenderPassEvent 实验入门
目标:亲眼看到 25.2 节的调用顺序;再用同一个“去色” Pass 放在三个不同事件上,观察插入点对结果的影响,顺便理解案例 25.1 最后那行“恢复渲染目标”的必要性。
探针 Feature 什么也不画,只在每个回调里打印一行:
Assets/Cookbook/Ch25/LifecycleProbeFeature.cs(节选:probe)
public override void Create() { Log("Feature.Create"); m_Pass = new ProbePass(); } public override void OnCameraPreCull(ScriptableRenderer renderer, in CameraData cameraData) { s_Frame++; Log($"Feature.OnCameraPreCull camera={cameraData.camera.name}"); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { Log("Feature.AddRenderPasses"); m_Pass.renderPassEvent = passEvent; m_Pass.ConfigureInput(ScriptableRenderPassInput.Depth); // 必须在这里声明输入 renderer.EnqueuePass(m_Pass); } public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData) { // 此时相机目标已分配,可以安全读取 Log($"Feature.SetupRenderPasses color={renderer.cameraColorTargetHandle.name}"); } protected override void Dispose(bool disposing) => Log("Feature.Dispose"); class ProbePass : ScriptableRenderPass { public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) => Log(" Pass.OnCameraSetup"); public override void Configure(CommandBuffer cmd, RenderTextureDescriptor desc) => Log($" Pass.Configure {desc.width}x{desc.height} msaa={desc.msaaSamples}"); public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) => Log($" Pass.Execute event={renderPassEvent}"); public override void OnCameraCleanup(CommandBuffer cmd) => Log(" Pass.OnCameraCleanup"); public override void OnFinishCameraStackRendering(CommandBuffer cmd) => Log(" Pass.OnFinishCameraStackRendering"); }
// 案例 25.2:生命周期探针的网页版(对应 Ch25/LifecycleProbeFeature.cs)。 // 它什么也不画,只把回调顺序打到浏览器控制台(console.debug),用来观察 Feature / Pass 在一帧里被调用的顺序。 import { RenderPassEvent } from '../engine/features.js'; let s_Frame = 0; const log = (s) => console.debug(`[帧${s_Frame}] ${s}`); export default class LifecycleProbeFeature { constructor(engine, data) { this.passEvent = (data && data.passEvent) ?? RenderPassEvent.AfterRenderingOpaques; log('Feature.Create'); } addPasses(frame, enqueue) { s_Frame++; log(`Feature.OnCameraPreCull camera=${frame.camera.json.name || 'Camera'}`); log('Feature.AddRenderPasses'); frame.requireDepth = true; // ConfigureInput(ScriptableRenderPassInput.Depth) log('Feature.SetupRenderPasses color=_CameraColorAttachment'); enqueue(this.passEvent, (fr) => { log(' Pass.OnCameraSetup'); log(` Pass.Configure ${fr.width}x${fr.height} msaa=${fr.msaa}`); log(` Pass.Execute event=${this.passEvent}`); log(' Pass.OnCameraCleanup'); }, 'Ch25 Lifecycle Probe'); } }
下面是配图脚本渲染一张图(连续渲染 3 帧)时真实写出的日志,删去了重复的第 2 帧:
[帧0] Feature.Create [帧0] Feature.Create [帧1] Feature.OnCameraPreCull camera=Main Camera [帧1] Feature.AddRenderPasses [帧1] Feature.SetupRenderPasses color=_CameraColorAttachmentA [帧1] Pass.OnCameraSetup [帧1] Pass.Configure 640x400 msaa=1 [帧1] Pass.Execute event=AfterRenderingOpaques [帧1] Pass.OnCameraCleanup [帧1] Pass.OnFinishCameraStackRendering …… [帧3] Pass.OnFinishCameraStackRendering [帧3] Feature.Dispose
几个观察:
Create被调用了两次:一次来自ScriptableObject.CreateInstance触发的OnEnable,一次来自 Renderer 构造函数。这验证了“Create 不只调一次”。- 相机颜色目标的名字是
_CameraColorAttachmentA:UniversalRenderer 的颜色缓冲是 A/B 两块(RenderTargetBufferSystem),后处理会在两块之间切换,所以每帧都要重新取cameraColorTargetHandle,不能在第一帧缓存起来一直用。 OnCameraSetup在Configure之前;两者都在同一帧的Execute之前完成,此时所有 Pass 都已入队。- 单相机(无相机栈)时,
OnFinishCameraStackRendering每帧都会调用——单个相机本身就是“只有一个成员的相机栈”。
接下来是事件实验。场景里有不透明物体、天空和两个半透明物体(青色球、品红方块),把案例 25.1 的 Feature 设为 desaturate = 1,只改 passEvent:

左图能正确显示,依赖的正是 Pass 最后那一行 CoreUtils.SetRenderTarget(cmd, color, depth)。原因藏在 ScriptableRenderer.SetRenderPassAttachments 里:Renderer 会记住“当前绑定的是哪张颜色、哪张深度”(m_ActiveColorAttachments、m_ActiveDepthAttachment),下一个 Pass 要用的目标如果和记录的一样,就不再重新绑定。而 Blitter.BlitCameraTexture 内部用 CoreUtils.SetRenderTarget(cmd, destination) 只绑定了颜色——深度被解绑了,但 Renderer 不知道。如果不恢复,紧接着的天空盒 Pass 会在“没有深度缓冲”的状态下绘制,结果是天空盖住整个画面;透明物体也失去深度测试,会画到不透明物体前面。
✓ 推荐
自己切换过渲染目标的 Pass,结束前把目标恢复成相机颜色+深度;或者用ConfigureTarget 明确告诉 Renderer 本 Pass 画到哪里(案例 25.5、25.6 的做法),让 Renderer 自己负责后续的重新绑定。✗ 避免
在 Execute 里随意cmd.SetRenderTarget 后直接返回。效果放在 BeforeRenderingPostProcessing 时也许看不出问题(后处理会自己绑定目标),换一个事件就出错,而且这种错很难从现象倒推原因。