在移动平台和复杂场景渲染中Overdraw 是性能杀手之一。当多个不透明物体在屏幕空间重叠时GPU 会为每个像素执行多次片元着色器造成严重的计算浪费。本文将深入探讨 Unity Universal Render Pipeline (URP) 中 Early-Z 和 Depth Prepass 技术的原理、实现方式及最佳实践。一、Overdraw 问题解析1.1 什么是 OverdrawOverdraw 指的是在渲染过程中同一个像素被多次绘制的情况。在不透明物体渲染中只有最靠近相机的像素最终可见但传统的从前向后渲染顺序会导致大量被遮挡的像素仍然执行了完整的片元着色器计算。1.2 Overdraw 的性能代价当片元着色器包含复杂计算时Overdraw 的影响被显著放大复杂光照计算PBR 材质的多层 BRDF 计算、实时阴影采样纹理采样多重纹理采样、三线性过滤、各向异性过滤程序化效果视差映射、曲面细分、程序化噪声生成后处理效果边缘检测、模糊、颜色校正⚠️ 性能警告在移动端 GPU 上Overdraw 是电池消耗和发热的主要原因。一个被遮挡的像素如果执行了 100 条指令的片元着色器就意味着这 100 条指令完全浪费了。二、Early-Z 技术原理2.1 传统深度测试 vs Early-Z传统的深度测试发生在片元着色器执行之后这意味着即使一个片元最终会被深度测试丢弃它仍然执行了完整的着色器计算。2.2 Early-Z 的工作原理Early-Z早期深度测试是 GPU 硬件的一项优化特性它允许在片元着色器执行之前进行深度测试。其工作流程如下1深度缓冲区预填充首先执行一次 Depth Prepass仅写入深度值到 Z-Buffer不进行颜色输出↓2Early-Z 测试在片元着色器之前GPU 使用已填充的深度缓冲区进行深度比较↓3片元着色器执行只有深度测试通过的片元才会执行复杂的片元着色器计算↓4颜色写入最终可见片元的颜色被写入帧缓冲区三、URP 中的 Depth Prepass 实现3.1 URP 渲染管线架构Unity URP 使用 Scriptable Render Pipeline (SRP) 架构允许开发者自定义渲染流程。Depth Prepass 是 URP 内置支持的重要渲染阶段。3.2 启用 Depth Prepass在 URP 中启用 Depth Prepass 非常简单可以通过 Universal Renderer Data 进行配置// 在 Project 窗口中选择 Universal Renderer Data 资源 // 路径通常为: Assets/Settings/URP-HighFidelity-Renderer.asset Rendering Features: ├─ Depth Priming Mode: Auto | Forced | Disabled │ ├─ Depth Texture: ☑ Enabled │ └─ 这将生成 _CameraDepthTexture 供着色器使用 │ └─ Opaque Layer Mask: ☑ Everything └─ 选择哪些层参与 Depth Prepass3.3 着色器中的 Depth Prepass Pass为了让材质参与 Depth Prepass需要在 Shader 中定义 DepthOnly Pass。URP 的 ShaderLab 语法如下Shader Custom/ComplexPBRShader { Properties { _BaseMap(Base Map, 2D) white {} _BaseColor(Base Color, Color) (1, 1, 1, 1) _NormalMap(Normal Map, 2D) bump {} _Metallic(Metallic, Range(0, 1)) 0 _Roughness(Roughness, Range(0, 1)) 0.5 _ParallaxScale(Parallax Scale, Range(0, 0.1)) 0.02 } SubShader { Tags { RenderType Opaque RenderPipeline UniversalPipeline } // // Pass 1: Depth Only - 用于 Depth Prepass // Pass { Name DepthOnly Tags { LightMode DepthOnly } ZWrite On ColorMask 0 Cull[_Cull] HLSLPROGRAM #pragma vertex DepthOnlyVertex #pragma fragment DepthOnlyFragment #pragma multi_compile_instancing #pragma multi_compile _ DOTS_INSTANCING_ON #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); // 深度预渲染顶点着色器 - 保持简单高效 Varyings DepthOnlyVertex(Attributes input) { Varyings output; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); // 仅进行基本的顶点变换 output.positionCS TransformObjectToHClip(input.positionOS.xyz); output.uv input.uv; return output; } // 深度预渲染片元着色器 - 可选 Alpha 测试 half4 DepthOnlyFragment(Varyings input) : SV_TARGET { UNITY_SETUP_INSTANCE_ID(input); UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); // 如果需要 Alpha Clip在这里进行 half4 baseColor SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); #if defined(_ALPHATEST_ON) clip(baseColor.a - _Cutoff); #endif return 0; } ENDHLSL } // // Pass 2: Forward Lit - 复杂片元着色器 // Pass { Name ForwardLit Tags { LightMode UniversalForward } ZWrite On ZTest LEqual Cull[_Cull] HLSLPROGRAM #pragma vertex LitPassVertex #pragma fragment LitPassFragment // 启用 GPU 的 Early-Z 优化 #pragma require earlydepthstencil #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl // ... 复杂的片元着色器代码 ... // 包括: PBR 计算、视差映射、多重纹理采样等 ENDHLSL } } }四、Shader Graph 中的配置4.1 使用 Shader Graph 创建支持 Depth Prepass 的着色器对于使用 Shader Graph 的开发者URP 会自动处理 Depth Prepass。但需要确保以下设置正确Graph Settings: ├─ Shader Graph Target: │ └─ Universal ✓ │ ├─ Universal Render Pipeline: │ ├─ Material: Opaque // 或 Transparent │ ├─ Workflow Mode: Metallic | Specular │ └─ Surface Type: Opaque // 不透明物体才能受益于 Early-Z │ └─ Active Targets: └─ Universal Render Pipeline ✓ // Shader Graph 会自动生成 DepthOnly Pass // 无需手动编写4.2 自定义 Shader Graph 的 Depth Pass如果需要自定义深度预渲染行为如使用顶点动画可以创建自定义的 Shader Graph 子着色器#ifndef CUSTOM_DEPTH_PASS_INCLUDED #define CUSTOM_DEPTH_PASS_INCLUDED #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl float4 _VertexAnimationParams; // 自定义顶点动画函数 - 确保深度预渲染使用相同的顶点位置 float3 ApplyVertexAnimation(float3 positionOS, float2 uv) { float time _Time.y * _VertexAnimationParams.x; float wave sin(positionOS.x * _VertexAnimationParams.y time); float wave2 cos(positionOS.z * _VertexAnimationParams.z time); positionOS.y wave * _VertexAnimationParams.w; positionOS.y wave2 * _VertexAnimationParams.w * 0.5; return positionOS; } // 顶点着色器 - 用于 DepthOnly 和 ForwardLit Pass VertexPositionInputs GetCustomVertexPositionInputs(float3 positionOS, float2 uv) { // 应用与主渲染相同的顶点动画 float3 animatedPosition ApplyVertexAnimation(positionOS, uv); VertexPositionInputs posInputs GetVertexPositionInputs(animatedPosition); return posInputs; } #endif // CUSTOM_DEPTH_PASS_INCLUDED五、性能分析与优化策略5.1 何时使用 Depth PrepassDepth Prepass 并非总是带来性能提升需要根据场景特点进行权衡场景特征建议原因复杂片元着色器 高 Overdraw启用 Depth PrepassEarly-Z 能显著减少片元着色器调用简单片元着色器禁用 Depth Prepass额外的 Draw Call 开销可能超过收益大量 Alpha Test 物体启用 Depth PrepassAlpha Test 破坏 Early-Z需要 Prepass前向渲染 多光源视情况启用复杂光照计算可从 Early-Z 受益延迟渲染不需要G-Buffer 阶段天然处理深度5.2 性能对比数据以下是在典型移动设备中端 GPU上的性能测试结果 优化建议使用 Unity Frame Debugger 分析实际渲染流程确认 Depth Prepass 是否生效。检查 DepthOnly Pass 是否在 ForwardLit Pass 之前执行。5.3 常见陷阱与解决方案⚠️ 陷阱 1片元着色器中的 discard/clip 操作在片元着色器中使用discard或clip()会禁用 Early-Z 优化因为 GPU 无法提前知道哪些片元会被丢弃。解决方案将 Alpha Test 逻辑移到 Depth Prepass 阶段主渲染 Pass 使用 ZTest Equal 进行精确深度测试。⚠️ 陷阱 2写入深度与颜色不一致如果 Depth Prepass 和 Forward Pass 的顶点位置计算不一致如顶点动画不同步会导致深度冲突Z-fighting或剔除错误。解决方案确保两个 Pass 使用完全相同的顶点变换逻辑将共享代码提取到单独的 HLSL 文件。⚠️ 陷阱 3透明物体的 Depth Prepass透明物体通常不写入深度缓冲区因此无法直接受益于 Early-Z。但可以为透明物体单独启用 Depth Prepass 来优化排序。解决方案对透明物体使用 Depth Prepass for Transparent 技术先写入深度但不写入颜色然后在透明渲染阶段利用深度信息进行排序优化。六、高级技巧与最佳实践6.1 动态切换 Depth Prepass根据运行时条件动态启用/禁用 Depth Prepassusing UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class DepthPrepassController : MonoBehaviour { [SerializeField] private UniversalRendererData rendererData; [SerializeField] private float complexityThreshold 50f; private ScriptableRendererFeature depthPrepassFeature; void Start() { // 获取 Depth Prepass 渲染特性 foreach (var feature in rendererData.rendererFeatures) { if (feature is RenderObjects renderObjects) { depthPrepassFeature feature; break; } } } void Update() { // 根据场景复杂度动态切换 float sceneComplexity CalculateSceneComplexity(); bool shouldEnable sceneComplexity complexityThreshold; if (depthPrepassFeature ! null depthPrepassFeature.isActive ! shouldEnable) { depthPrepassFeature.SetActive(shouldEnable); Debug.Log($Depth Prepass {(shouldEnable ? Enabled : Disabled)} - Complexity: {sceneComplexity:F1}); } } private float CalculateSceneComplexity() { // 基于可见物体数量、重叠程度等计算场景复杂度 var renderers FindObjectsOfTypeRenderer(); float complexity renderers.Length; // 可以添加更多复杂的计算逻辑... return complexity; } }6.2 自定义 Render Feature 实现高级 Depth Prepass对于需要精细控制的项目可以实现自定义的 Render Featureusing UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class CustomDepthPrepassFeature : ScriptableRendererFeature { [System.Serializable] public class CustomDepthPrepassSettings { public LayerMask layerMask ~0; public bool enableAlphaTest true; public float alphaThreshold 0.5f; } public CustomDepthPrepassSettings settings new CustomDepthPrepassSettings(); private CustomDepthPrepassPass depthPrepassPass; public override void Create() { depthPrepassPass new CustomDepthPrepassPass(settings); depthPrepassPass.renderPassEvent RenderPassEvent.AfterRenderingPrePasses; } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { renderer.EnqueuePass(depthPrepassPass); } class CustomDepthPrepassPass : ScriptableRenderPass { private CustomDepthPrepassSettings settings; private FilteringSettings filteringSettings; private ShaderTagId shaderTagId new ShaderTagId(DepthOnly); public CustomDepthPrepassPass(CustomDepthPrepassSettings settings) { this.settings settings; filteringSettings new FilteringSettings(RenderQueueRange.opaque, settings.layerMask); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { SortingCriteria sortingCriteria renderingData.cameraData.defaultOpaqueSortFlags; DrawingSettings drawingSettings new DrawingSettings(shaderTagId, sortingCriteria); // 配置深度写入状态 var depthState new DepthState(true, CompareFunction.LessEqual); // 执行深度预渲染 context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref filteringSettings); } } }七、总结Early-Z 和 Depth Prepass 是优化复杂片元着色器性能的关键技术。在 Unity URP 中通过合理配置和着色器编写可以显著减少 Overdraw 带来的性能损失。关键要点回顾Overdraw 是性能杀手- 复杂片元着色器在重叠区域会被多次执行Early-Z 提前剔除- 在片元着色器之前进行深度测试避免无效计算Depth Prepass 准备深度- 预先填充深度缓冲区为 Early-Z 提供数据着色器需要 DepthOnly Pass- 确保材质支持深度预渲染权衡使用场景- 简单场景可能不需要复杂场景收益显著避免破坏 Early-Z- 注意 discard/clip 操作和顶点一致性