第13章 ScriptableRenderPass深入
理论讲解
13.1 ScriptableRenderPass生命周期
ScriptableRenderPass是URP中最小的渲染单元,它定义了渲染管线中的一个特定渲染步骤。理解其生命周期对于正确实现自定义渲染通道至关重要。
ScriptableRenderPass的完整生命周期包括:
- 创建阶段:通过构造函数创建渲染通道实例
- 配置阶段:在Configure方法中设置渲染目标和渲染状态
- 执行阶段:在Execute方法中执行实际的渲染命令
- 清理阶段:在FrameCleanup方法中释放资源
生命周期方法详解:
构造函数:初始化渲染通道的基本参数,如执行事件、渲染目标等。
Configure:配置渲染通道的渲染目标和初始状态。这个方法在每帧渲染前被调用,用于设置渲染目标和清除状态。
Execute:执行渲染通道的核心逻辑。这是渲染通道的主要工作方法,包含实际的渲染命令。
FrameCleanup:清理帧级别的资源。在每帧渲染完成后调用,用于释放临时资源。
Configure方法是渲染通道配置的核心,它决定了渲染通道的渲染目标和初始状态。
1 2 3 4 5 6 7
| public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor) { ConfigureTarget(renderTextureIdentifier); ConfigureClear(ClearFlag.All, Color.black); }
|
Configure方法的参数:
- cmd:用于记录配置命令的CommandBuffer
- cameraTextureDescriptor:相机纹理描述符,包含分辨率、格式等信息
Execute()方法
Execute方法是渲染通道的核心执行逻辑,它使用ScriptableRenderContext执行实际的渲染命令。
1 2 3 4 5
| public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref filteringSettings); }
|
Execute方法的参数:
- context:ScriptableRenderContext,用于执行渲染命令
- renderingData:RenderingData,包含当前帧的渲染数据
FrameCleanup()方法
FrameCleanup方法用于清理帧级别的资源,确保没有资源泄漏。
1 2 3 4
| public override void FrameCleanup(ref RenderingData renderingData) { }
|
13.3 RenderPassEvent枚举详解
RenderPassEvent定义了渲染通道在渲染流程中的执行时机,这是控制渲染顺序的关键。
主要的RenderPassEvent值:
BeforeRendering:在所有渲染之前执行
- BeforeRenderingShadows:在阴影渲染之前
- BeforeRenderingPrePasses:在预通道渲染之前
- BeforeRenderingOpaques:在不透明物体渲染之前
- BeforeRenderingTransparents:在透明物体渲染之前
- BeforeRenderingPostProcessing:在后处理渲染之前
AfterRendering:在所有渲染之后执行
- AfterRenderingOpaques:在不透明物体渲染之后
- AfterRenderingTransparents:在透明物体渲染之后
- AfterRenderingPostProcessing:在后处理渲染之后
- AfterRendering:在所有渲染之后
RenderPassEvent的使用原则:
- 依赖关系:确保依赖的渲染通道在当前通道之前执行
- 性能考虑:将计算密集型通道安排在合适的位置
- 资源管理:确保所需资源在通道执行前已准备就绪
13.4 RTHandle资源管理
RTHandle是URP中用于管理渲染纹理资源的类型安全句柄,它提供了自动资源管理和生命周期控制。
RTHandle的优势:
- 自动管理:自动处理渲染纹理的创建和释放
- 类型安全:编译时检查,避免运行时错误
- 性能优化:减少不必要的资源分配和释放
RTHandle的使用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private RTHandle m_CameraColorTarget; private RTHandle m_CameraDepthTarget;
m_CameraColorTarget = RTHandles.Alloc(1280, 720, colorFormat: GraphicsFormat.R8G8B8A8_SRGB); m_CameraDepthTarget = RTHandles.Alloc(1280, 720, depthBufferBits: DepthBits.Depth24);
ConfigureTarget(m_CameraColorTarget); ConfigureClear(ClearFlag.All, Color.black);
m_CameraColorTarget?.Release(); m_CameraDepthTarget?.Release();
|
13.5 Pass之间的数据传递
在多Pass渲染中,Pass之间的数据传递是实现复杂渲染效果的关键。
数据传递方式:
- 渲染纹理:通过共享的渲染纹理在Pass间传递数据
- 常量缓冲区:通过Shader属性传递参数
- 渲染状态:通过渲染状态传递配置信息
实现数据传递的步骤:
- 创建共享资源:创建Pass间共享的渲染纹理
- 配置数据源:在输出Pass中写入数据到共享资源
- 配置数据目标:在输入Pass中从共享资源读取数据
13.6 自定义RenderPass完整实现
自定义RenderPass的实现需要遵循特定的模式,确保正确集成到URP渲染流程中。
自定义RenderPass的实现步骤:
- 继承ScriptableRenderPass:创建自定义渲染通道类
- 实现构造函数:初始化通道参数
- 重写Configure方法:配置渲染目标
- 重写Execute方法:实现渲染逻辑
- 重写FrameCleanup方法:清理资源
- 集成到渲染器:将自定义通道添加到渲染器
代码示例
13.7 自定义深度渲染通道
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal;
public class CustomDepthPass : ScriptableRenderPass { private FilteringSettings m_FilteringSettings; private RenderTargetHandle m_DepthTextureHandle; private List<ShaderTagId> m_ShaderTagIdList = new List<ShaderTagId>(); private Material m_DepthMaterial; private ProfilingSampler m_ProfilingSampler; public CustomDepthPass(RenderPassEvent renderPassEvent, Material depthMaterial) { this.renderPassEvent = renderPassEvent; m_DepthMaterial = depthMaterial; m_ProfilingSampler = new ProfilingSampler("Custom Depth Pass"); m_FilteringSettings = new FilteringSettings(RenderQueueRange.all); m_ShaderTagIdList.Add(new ShaderTagId("SRPDefaultUnlit")); m_ShaderTagIdList.Add(new ShaderTagId("UniversalForward")); m_ShaderTagIdList.Add(new ShaderTagId("LightweightForward")); } public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor) { var depthDescriptor = cameraTextureDescriptor; depthDescriptor.colorFormat = RenderTextureFormat.Depth; depthDescriptor.depthBufferBits = 32; ConfigureTarget(m_DepthTextureHandle.Identifier()); ConfigureClear(ClearFlag.Depth, Color.black); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (m_DepthMaterial == null) { Debug.LogError("Depth material is null in CustomDepthPass"); return; } var cmd = CommandBufferPool.Get(); using (new ProfilingScope(cmd, m_ProfilingSampler)) { var drawingSettings = CreateDrawingSettings(m_ShaderTagIdList, ref renderingData, SortingCriteria.CommonOpaque); drawingSettings.overrideMaterial = m_DepthMaterial; context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref m_FilteringSettings); } context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } public override void FrameCleanup(ref RenderingData renderingData) { } public void SetupDepthTextureHandle(RenderTargetHandle depthHandle) { m_DepthTextureHandle = depthHandle; } }
|
13.8 自定义后处理渲染通道
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal;
public class CustomPostProcessPass : ScriptableRenderPass { private ProfilingSampler m_ProfilingSampler; private Material m_PostProcessMaterial; private int m_SourceTextureId; private int m_DestinationTextureId; private string m_KernelName; public CustomPostProcessPass(RenderPassEvent renderPassEvent, Material postProcessMaterial, string kernelName) { this.renderPassEvent = renderPassEvent; m_PostProcessMaterial = postProcessMaterial; m_KernelName = kernelName; m_ProfilingSampler = new ProfilingSampler("Custom Post Process Pass"); m_SourceTextureId = Shader.PropertyToID("_SourceTexture"); m_DestinationTextureId = Shader.PropertyToID("_DestinationTexture"); } public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor) { ConfigureTarget(BuiltinRenderTextureType.CameraTarget); ConfigureClear(ClearFlag.None, Color.black); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (m_PostProcessMaterial == null) { Debug.LogError("Post process material is null in CustomPostProcessPass"); return; } var cmd = CommandBufferPool.Get(); using (new ProfilingScope(cmd, m_ProfilingSampler)) { var source = renderingData.cameraData.renderer.cameraColorTarget; var destination = RenderTargetHandle.CameraTarget; m_PostProcessMaterial.SetTexture(m_SourceTextureId, source); cmd.Blit(source, destination.Identifier(), m_PostProcessMaterial); } context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } public override void FrameCleanup(ref RenderingData renderingData) { } public void SetFloat(string name, float value) { if (m_PostProcessMaterial != null) { m_PostProcessMaterial.SetFloat(name, value); } } public void SetVector(string name, Vector4 value) { if (m_PostProcessMaterial != null) { m_PostProcessMaterial.SetVector(name, value); } } public void SetTexture(string name, Texture texture) { if (m_PostProcessMaterial != null) { m_PostProcessMaterial.SetTexture(name, texture); } } }
|
13.9 自定义阴影渲染通道
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal;
public class CustomShadowPass : ScriptableRenderPass { private FilteringSettings m_FilteringSettings; private List<ShaderTagId> m_ShaderTagIdList = new List<ShaderTagId>(); private Material m_ShadowCasterMaterial; private ProfilingSampler m_ProfilingSampler; private int m_MaxShadowCasters = 16; public CustomShadowPass(RenderPassEvent renderPassEvent, Material shadowCasterMaterial) { this.renderPassEvent = renderPassEvent; m_ShadowCasterMaterial = shadowCasterMaterial; m_ProfilingSampler = new ProfilingSampler("Custom Shadow Pass"); m_FilteringSettings = new FilteringSettings(RenderQueueRange.all); m_ShaderTagIdList.Add(new ShaderTagId("ShadowCaster")); m_ShaderTagIdList.Add(new ShaderTagId("UniversalForward")); m_ShaderTagIdList.Add(new ShaderTagId("LightweightForward")); } public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor) { } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (m_ShadowCasterMaterial == null) { Debug.LogError("Shadow caster material is null in CustomShadowPass"); return; } var cmd = CommandBufferPool.Get(); using (new ProfilingScope(cmd, m_ProfilingSampler)) { var shadowDrawingSettings = new ShadowDrawingSettings(renderingData.cullResults, 0); shadowDrawingSettings.SetOverrideMaterial(m_ShadowCasterMaterial, 0); context.DrawShadows(ref shadowDrawingSettings); } context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } public override void FrameCleanup(ref RenderingData renderingData) { } public void SetShadowBias(float bias, float normalBias) { if (m_ShadowCasterMaterial != null) { m_ShadowCasterMaterial.SetFloat("_ShadowBias", bias); m_ShadowCasterMaterial.SetFloat("_NormalBias", normalBias); } } }
|
13.10 RenderPass管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
| using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal;
public class RenderPassManager : MonoBehaviour { [System.Serializable] public class RenderPassConfiguration { public string name; public RenderPassEvent passEvent; public bool isEnabled = true; public Material material; public string kernelName; public float executionPriority = 0f; } [Header("Render Pass Configurations")] public List<RenderPassConfiguration> passConfigurations = new List<RenderPassConfiguration>(); [Header("Runtime Settings")] public bool enablePassManagement = true; public bool logPassExecution = false; private List<ScriptableRenderPass> m_RenderPasses = new List<ScriptableRenderPass>(); private Dictionary<string, ScriptableRenderPass> m_PassMap = new Dictionary<string, ScriptableRenderPass>(); private List<float> m_PassExecutionTimes = new List<float>(); void Start() { if (enablePassManagement) { InitializeRenderPasses(); } } private void InitializeRenderPasses() { m_RenderPasses.Clear(); m_PassMap.Clear(); passConfigurations.Sort((a, b) => a.executionPriority.CompareTo(b.executionPriority)); foreach (var config in passConfigurations) { if (config.isEnabled) { var renderPass = CreateRenderPass(config); if (renderPass != null) { m_RenderPasses.Add(renderPass); m_PassMap[config.name] = renderPass; if (logPassExecution) { Debug.Log($"[RenderPassManager] Created pass: {config.name} at event: {config.passEvent}"); } } } } } private ScriptableRenderPass CreateRenderPass(RenderPassConfiguration config) { switch (config.name.ToLower()) { case "customdepth": return new CustomDepthPass(config.passEvent, config.material); case "custompostprocess": return new CustomPostProcessPass(config.passEvent, config.material, config.kernelName); case "customshadow": return new CustomShadowPass(config.passEvent, config.material); default: Debug.LogWarning($"[RenderPassManager] Unknown pass type: {config.name}"); return null; } } public void AddRenderPass(string name, ScriptableRenderPass pass, RenderPassEvent passEvent) { if (pass != null && !m_PassMap.ContainsKey(name)) { pass.renderPassEvent = passEvent; m_RenderPasses.Add(pass); m_PassMap[name] = pass; if (logPassExecution) { Debug.Log($"[RenderPassManager] Added custom pass: {name}"); } } } public bool RemoveRenderPass(string name) { if (m_PassMap.ContainsKey(name)) { var pass = m_PassMap[name]; m_RenderPasses.Remove(pass); m_PassMap.Remove(name); if (logPassExecution) { Debug.Log($"[RenderPassManager] Removed pass: {name}"); } return true; } return false; } public bool SetPassEnabled(string name, bool enabled) { if (m_PassMap.ContainsKey(name)) { var pass = m_PassMap[name]; if (enabled && !m_RenderPasses.Contains(pass)) { m_RenderPasses.Add(pass); } else if (!enabled && m_RenderPasses.Contains(pass)) { m_RenderPasses.Remove(pass); } return true; } return false; } public ScriptableRenderPass GetRenderPass(string name) { if (m_PassMap.ContainsKey(name)) { return m_PassMap[name]; } return null; } public List<ScriptableRenderPass> GetAllRenderPasses() { return new List<ScriptableRenderPass>(m_RenderPasses); } public void ExecuteRenderPasses(ScriptableRenderContext context, ref RenderingData renderingData) { if (!enablePassManagement) return; m_RenderPasses.Sort((a, b) => a.renderPassEvent.CompareTo(b.renderPassEvent)); for (int i = 0; i < m_RenderPasses.Count; i++) { var pass = m_RenderPasses[i]; if (pass != null) { var startTime = Time.realtimeSinceStartup; pass.Execute(context, ref renderingData); var executionTime = (Time.realtimeSinceStartup - startTime) * 1000f; m_PassExecutionTimes.Add(executionTime); if (logPassExecution) { Debug.Log($"[RenderPassManager] Executed pass: {pass.GetType().Name}, Time: {executionTime:F2}ms"); } } } } public string GetPerformanceStats() { if (m_PassExecutionTimes.Count == 0) return "No pass execution data available"; float totalTime = 0f; float maxTime = 0f; float minTime = float.MaxValue; foreach (var time in m_PassExecutionTimes) { totalTime += time; if (time > maxTime) maxTime = time; if (time < minTime) minTime = time; } float avgTime = totalTime / m_PassExecutionTimes.Count; return $"Render Pass Performance:\n" + $"Total Passes: {m_PassExecutionTimes.Count}\n" + $"Avg Time: {avgTime:F2}ms\n" + $"Max Time: {maxTime:F2}ms\n" + $"Min Time: {minTime:F2}ms\n" + $"Total Time: {totalTime:F2}ms"; } public void ResetPerformanceStats() { m_PassExecutionTimes.Clear(); } public bool ValidatePassConfiguration() { bool isValid = true; List<string> errors = new List<string>(); foreach (var config in passConfigurations) { if (config.isEnabled) { if (config.material == null && RequiresMaterial(config.name)) { errors.Add($"Pass '{config.name}' requires a material but none is assigned"); isValid = false; } } } if (!isValid && logPassExecution) { foreach (var error in errors) { Debug.LogError($"[RenderPassManager] Configuration Error: {error}"); } } return isValid; } private bool RequiresMaterial(string passName) { return passName.ToLower().Contains("postprocess") || passName.ToLower().Contains("depth") || passName.ToLower().Contains("shadow"); } }
|
实践练习
13.11 练习1:创建自定义渲染通道
目标:实现一个简单的自定义渲染通道
步骤:
- 创建继承自ScriptableRenderPass的类
- 实现Configure方法配置渲染目标
- 实现Execute方法执行渲染逻辑
- 实现FrameCleanup方法清理资源
- 将自定义通道集成到渲染器中
实现要求:
- 正确设置RenderPassEvent
- 处理渲染数据
- 管理渲染资源
13.12 练习2:实现深度渲染通道
目标:创建专门的深度渲染通道
步骤:
- 创建CustomDepthPass类
- 配置深度纹理格式
- 实现深度渲染逻辑
- 测试深度纹理生成
- 验证深度精度
技术要点:
13.13 练习3:实现后处理通道
目标:创建自定义后处理渲染通道
步骤:
- 创建CustomPostProcessPass类
- 实现Blit操作
- 配置材质参数
- 测试后处理效果
- 优化性能
实现要素:
13.14 练习4:渲染通道性能测试
目标:测试不同渲染通道的性能
步骤:
- 创建性能监控工具
- 记录各通道执行时间
- 分析性能瓶颈
- 优化通道实现
- 验证优化效果
监控指标:
13.15 练习5:多Pass数据传递
目标:实现Pass间的数据传递
步骤:
- 创建共享渲染纹理
- 在输出Pass中写入数据
- 在输入Pass中读取数据
- 验证数据传递正确性
- 优化数据传递效率
传递方式:
总结
第13章深入探讨了ScriptableRenderPass的实现原理和使用方法。ScriptableRenderPass是URP渲染管线中的基本构建块,通过它可以实现各种自定义渲染效果。
关键要点总结:
- 生命周期管理:理解Configure、Execute、FrameCleanup方法的作用
- 事件系统:掌握RenderPassEvent控制渲染顺序
- 资源管理:使用RTHandle进行类型安全的资源管理
- 数据传递:实现Pass间的高效数据传递
- 性能优化:优化渲染通道的执行效率
ScriptableRenderPass为开发者提供了强大的渲染定制能力,通过合理设计和实现自定义渲染通道,可以实现复杂的渲染效果和优化特定的渲染需求。理解其工作原理对于深入掌握URP渲染管线至关重要。
下一章将探讨RenderingData与Context的概念,了解渲染数据的传递和上下文管理机制。