URP 渲染实战 / 第 2 章 URP 架构与一帧的流程 / 案例 2.1
案例 2.1帧快照:一格一格看一帧怎么画出来进阶
目标:写一个 Renderer Feature,在指定的 RenderPassEvent 时刻把相机颜色缓冲“拍”一张快照,等整帧画完再贴回屏幕。这样就能在 Game 视图里直接看到这一帧在不透明之后、天空盒之后、透明之后各是什么样,与 Frame Debugger 互相印证。
原理:两个 Pass。CapturePass 挂在 captureEvent 上,把当前相机颜色复制到一张 RT;RestorePass 挂在 AfterRenderingPostProcessing 上,把快照复制回相机颜色缓冲,随后 FinalBlit 把它送上屏幕。Renderer Feature 的完整写法是第 25 章的内容,这里先当工具用,只看关键部分:
Assets/Cookbook/Ch02/FrameSnapshotFeature.cs(节选:feature)
public override void Create() { m_Clear = new ClearColorPass { renderPassEvent = RenderPassEvent.BeforeRenderingOpaques }; m_Capture = new CapturePass(); m_Restore = new RestorePass(); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { // 只处理游戏相机,Scene 视图、预览窗口保持原样 if (renderingData.cameraData.cameraType != CameraType.Game) return; m_Capture.renderPassEvent = captureEvent; // 贴回去的时机:后处理之后、FinalBlit 之前 m_Restore.renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing; if (clearColorBeforeOpaques) renderer.EnqueuePass(m_Clear); renderer.EnqueuePass(m_Capture); renderer.EnqueuePass(m_Restore); } public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData) { if (renderingData.cameraData.cameraType != CameraType.Game) return; // 快照的格式与相机颜色缓冲一致(HDR 时是浮点格式),但不需要深度、不需要 MSAA var desc = renderingData.cameraData.cameraTargetDescriptor; desc.depthBufferBits = 0; desc.msaaSamples = 1; RenderingUtils.ReAllocateIfNeeded(ref m_Snapshot, desc, FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_FrameSnapshot"); m_Capture.Setup(m_Snapshot); m_Restore.Setup(m_Snapshot); } protected override void Dispose(bool disposing) { m_Snapshot?.Release(); }
// 第 2 章 案例 2.1:帧快照 Renderer Feature(JS 版,对应 Ch02/FrameSnapshotFeature.cs) // 在 captureEvent 这个时间点,把相机颜色缓冲“拍”一张快照; // 在后处理之后,再把快照贴回相机颜色缓冲。于是屏幕上看到的,就是这一帧在 captureEvent 时刻的“半成品”。 import { RenderPassEvent } from '../engine/features.js'; export default class FrameSnapshotFeature { constructor(engine, data) { this.engine = engine; this.captureEvent = data.captureEvent ?? RenderPassEvent.AfterRenderingOpaques; // 在哪个时间点拍快照 this.clearColorBeforeOpaques = data.clearColorBeforeOpaques ?? true; // 画不透明物体之前把颜色清成黑色 } addPasses(frame, enqueue) { const E = this.engine, gl = E.gl; let snapshot = null; // ClearColorPass:此时相机颜色/深度缓冲已经绑定,只清颜色 if (this.clearColorBeforeOpaques) { enqueue(RenderPassEvent.BeforeRenderingOpaques, (fr) => { fr.bindCamera(); gl.colorMask(true, true, true, true); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); }, 'Cookbook Snapshot Clear'); } // CapturePass:快照格式与相机颜色缓冲一致(HDR 时是浮点),不要深度、不要 MSAA。 // cameraColorTexture() 会先把多重采样缓冲解析(resolve)出来,读到的才是此刻的画面 enqueue(this.captureEvent, (fr) => { const rt = fr.getTemporaryRT('_FrameSnapshot', fr.width, fr.height, { color: fr.colorFormat }); fr.blit(fr.cameraColorTexture(), rt, E.blitMat, 'Copy'); snapshot = rt.texture; fr.bindCamera(); }, 'Cookbook Snapshot Capture'); // RestorePass:后处理之后、FinalBlit 之前,把快照贴回“当前”的相机颜色(后处理开启时就是后处理的输出) enqueue(RenderPassEvent.AfterRenderingPostProcessing, (fr) => { if (!snapshot) return; if (fr.postDone) fr.postOutput = snapshot; else { fr.blit(snapshot, 'camera', E.blitMat, 'Copy'); fr.bindCamera(); } }, 'Cookbook Snapshot Restore'); } }
Assets/Cookbook/Ch02/FrameSnapshotFeature.cs(节选:passes)
class ClearColorPass : ScriptableRenderPass { public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { // 没有调用 ConfigureTarget,执行到这里时 URP 已经绑定了相机颜色/深度缓冲 var cmd = CommandBufferPool.Get(); cmd.ClearRenderTarget(RTClearFlags.Color, Color.black, 1f, 0); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } } class CapturePass : ScriptableRenderPass { RTHandle m_Destination; public CapturePass() { profilingSampler = new ProfilingSampler("Cookbook Snapshot Capture"); } public void Setup(RTHandle destination) { m_Destination = destination; } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { // 告诉 URP 这个 Pass 的输出目标,URP 才知道之后的 Pass 需要重新绑定相机目标 ConfigureTarget(m_Destination); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { // 相机颜色缓冲在 Execute 里现取:后处理开启时 URP 用两个缓冲交替(swap buffer), // 在 SetupRenderPasses 里缓存的句柄不一定是此刻正在画的那一个 var source = renderingData.cameraData.renderer.cameraColorTargetHandle; var cmd = CommandBufferPool.Get(); using (new ProfilingScope(cmd, profilingSampler)) { // 开了 MSAA 时,相机颜色缓冲是多重采样纹理,“解析”(resolve)之前直接采样它, // 读到的可能是上一次解析留下的旧内容。这里显式解析到快照里,读到的才是此刻的画面。 if (source.rt != null && source.rt.antiAliasing > 1) cmd.ResolveAntiAliasedSurface(source.rt, m_Destination.rt); else Blitter.BlitCameraTexture(cmd, source, m_Destination); } context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } } class RestorePass : ScriptableRenderPass { RTHandle m_Snapshot; public RestorePass() { profilingSampler = new ProfilingSampler("Cookbook Snapshot Restore"); } public void Setup(RTHandle snapshot) { m_Snapshot = snapshot; } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { // 后处理会在两个颜色缓冲之间来回切换(swap buffer),所以在 Execute 里现取“当前”的相机颜色缓冲 var target = renderingData.cameraData.renderer.cameraColorTargetHandle; var cmd = CommandBufferPool.Get(); using (new ProfilingScope(cmd, profilingSampler)) Blitter.BlitCameraTexture(cmd, m_Snapshot, target); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } }
// 第 2 章 案例 2.1:帧快照 Renderer Feature(JS 版,对应 Ch02/FrameSnapshotFeature.cs) // 在 captureEvent 这个时间点,把相机颜色缓冲“拍”一张快照; // 在后处理之后,再把快照贴回相机颜色缓冲。于是屏幕上看到的,就是这一帧在 captureEvent 时刻的“半成品”。 import { RenderPassEvent } from '../engine/features.js'; export default class FrameSnapshotFeature { constructor(engine, data) { this.engine = engine; this.captureEvent = data.captureEvent ?? RenderPassEvent.AfterRenderingOpaques; // 在哪个时间点拍快照 this.clearColorBeforeOpaques = data.clearColorBeforeOpaques ?? true; // 画不透明物体之前把颜色清成黑色 } addPasses(frame, enqueue) { const E = this.engine, gl = E.gl; let snapshot = null; // ClearColorPass:此时相机颜色/深度缓冲已经绑定,只清颜色 if (this.clearColorBeforeOpaques) { enqueue(RenderPassEvent.BeforeRenderingOpaques, (fr) => { fr.bindCamera(); gl.colorMask(true, true, true, true); gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); }, 'Cookbook Snapshot Clear'); } // CapturePass:快照格式与相机颜色缓冲一致(HDR 时是浮点),不要深度、不要 MSAA。 // cameraColorTexture() 会先把多重采样缓冲解析(resolve)出来,读到的才是此刻的画面 enqueue(this.captureEvent, (fr) => { const rt = fr.getTemporaryRT('_FrameSnapshot', fr.width, fr.height, { color: fr.colorFormat }); fr.blit(fr.cameraColorTexture(), rt, E.blitMat, 'Copy'); snapshot = rt.texture; fr.bindCamera(); }, 'Cookbook Snapshot Capture'); // RestorePass:后处理之后、FinalBlit 之前,把快照贴回“当前”的相机颜色(后处理开启时就是后处理的输出) enqueue(RenderPassEvent.AfterRenderingPostProcessing, (fr) => { if (!snapshot) return; if (fr.postDone) fr.postOutput = snapshot; else { fr.blit(snapshot, 'camera', E.blitMat, 'Copy'); fr.bindCamera(); } }, 'Cookbook Snapshot Restore'); } }
这段代码里藏着三个在 URP 14 中写 Pass 的要点,都是实测踩出来的:
- 相机颜色缓冲要在 Execute 里现取。后处理开启时 URP 用 A/B 两张缓冲交替,在
SetupRenderPasses里缓存的句柄可能不是不透明 Pass 正在画的那一张。 - MSAA 下要显式解析。开了 MSAA,相机颜色缓冲是多重采样纹理,“解析(Resolve)”到普通纹理之前直接采样,读到的可能是上一次解析的旧内容。这里用
CommandBuffer.ResolveAntiAliasedSurface解析到快照里。 - 想看“不透明之后”的真实背景,得先清屏。前面说过天空盒模式下 URP 只清深度,所以 Feature 在
BeforeRenderingOpaques加了一个清成黑色的 Pass(clearColorBeforeOpaques),否则快照的背景里是上一帧的残影。

对照四张图:发光球在①②③里是一团刺眼的黄色,到了④才变成带辉光的橙色——前三张是 HDR 颜色缓冲里“原始的”线性值(超过 1 的部分在显示时被截断),色调映射之后才回到可显示的范围,Bloom 也是在后处理阶段才加上的。透明物体在③才出现,所以①②阶段拷贝出的 Opaque Texture 里不会有它们。
本案例出自《URP 渲染实战》第 2 章「URP 架构与一帧的流程」。在章节中阅读上下文 →