Unity Shader 深入理解 深度冲突

从深度缓冲的基本原理出发,系统解析 Z-Fighting 的成因,并提供 URP Shader 中的实用解决方案。

一、什么是深度

在 3D 渲染中,深度 描述的是物体表面上的某个点距离摄像机有多远。 想象你站在一条笔直的马路上,近处的路灯比远处的路灯"更浅",这个"深浅"就是深度。

Unity 使用 深度缓冲 来记录屏幕上每个像素的深度值。 它是一张与屏幕分辨率相同的"灰度图"------每个像素存储的不是颜色,而是一个 [0, 1] 范围的值: 0 代表最近(靠近近裁面),1 代表最远(接近远裁面)

关键认知

深度缓冲本质上是一张逐像素的浮点纹理 。URP 中默认使用 _CameraDepthTexture 来访问它,精度通常为 24 位或 32 位。


二、深度测试的工作原理

当 GPU 要绘制一个片元时,它不会直接覆盖屏幕上已有的颜色。它会先做一次深度测试

  1. 读取目标像素当前深度缓冲中的值(已有值)
  2. 用当前片元的深度值与已有值比较
  3. 如果片元更近(默认是小于),则通过测试 → 更新颜色和深度缓冲
  4. 如果片元更远,则丢弃该片元

三、什么是深度冲突

深度冲突,英文叫 Z-Fighting ,是 3D 渲染中最常见的视觉 bug 之一。 当两个(或多个)面在空间中几乎重叠时,深度缓冲的精度不足以区分它们谁在前、谁在后。 GPU 在不同像素上随机判定不同的面"获胜",结果就是两个面的纹理锯齿状地交错闪烁。

典型场景

深度冲突最常见于以下情况:① 两个面恰好重合(如墙壁和贴花);② 远处物体因为深度缓冲的非线性精度而"挤在一起";③ Shadow Map 中自阴影导致的阴影斑块(Shadow Acne)。


四、深度冲突的根本原因

要理解为什么深度缓冲"不够用",需要了解深度值的非线性分布

4.1 透视投影下的深度非线性

经过透视投影矩阵变换后,深度值并不是均匀分布的。 近处的精度极高,远处则迅速降低。这个非线性关系可以表示为:

4.2 浮点精度问题

标准的 24 位深度缓冲有约 1677 万个离散深度级别。这听起来很多,但在非线性分布下:

  • 近裁面 0.1m、远裁面 1000m 时,在距离摄像机 900m 处,相邻两个深度级别之间可能相差 数米
  • 如果两个面在这个距离上间距小于这个级别差,GPU 就无法区分它们
深度缓冲格式 精度 适用范围 备注
D16_UNorm 65536 级 移动端 / 低端 精度最低,远距离易出问题
D24_UNorm_S8_UInt 1677 万级 桌面端常用 带 8 位 Stencil,最通用的选择
D32_Float 约 23 位尾数 高端 / PC 浮点格式,近处精度优于远处
D32_Float_S8_UInt 约 23 位尾数 高端 / 主机 浮点 + Stencil,最佳选择

五、URP 中的解决方案

5.1 调整深度偏移 --- Offset

HLSL/ShaderLab 提供了 Offset 命令,可以在光栅化阶段对深度值施加一个微小的偏移:

cs 复制代码
HLSL / ShaderLab
Pass {
    // Offset Factor, Units
    Offset -1, -1

    // Factor: 按斜率缩放的最大深度偏移
    // Units:  最小可分辨深度值的倍数
}

Offset factor, units 的实际偏移量为 factor × |dZ/dX| + units × r,其中 r 是深度缓冲的最小可分辨值。负值将片元"拉近"摄像机,正数推远。

实用建议

对于贴花类效果,常用 Offset -1, -1 让贴花略微浮在表面上方。对于轮廓描边,常用 Offset -1, -100 以确保描边始终可见。

5.2 调整近远裁面比例

近裁面和远裁面的比例直接决定了深度的均匀程度。增大 Near、减小 Far 可以显著改善远处精度:

cs 复制代码
C# / URP RenderPipelineAsset
// 在 URP Asset 中设置
// 或者通过代码:
var cameraData = renderingData.cameraData;
cameraData.camera.nearClipPlane = 0.3f;   // 不要设置得太小!
cameraData.camera.farClipPlane  = 500f;    // 按场景需要调整
  • Near = 0.01 会使 99% 的深度精度浪费在摄像机前 1 米内
  • 推荐 Near ≥ 0.1(VR 场景 ≥ 0.05),Far 按实际可见距离设置

5.3 使用 Reverse-Z

Reverse-Z 是近年来最重要的深度优化之一。它将深度值反转:近处 = 1,远处 = 0。 这样做的好处是让浮点精度在远处也能保持较高分辨率------因为浮点数在 0, 0.5 区间的精度远高于 0.5, 1

URP 中的 Reverse-Z

URP 从 Unity 2020.3+ 开始默认启用 Reverse-Z。如果你的项目使用旧版 URP,可以在 URP Asset → Quality → 勾选 "Depth Texture" 来确保深度纹理格式正确。

