GameTech/URP 渲染实战
源码 / Ch25/TintFeature.cs
Ch25/TintFeature.cs
// 案例 25.1 / 25.2:一个完整、可直接用于项目的全屏效果 Renderer Feature(URP 14 写法)。
// 结构:Feature(资源与入队) + Pass(RTHandle 与绘制)。
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace Cookbook.Ch25
{
    public class TintFeature : ScriptableRendererFeature
    {
        [System.Serializable]
        public class Settings
        {
            public RenderPassEvent passEvent = RenderPassEvent.BeforeRenderingPostProcessing;
            public Color tint = Color.white;
            [Range(0, 1)] public float invert = 1f;
            [Range(0, 1)] public float desaturate = 0f;
        }

        public Settings settings = new Settings();
        // 在 Inspector 里拖入 Cookbook/Ch25/Tint。直接引用能保证打包时 Shader 不被剔除;
        // 留空时退回 Shader.Find(只在编辑器或 Shader 已被别处引用时可靠)。
        public Shader shader;

        Material m_Material;
        TintPass m_Pass;

        // Create 在 OnEnable、以及 Inspector 每次改值(OnValidate)时都会被调用,
        // 所以这里必须先清理上一次创建的东西,否则每改一次参数就泄漏一份材质和 RT。
        public override void Create()
        {
            m_Pass?.Dispose();
            CoreUtils.Destroy(m_Material);

            if (shader == null) shader = Shader.Find("Cookbook/Ch25/Tint");
            m_Material = shader != null ? CoreUtils.CreateEngineMaterial(shader) : null;
            m_Pass = new TintPass(m_Material);
        }

        // 每个相机、每帧调用一次:决定这一帧要不要把 Pass 放进队列
        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            if (m_Material == null) return;
            var camType = renderingData.cameraData.cameraType;
            // 材质球预览、反射探针烘焙用的相机不需要全屏效果
            if (camType == CameraType.Preview || camType == CameraType.Reflection) return;

            m_Pass.renderPassEvent = settings.passEvent;
            m_Pass.Setup(settings);
            renderer.EnqueuePass(m_Pass);
        }

        // 相机的颜色/深度目标在这里才已分配好;在 AddRenderPasses 里取会报错
        public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData)
        {
            m_Pass.SetTarget(renderer.cameraColorTargetHandle, renderer.cameraDepthTargetHandle);
        }

        // Renderer 被销毁(切换管线资产、改 Renderer 列表、退出)时调用
        protected override void Dispose(bool disposing)
        {
            m_Pass?.Dispose();
            CoreUtils.Destroy(m_Material);
        }
    }

    class TintPass : ScriptableRenderPass
    {
        static readonly int s_TintColor = Shader.PropertyToID("_TintColor");
        static readonly int s_Invert = Shader.PropertyToID("_Invert");
        static readonly int s_Desaturate = Shader.PropertyToID("_Desaturate");

        readonly Material m_Material;
        RTHandle m_CameraColor, m_CameraDepth;   // 借来的,不归我们释放
        RTHandle m_Temp;                         // 自己分配的,Dispose 里释放

        public TintPass(Material material)
        {
            m_Material = material;
            profilingSampler = new ProfilingSampler("Ch25 Tint");   // Frame Debugger 里显示的名字
        }

        public void Setup(TintFeature.Settings s)
        {
            // 这些值在一帧内不变,直接写材质即可
            m_Material.SetColor(s_TintColor, s.tint);
            m_Material.SetFloat(s_Invert, s.invert);
            m_Material.SetFloat(s_Desaturate, s.desaturate);
        }

        public void SetTarget(RTHandle color, RTHandle depth)
        {
            m_CameraColor = color;
            m_CameraDepth = depth;
        }

        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
        {
            // 临时 RT 和相机颜色同尺寸、同格式(HDR 时是浮点格式),但不要 MSAA、不要深度
            var desc = renderingData.cameraData.cameraTargetDescriptor;
            desc.msaaSamples = 1;
            desc.depthBufferBits = 0;
            RenderingUtils.ReAllocateIfNeeded(ref m_Temp, desc, FilterMode.Bilinear,
                TextureWrapMode.Clamp, name: "_Ch25TintTemp");
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            var cmd = CommandBufferPool.Get();
            using (new ProfilingScope(cmd, profilingSampler))
            {
                // 不能边读边写同一张纹理:先“相机颜色 → 临时 RT(经过材质)”,再拷回
                Blitter.BlitCameraTexture(cmd, m_CameraColor, m_Temp, m_Material, 0);
                Blitter.BlitCameraTexture(cmd, m_Temp, m_CameraColor);
                // BlitCameraTexture 只绑定了颜色。URP 以为相机的颜色+深度还绑着,
                // 后面的天空盒/透明物体会直接画,所以由我们把深度一起恢复回去。
                CoreUtils.SetRenderTarget(cmd, m_CameraColor, m_CameraDepth);
            }
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }

        public void Dispose()
        {
            m_Temp?.Release();
            m_Temp = null;
        }
    }
}