1Z-fighting:问题的根源
当两个多边形(或同一多边形的不同 Pass)映射到屏幕上几乎重叠 时, GPU 在逐像素比较深度值时会出现交替胜负的情况------ 某帧 A 在前,下一帧 B 在前,画面产生闪烁条纹,这便是 Z-fighting(深度冲突)。

Z-fighting 的直接原因是浮点深度精度有限------ 深度缓冲区的每一位都在映射极大的世界空间范围, 当两层面的深度差小于 1 ULP(最小精度单位)时,比较结果就变得不确定。
⚠️距摄像机越远,深度精度越低(NDC 空间中近平面附近的精度远高于远平面), 因此 Z-fighting 在远处更明显。反转深度(Reverse-Z)方案可缓解,但不能完全消除覆盖层问题。
2深度缓冲区与深度测试基础
GPU 维护一张与帧缓冲同分辨率的深度缓冲区(Depth Buffer / Z-Buffer), 存储每像素的当前最近深度值(范围 0--1,精度通常为 24 位定点或 32 位浮点)。

深度测试的判断依据是经过 Offset 调整后的深度值, 因此 Offset 直接影响该像素是否通过测试、是否被写入帧缓冲。
💡URP 默认使用 反转深度(Reversed-Z) :近平面映射到 NDC 深度 1, 远平面映射到 0。这使 Offset 的"向摄像机移近"方向为增大偏移量而非减小, 调参时需特别注意。
3Offset 指令:Factor 与 Units
Unity ShaderLab 的 Offset 指令对应 OpenGL 的 glPolygonOffset / Vulkan 的深度偏移状态, 语法为:
cs
ShaderLab
Offset 语法
// 语法
Offset Factor, Units
// 示例:将片元深度向摄像机推近一点点
Offset -1, -1
参数含义
Factor(斜率偏移因子)
与多边形对摄像机的**倾斜程度(深度斜率)**相乘。 当多边形斜对摄像机时,单位像素内深度变化大,此因子同步放大偏移,保证边缘不泄漏。
Units(固定偏移单位)
乘以深度缓冲区的最小可分辨单位(1 ULP),提供与斜率无关的固定推移量。 即使是与屏幕完全平行的多边形,也能获得偏移。
两个参数的符号约定(OpenGL/Vulkan 传统):
| 参数 | 负值效果 | 正值效果 | 常用场景 |
|---|---|---|---|
Factor |
朝摄像机偏移(更靠近) | 远离摄像机 | 贴花、投影纹理:-1 |
Units |
朝摄像机偏移 | 远离摄像机 | 贴花、覆盖层:-1 |
⚠️URP + Reversed-Z 注意: 深度轴方向反转,但 Offset 参数本身的符号约定不变 ------ Unity 内部已处理方向映射,-1, -1 仍然等同于"向摄像机推近"。
4偏移公式详解
GPU 计算偏移量的公式源自 OpenGL 规范:
depth_offset = Factor × max(|∂z/∂x|, |∂z/∂y|) + Units × r∂z/∂x, ∂z/∂y --- 当前多边形在屏幕空间的深度偏导数(即斜率)
r --- 深度缓冲区能表示的最小增量(与精度位数有关)
最终写入深度测试的深度值为:
z_adjusted = z_fragment + depth_offsetz_fragment 为片元着色器输出的原始深度(裁剪空间)

为什么需要 Factor?
斜面多边形在屏幕空间内每个像素的深度变化很大(深度偏导数大)。 如果只用 Units 做固定偏移,斜面边缘的像素偏移量可能不够, 仍然发生 Z-fighting。Factor 会随斜率自动放大偏移量, 保证整块多边形都能"浮出"底层几何体。
5在 URP Shader 中使用 Offset
Offset 指令写在 Pass 块内, 与 ZTest、ZWrite 同级。 下面展示一个完整的 URP Unlit Shader 片段:
cs
Shader "Custom/URP/DepthOffsetUnlit"
{
Properties
{
_BaseColor ("Base Color", Color) = (1,1,1,1)
_BaseMap ("Base Texture", 2D) = "white" {}
// 深度偏移参数,开放给 Inspector 调节
_OffsetFactor ("Offset Factor", Range(-5,5)) = -1
_OffsetUnits ("Offset Units", Range(-100,100)) = -1
}
SubShader
{
// URP 通用标签
Tags {
"RenderType" = "Transparent"
"Queue" = "Transparent"
"RenderPipeline" = "UniversalPipeline"
}
Pass
{
Name "UnlitPass"
Tags { "LightMode" = "UniversalForward" }
// ── 深度状态 ──
ZWrite Off // 覆盖层通常不写入深度
ZTest LEqual // 保持默认比较方式
Offset -1, -1 // ← 关键:Factor=-1, Units=-1
// ── 混合模式(透明贴花) ──
Blend SrcAlpha OneMinusSrcAlpha
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float4 _BaseMap_ST;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
struct Attributes {
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};
struct Varyings {
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
};
Varyings vert(Attributes input)
{
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
return output;
}
half4 frag(Varyings input) : SV_Target
{
half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
return color * _BaseColor;
}
ENDHLSL
}
}
}
第 29 行的 Offset -1, -1 是核心,它告诉 GPU: 在执行深度测试前,将此片元的深度值向摄像机方向偏移, 使其"优先"通过对比底层网格存储在深度缓冲里的值。
6贴花(Decal)中的应用
贴花是深度偏移最典型的使用场景。贴花网格(通常是覆盖在地面、墙面上的薄片) 与底层几何体几乎完全共面,若不施加偏移必然产生 Z-fighting。