5.4 分层渲染 & Render Queue

有时深度冲突并非精度问题,而是渲染顺序的问题。调整 Render Queue 确保正确的绘制顺序:

cs 复制代码
HLSL / ShaderLab
Tags {
    "Queue" = "Geometry"      // 不透明几何体: 2000
    "Queue" = "AlphaTest"     // Alpha测试:     2450
    "Queue" = "Transparent"  // 透明物体:      3000
    "Queue" = "Overlay"      // 覆盖层:        4000
}

// 自定义偏移:
"Queue" = "Geometry+50"  // 在不透明几何体之后渲染

5.5 使用深度偏移值 --- DepthBias

在某些情况下(如 Shadow Map、自定义深度写入),可以在 Shader 中手动添加深度偏移:

cs 复制代码
HLSL / URP Shader
// 方法一:在顶点着色器中手动偏移
Varyings vert(Attributes input) {
    Varyings output;
    VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);

    // 沿法线方向略微偏移顶点
    float3 normalWS = TransformObjectToWorldNormal(input.normalOS);
    float3 posWS = vertexInput.positionWS + normalWS * 0.001;

    output.positionCS = TransformWorldToHClip(posWS);
    return output;
}
cs 复制代码
HLSL / URP Shader
// 方法二:在片元着色器中修改深度输出
half4 frag(Varyings input, out float outDepth : SV_Depth) : SV_Target {
    half4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);

    // 获取当前深度并略微推远
    outDepth = input.positionCS.z / input.positionCS.w;
    outDepth += 0.00001;  // 小心!过大可能出现孔洞

    return color;
}

⚠ 注意

直接修改 SV_Depth 会禁用 Early-Z 优化,对性能有显著影响。仅在必要时使用,优先选择 Offset 命令或顶点偏移方案。


六、常见深度冲突场景实战

场景一:贴花 (Decal) 闪烁

在墙壁上放置弹孔贴花时,贴花面与墙面几乎共面。解决方案:

  1. 使用 Offset -1, -1 将贴花略微拉近
  2. 或将贴花面沿法线偏移 0.001-0.005 单位
  3. 使用 URP Decal Projector 或 DBuffer Decal(如果已启用)
cs 复制代码
HLSL --- Decal Shader Pass
Pass {
    Name "Decal"
    Tags { "LightMode" = "UniversalForward" }

    ZWrite Off
    ZTest LEqual
    Offset -1, -1      // 核心:轻微拉近贴花

    Blend SrcAlpha OneMinusSrcAlpha
}

场景二:两个平面重叠

当地形与水面、或两层地面材质重叠时:

  1. 增大层间距(最简单的方案)
  2. 使用 ZTest GEqual + ZWrite Off 确保仅在不透明物体之后渲染
  3. 调整 Render Queue 控制渲染顺序

场景三:Shadow Map 自阴影斑块

这不是典型的深度冲突,但本质同样是深度精度不足。URP 通过 ShadowCaster Pass 中的 normalBiasdepthBias 来处理。 你可以在 URP Asset → Shadows 中调整这些参数。

cs 复制代码
C# --- Light Inspector
// URP Asset → Shadows:
// Depth Bias:    控制阴影深度偏移量
// Normal Bias:   沿法线方向偏移采样位置
// Soft Shadows:  启用后叠加 PCF 滤波

// 或者通过脚本调整定向光:
Light light = GetComponent<Light>();
light.shadowBias = 0.05f;
light.shadowNormalBias = 0.4f;

七、排查清单 & 总结

当你在 URP 项目中遇到视觉闪烁/锯齿交错时,按以下顺序排查:

  1. **确认是深度冲突:**移动摄像机时,闪烁是否持续?两个面的纹理是否交替出现?
  2. **检查面间距:**在场景编辑器中拉近查看,两个面是否几乎重合?
  3. **调整 Near/Far:**Near 是否设置得过小(< 0.05)?
  4. 尝试 Offset: 在 Shader 中加 Offset -1, -1
  5. **调整 Render Queue:**确保渲染顺序符合预期
  6. **检查深度格式:**是否使用了 16 位深度?换用 24 位或 32 位
  7. **考虑 Reverse-Z:**新版 URP 默认启用,确认未被意外禁用
解决方案 适用场景 成本 效果
Offset 命令 贴花、描边、UI 叠加 几乎为零 ★★★★★
增大 Near / 减小 Far 大场景、远处物体 ★★★★☆
Reverse-Z 所有场景(URP 默认) ★★★★★
顶点法线偏移 双层平面、墙面覆盖 ★★★★☆
Render Queue 调整 同位置多层物体 ★★★☆☆
升级深度格式 (32-bit) 整体精度不足 带宽增加 ★★★★☆
SV_Depth 手动偏移 复杂自定义需求 高(禁用 Early-Z) ★★★☆☆

深度冲突本质上是深度缓冲的精度与场景需求之间的资源竞争 。 理解深度值的非线性分布,合理设置近远裁面,利用 Offset 和 Reverse-Z 等工具,大多数深度冲突都能在不牺牲性能的前提下优雅解决。