GameTech/URP 渲染实战

案例 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

几个观察:

接下来是事件实验。场景里有不透明物体、天空和两个半透明物体(青色球、品红方块),把案例 25.1 的 Feature 设为 desaturate = 1,只改 passEvent:

图 25-6同一个去色 Pass 放在不同事件上。左:AfterRenderingOpaques,天空和半透明物体在它之后才画,所以仍是彩色;中:AfterRenderingSkybox,天空也变灰了,半透明物体仍是彩色;右:AfterRenderingTransparents,全部变灰

左图能正确显示,依赖的正是 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 时也许看不出问题(后处理会自己绑定目标),换一个事件就出错,而且这种错很难从现象倒推原因。