Unity URP 下的 Early-Z / Depth Prepass 解决复杂片元着色器造成的 Overdraw 问题

在移动平台和复杂场景渲染中,Overdraw 是性能杀手之一。当多个不透明物体在屏幕空间重叠时,GPU 会为每个像素执行多次片元着色器,造成严重的计算浪费。本文将深入探讨 Unity Universal Render Pipeline (URP) 中 Early-Z 和 Depth Prepass 技术的原理、实现方式及最佳实践。

一、Overdraw 问题解析

1.1 什么是 Overdraw

Overdraw 指的是在渲染过程中,同一个像素被多次绘制的情况。在不透明物体渲染中,只有最靠近相机的像素最终可见,但传统的从前向后渲染顺序会导致大量被遮挡的像素仍然执行了完整的片元着色器计算。

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,不进行颜色输出

2

Early-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 进行配置:

cs 复制代码
// 在 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 Prepass

3.3 着色器中的 Depth Prepass Pass

为了让材质参与 Depth Prepass,需要在 Shader 中定义 DepthOnly Pass。URP 的 ShaderLab 语法如下:

cs 复制代码
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。但需要确保以下设置正确:

cs 复制代码
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 子着色器:

cs 复制代码
#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 Prepass

Depth Prepass 并非总是带来性能提升,需要根据场景特点进行权衡:

场景特征 建议 原因
复杂片元着色器 + 高 Overdraw 启用 Depth Prepass Early-Z 能显著减少片元着色器调用
简单片元着色器 禁用 Depth Prepass 额外的 Draw Call 开销可能超过收益
大量 Alpha Test 物体 启用 Depth Prepass Alpha 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 操作

在片元着色器中使用 discardclip() 会禁用 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 Prepass:

cs 复制代码
using 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 = FindObjectsOfType<Renderer>();
        float complexity = renderers.Length;
        
        // 可以添加更多复杂的计算逻辑...
        
        return complexity;
    }
}

6.2 自定义 Render Feature 实现高级 Depth Prepass

对于需要精细控制的项目,可以实现自定义的 Render Feature:

cs 复制代码
using 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 带来的性能损失。

关键要点回顾:

  1. Overdraw 是性能杀手 - 复杂片元着色器在重叠区域会被多次执行
  2. Early-Z 提前剔除 - 在片元着色器之前进行深度测试,避免无效计算
  3. Depth Prepass 准备深度 - 预先填充深度缓冲区,为 Early-Z 提供数据
  4. 着色器需要 DepthOnly Pass - 确保材质支持深度预渲染
  5. 权衡使用场景 - 简单场景可能不需要,复杂场景收益显著
  6. 避免破坏 Early-Z - 注意 discard/clip 操作和顶点一致性
相关推荐
mxwin2 小时前
Unity Shader 顶点色:利用模型顶点颜色传递渲染数据
unity·游戏引擎·shader
星夜泊客4 小时前
Unity 排行榜 UI 优化:从全量生成到滚动复用
ui·unity·性能优化·游戏引擎
CDN3604 小时前
游戏盾导致 Unity/UE 引擎崩溃?内存占用、SO 库冲突深度排查
游戏·unity·游戏引擎
心前阳光4 小时前
Unity之Luban使用流程
unity·游戏引擎
mxwin5 小时前
Unity URP 下的 GPU Instancing减少 DrawCall 的关键技术
unity·游戏引擎·shader
小贺儿开发5 小时前
Unity3D LED点阵屏幕模拟
http·unity·浏览器·网络通信·led·互动·点阵屏
RReality7 小时前
【Unity Shader】 溶解效果实战教程
unity·游戏引擎
mxwin7 小时前
Unity URP SRP Batcher 完全指南 URP/HDRP 下的核心批处理机制,大幅降低 CPU 开销
unity·游戏引擎·shader·单一职责原则
小清兔7 小时前
unity中的音频相关_笔记
笔记·unity·音视频