GameTech/URP 渲染实战
源码 / Ch02/FrameSnapshotFeature.cs
Ch02/FrameSnapshotFeature.cs
// 第 2 章 案例 2.1:帧快照 Renderer Feature
// 在 captureEvent 这个时间点,把相机颜色缓冲“拍”一张快照;
// 在后处理之后,再把快照贴回相机颜色缓冲。于是屏幕上看到的,就是这一帧在 captureEvent 时刻的“半成品”。
// 用它可以一格一格地看 URP 一帧是怎么画出来的(与 Frame Debugger 互相印证)。
// Renderer Feature 的完整写法在第 25 章详细讲,这里先当工具用。
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace Cookbook.Ch02
{
    public class FrameSnapshotFeature : ScriptableRendererFeature
    {
        [Tooltip("在哪个时间点拍快照")]
        public RenderPassEvent captureEvent = RenderPassEvent.AfterRenderingOpaques;

        [Tooltip("在画不透明物体之前把颜色缓冲清成黑色。天空盒模式下 URP 通常只清深度,颜色缓冲里会残留上一帧的内容")]
        public bool clearColorBeforeOpaques = true;

        ClearColorPass m_Clear;
        CapturePass m_Capture;
        RestorePass m_Restore;
        RTHandle m_Snapshot;

        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();
        }

        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);
            }
        }
    }
}