Unity Shader 深度偏移Depth Bias / Offset 完全指南

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 块内, 与 ZTestZWrite 同级。 下面展示一个完整的 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
调参原则 用最小有效值,避免浮起视差;远距离斜面需适度加大
相关推荐
星河耀银海3 小时前
Unity基础:UI组件详解:Button按钮的点击事件绑定
ui·unity·lucene
RReality4 小时前
【Unity Shader URP】平面反射(Planar Reflection)实战教程
ui·平面·unity·游戏引擎·图形渲染·材质
风酥糖4 小时前
Godot游戏练习01-第30节-教程结束我继续
游戏·游戏引擎·godot
Heikepengmu5 小时前
用Unity打造愤怒的小鸟游戏
游戏·unity·游戏引擎
雪儿waii14 小时前
Unity 中的 Resources 详解
unity·游戏引擎
RReality1 天前
【Unity UGUI】Toggle / ToggleGroup 与 Dropdown
ui·unity·游戏引擎·图形渲染·材质
雪儿waii1 天前
Unity 中的 InvokeRepeating 详解
unity·游戏引擎
mxwin1 天前
Unity Shader 程序化生成:Shader 中的数学宇宙
unity·游戏引擎
雪儿waii1 天前
Unity 中的 Quaternion(四元数)详解
unity·游戏引擎