从深度缓冲的基本原理出发,系统解析 Z-Fighting 的成因,并提供 URP Shader 中的实用解决方案。
一、什么是深度
在 3D 渲染中,深度 描述的是物体表面上的某个点距离摄像机有多远。 想象你站在一条笔直的马路上,近处的路灯比远处的路灯"更浅",这个"深浅"就是深度。
Unity 使用 深度缓冲 来记录屏幕上每个像素的深度值。 它是一张与屏幕分辨率相同的"灰度图"——每个像素存储的不是颜色,而是一个 [0, 1] 范围的值: 0 代表最近(靠近近裁面),1 代表最远(接近远裁面)。

关键认知
深度缓冲本质上是一张逐像素的浮点纹理。URP 中默认使用 _CameraDepthTexture 来访问它,精度通常为 24 位或 32 位。
二、深度测试的工作原理
当 GPU 要绘制一个片元时,它不会直接覆盖屏幕上已有的颜色。它会先做一次深度测试:
- 读取目标像素当前深度缓冲中的值(已有值)
- 用当前片元的深度值与已有值比较
- 如果片元更近(默认是小于),则通过测试 → 更新颜色和深度缓冲
- 如果片元更远,则丢弃该片元

三、什么是深度冲突
深度冲突,英文叫 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 命令,可以在光栅化阶段对深度值施加一个微小的偏移:
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 可以显著改善远处精度:
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 确保正确的绘制顺序:
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 中手动添加深度偏移:
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;
}
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) 闪烁
在墙壁上放置弹孔贴花时,贴花面与墙面几乎共面。解决方案:
- 使用
Offset -1, -1将贴花略微拉近 - 或将贴花面沿法线偏移 0.001-0.005 单位
- 使用 URP Decal Projector 或 DBuffer Decal(如果已启用)
HLSL — Decal Shader Pass
Pass {
Name "Decal"
Tags { "LightMode" = "UniversalForward" }
ZWrite Off
ZTest LEqual
Offset -1, -1 // 核心:轻微拉近贴花
Blend SrcAlpha OneMinusSrcAlpha
}
场景二:两个平面重叠
当地形与水面、或两层地面材质重叠时:
- 增大层间距(最简单的方案)
- 使用
ZTest GEqual+ZWrite Off确保仅在不透明物体之后渲染 - 调整 Render Queue 控制渲染顺序
场景三:Shadow Map 自阴影斑块
这不是典型的深度冲突,但本质同样是深度精度不足。URP 通过 ShadowCaster Pass 中的 normalBias 和 depthBias 来处理。 你可以在 URP Asset → Shadows 中调整这些参数。
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 项目中遇到视觉闪烁/锯齿交错时,按以下顺序排查:
- 确认是深度冲突:移动摄像机时,闪烁是否持续?两个面的纹理是否交替出现?
- 检查面间距:在场景编辑器中拉近查看,两个面是否几乎重合?
- 调整 Near/Far:Near 是否设置得过小(< 0.05)?
- 尝试 Offset:在 Shader 中加
Offset -1, -1 - 调整 Render Queue:确保渲染顺序符合预期
- 检查深度格式:是否使用了 16 位深度?换用 24 位或 32 位
- 考虑 Reverse-Z:新版 URP 默认启用,确认未被意外禁用
| 解决方案 | 适用场景 | 成本 | 效果 |
|---|---|---|---|
Offset 命令 | 贴花、描边、UI 叠加 | 几乎为零 | ★★★★★ |
| 增大 Near / 减小 Far | 大场景、远处物体 | 零 | ★★★★☆ |
| Reverse-Z | 所有场景(URP 默认) | 零 | ★★★★★ |
| 顶点法线偏移 | 双层平面、墙面覆盖 | 低 | ★★★★☆ |
| Render Queue 调整 | 同位置多层物体 | 零 | ★★★☆☆ |
| 升级深度格式 (32-bit) | 整体精度不足 | 带宽增加 | ★★★★☆ |
| SV_Depth 手动偏移 | 复杂自定义需求 | 高(禁用 Early-Z) | ★★★☆☆ |
深度冲突本质上是深度缓冲的精度与场景需求之间的资源竞争。 理解深度值的非线性分布,合理设置近远裁面,利用 Offset 和 Reverse-Z 等工具,大多数深度冲突都能在不牺牲性能的前提下优雅解决。
250

被折叠的 条评论
为什么被折叠?



