GameTech/URP 渲染实战
源码 / Ch28/SimpleSSAOFeature.cs
Ch28/SimpleSSAOFeature.cs
// 案例 28.4:简化版 SSAO 的 Renderer Feature。
// 两种接入方式:
//   IntoLighting      —— 像 URP 自带 SSAO 一样,在不透明物体之前算好 AO,作为 _ScreenSpaceOcclusionTexture
//                        交给 Lit 等着色器,只压暗间接光(以及少量直接光);
//   MultiplyAfterOpaque —— 不透明物体画完后直接把 AO 乘到颜色上(对应 URP SSAO 的 After Opaque 选项),
//                        便宜、对自定义 Shader 也生效,但直接光也会被压暗。
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace Cookbook.Ch28
{
    public class SimpleSSAOFeature : ScriptableRendererFeature
    {
        public enum Mode { IntoLighting, MultiplyAfterOpaque, ShowAO }

        [System.Serializable]
        public class Settings
        {
            public Material material;            // Cookbook/Ch28/SimpleSSAO
            public Mode mode = Mode.IntoLighting;
            public bool blur = true;
            public bool halfResolution = false;
            [Range(0, 1)] public float directLightingStrength = 0.25f;
        }
        public Settings settings = new Settings();

        SSAOPass m_Pass;

        public override void Create() { m_Pass = new SSAOPass(); }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            var camType = renderingData.cameraData.cameraType;
            if (settings.material == null || camType == CameraType.Preview || camType == CameraType.Reflection)
                return;
            // 进光照:必须在不透明物体之前,且要等深度/法线预通道画完(+1 与 URP 自带 SSAO 一致)
            m_Pass.renderPassEvent = settings.mode == Mode.IntoLighting
                ? RenderPassEvent.AfterRenderingPrePasses + 1
                : RenderPassEvent.AfterRenderingOpaques;
            // 要法线:Forward 路径下这会让 URP 执行 DepthNormals 预通道
            m_Pass.ConfigureInput(ScriptableRenderPassInput.Depth | ScriptableRenderPassInput.Normal);
            m_Pass.Setup(settings);
            renderer.EnqueuePass(m_Pass);
        }

        public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData)
        {
            m_Pass.SetCameraColor(renderer.cameraColorTargetHandle);
        }

        protected override void Dispose(bool disposing) { m_Pass?.Dispose(); }

        class SSAOPass : ScriptableRenderPass
        {
            static readonly int s_TexelSize = Shader.PropertyToID("_SSAOTexelSize");
            static readonly int s_Result = Shader.PropertyToID("_SSAOResult");
            static readonly int s_URPOcclusionTex = Shader.PropertyToID("_ScreenSpaceOcclusionTexture");
            static readonly int s_URPOcclusionParam = Shader.PropertyToID("_AmbientOcclusionParam");
            const string k_URPKeyword = "_SCREEN_SPACE_OCCLUSION";   // ShaderKeywordStrings.ScreenSpaceOcclusion

            Settings m_Settings;
            RTHandle m_CameraColor, m_AO, m_AOTemp, m_ColorCopy;

            public SSAOPass() { profilingSampler = new ProfilingSampler("SimpleSSAO"); }
            public void Setup(Settings s) { m_Settings = s; }
            public void SetCameraColor(RTHandle h) { m_CameraColor = h; }

            public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
            {
                var desc = renderingData.cameraData.cameraTargetDescriptor;
                desc.msaaSamples = 1;
                desc.depthBufferBits = 0;
                var aoDesc = desc;
                if (m_Settings.halfResolution) { aoDesc.width /= 2; aoDesc.height /= 2; }
                aoDesc.colorFormat = RenderTextureFormat.ARGB32;   // AO 只要 8 位精度
                RenderingUtils.ReAllocateIfNeeded(ref m_AO, aoDesc, FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_SimpleSSAO");
                RenderingUtils.ReAllocateIfNeeded(ref m_AOTemp, aoDesc, FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_SimpleSSAOTemp");
                if (m_Settings.mode != Mode.IntoLighting)
                    RenderingUtils.ReAllocateIfNeeded(ref m_ColorCopy, desc, FilterMode.Point, TextureWrapMode.Clamp, name: "_SimpleSSAOColor");
                ConfigureTarget(m_AO);
            }

            public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
            {
                var mat = m_Settings.material;
                var cmd = CommandBufferPool.Get();
                using (new ProfilingScope(cmd, profilingSampler))
                {
                    int w = m_AO.rt.width, h = m_AO.rt.height;
                    mat.SetVector(s_TexelSize, new Vector4(1f / w, 1f / h, w, h));

                    // 1) AO:没有输入纹理,直接画全屏三角形到 m_AO
                    CoreUtils.SetRenderTarget(cmd, m_AO);
                    Blitter.BlitTexture(cmd, new Vector4(1, 1, 0, 0), mat, 0);
                    // 2) 可分离双边模糊:AO → Temp(横)→ AO(竖)
                    if (m_Settings.blur)
                    {
                        Blitter.BlitCameraTexture(cmd, m_AO, m_AOTemp, mat, 1);
                        Blitter.BlitCameraTexture(cmd, m_AOTemp, m_AO, mat, 2);
                    }

                    if (m_Settings.mode == Mode.IntoLighting)
                    {
                        // 3a) 按 URP 的约定把结果交给光照:纹理 + 关键字 + 参数(w = 直接光受 AO 影响的比例)
                        cmd.SetGlobalTexture(s_URPOcclusionTex, m_AO);
                        cmd.SetGlobalVector(s_URPOcclusionParam, new Vector4(1, 0, 0, m_Settings.directLightingStrength));
                        CoreUtils.SetKeyword(cmd, k_URPKeyword, true);
                    }
                    else
                    {
                        // 3b) 直接乘到相机颜色上(或只显示 AO)
                        cmd.SetGlobalTexture(s_Result, m_AO);
                        Blitter.BlitCameraTexture(cmd, m_CameraColor, m_ColorCopy);
                        Blitter.BlitCameraTexture(cmd, m_ColorCopy, m_CameraColor, mat, m_Settings.mode == Mode.ShowAO ? 4 : 3);
                    }
                }
                context.ExecuteCommandBuffer(cmd);
                CommandBufferPool.Release(cmd);
            }

            public override void OnCameraCleanup(CommandBuffer cmd)
            {
                // 关键字是全局状态,用完一定要关,否则会“漏”到下一台相机
                if (m_Settings != null && m_Settings.mode == Mode.IntoLighting)
                    CoreUtils.SetKeyword(cmd, k_URPKeyword, false);
            }

            public void Dispose()
            {
                m_AO?.Release(); m_AOTemp?.Release(); m_ColorCopy?.Release();
            }
        }
    }
}