URP 内置 Decal 系统
Unity URP 14+ 提供了 Decal Projector 组件和配套的 DBuffer / ScreenSpace 两种投影模式, 它们内部自动处理了深度偏移与渲染队列,无需手动编写 Offset。 但若你使用自定义贴花 Shader,则必须自行添加。
cs
Pass
{
Name "DecalForwardLit"
Tags { "LightMode" = "DecalForwardEmissive" }
// 贴花必须禁止写入深度,否则遮挡后续物体
ZWrite Off
ZTest LEqual
// Factor=-1 处理斜面,Units=-1 处理平面
Offset -1, -1
// Alpha 混合
Blend SrcAlpha OneMinusSrcAlpha
ColorMask RGB
HLSLPROGRAM
#pragma vertex DecalVert
#pragma fragment DecalFrag
// ... 省略顶点/片元实现 ...
ENDHLSL
}
多层贴花叠加
当多张贴花叠加时(如地面积水 + 弹孔),需要让它们各自偏移: 第一层 Offset -1, -1, 第二层 Offset -2, -2,以此类推。 这通过 Material 的 RenderQueue 差分 + 偏移叠加共同保证层序。
cs
using UnityEngine;
public class DecalLayerManager : MonoBehaviour
{
[SerializeField] private Renderer[] decalRenderers;
void Start()
{
// 为每层贴花动态设置不同的渲染队列,拉开层次
for (int i = 0; i < decalRenderers.Length; i++)
{
// Transparent = 3000,每层 +1
decalRenderers[i].sharedMaterial.renderQueue = 3000 + i;
}
}
}
7调参实战与常见陷阱
推荐起始值
| 场景 | Factor | Units | 备注 |
|---|---|---|---|
| 平面贴花(与屏幕近乎平行) | 0 | -1 ~ -4 | 斜率≈0,Factor 无效,只需 Units |
| 斜面贴花 / 任意方向 | -1 | -1 | 通用起始值 |
| 远距离斜面(精度损失大) | -1 ~ -2 | -4 ~ -8 | 适度加大 |
| 阴影投影面(ShadowCaster) | 1 | 1 | 反向推开避免自阴影痤疮(Acne) |
| 多层贴花第 n 层 | -n | -n | 各层线性递增 |
常见陷阱
🚫
陷阱 1:偏移过大导致视差(Parallax)
Factor / Units 设置过大时,覆盖层在摄像机移动时会出现明显"浮起感", 尤其在斜视角下。应尽量用最小有效值。
🚫
陷阱 2:ZWrite On + Offset 冲突
若贴花开启了 ZWrite On,其偏移后的深度值会写入 Z-Buffer, 导致后续透明物体被错误遮挡。贴花几乎都应使用 ZWrite Off。
🚫
陷阱 3:在 Shader Graph 中找不到 Offset
Shader Graph 目前(Unity 2022/2023/6)不支持直接设置 Offset 。 解决方案:在 Graph 的 Graph Settings 中设置自定义 Pass 标签, 或切换为 .shader 文件手写该 Pass。
✅
技巧:Shader Graph 变通方案
在 Shader Graph 资产旁创建一个同名 .shader 文件, 仅覆盖需要 Offset 的 Pass,其余 Pass 仍走 Graph 生成的代码。 或使用 ShaderGraph.Target 自定义 Renderer Feature 注入状态。
✅
技巧:Frame Debugger 辅助调参
打开 Window → Analysis → Frame Debugger,找到贴花的 Draw Call, 观察深度写入与比较结果,可快速验证 Offset 是否生效。
贴花 Shader 检查清单1RenderType = "Transparent" 且 Queue = "Transparent+1"(或更高)2ZWrite Off3ZTest LEqual(默认值,勿改为 Always,否则穿透遮挡物)4Offset -1, -1(根据斜率与距离酌情调大绝对值)✓Blend SrcAlpha OneMinusSrcAlpha(或 Alpha Premultiplied)
图5:贴花 Shader 标准配置清单
8总结速查
| 概念 | 要点 |
|---|---|
| Z-fighting | 两层多边形深度值过于接近,浮点精度不足导致交替可见,产生闪烁 |
| Offset 语法 | Offset Factor, Units,写在 Pass 块内,深度测试前生效 |
| Factor | 乘以深度斜率,处理斜面;负值向摄像机推近 |
| Units | 乘以深度最小单位,处理平面;负值向摄像机推近 |
| 贴花标准配置 | ZWrite Off + ZTest LEqual + Offset -1, -1 |
| 阴影 Acne | ShadowCaster Pass 使用 Offset 1, 1 反推 |
| Shader Graph 限制 | 不支持 Offset 节点,需手写 .shader 或自定义 Pass |
| 调参原则 | 用最小有效值,避免浮起视差;远距离斜面需适度加大 |