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
调参原则 用最小有效值,避免浮起视差;远距离斜面需适度加大
相关推荐
winlife_9 分钟前
让 AI 跑通“调跳跃手感“的完整闭环:funplay-unity-mcp 实战案例
人工智能·unity·游戏引擎·ai编程·mcp·游戏手感
winlife_37 分钟前
从一句话到可玩原型:用 funplay-unity-mcp 让 AI 搭起完整游戏循环
人工智能·游戏·unity·ai编程·mcp·游戏原型
ellis19703 小时前
Unity中使用Cursor辅助开发
unity
avi91115 小时前
Unity商业插件之(三) Editor扩展,二次开发
unity·单例·editor扩展·editor开发
winlife_6 小时前
让 AI 自动跑 PlayMode 回归测试:从 BUG 注入到自动判 FAIL 的完整闭环
人工智能·unity·bug·ai编程·mcp·回归测试·游戏测试
WarPigs1 天前
游戏签到系统
unity
小拉达不是臭老鼠1 天前
Unity中的UI系统之UGUI
学习·ui·unity
万兴丶1 天前
Coplay适用于 Unity 的“Al 代理”使用指南
unity·游戏引擎·ai编程
魔士于安1 天前
Unity材质球大合集
unity·游戏引擎·材质
mxwin1 天前
Unity Shader 冰面 Shader 制作原理与流程
unity·游戏引擎·shader