Unity Shader 深度写入与关闭ZWrite Off · 半透明排序 · 粒子穿插

半透明物体为什么必须关闭深度写入?关闭后粒子系统为什么会互相穿插? CPU 端距离排序与 Order in Layer 如何配合?某些特效为什么要故意开启 ZWrite? 本文逐一拆解这些问题,并给出可落地的 URP Shader 代码。

Section 01

深度缓冲的工作原理回顾

深度缓冲(Depth Buffer,又称 Z-Buffer)是 GPU 中与颜色缓冲等大的一张纹理, 每个像素存储一个归一化后的深度值 d ∈ [0, 1](Direct3D 约定;OpenGL 为 [-1,1] 映射至 [0,1])。

每次 Fragment Shader 输出一个片元,GPU 会先做 深度测试(Depth Test) : 将当前片元的深度与缓冲中已有的值做比较(默认 ZTest LEqual,即"离相机更近或相等则通过")。 测试通过后,如果 ZWrite On,则用当前深度值覆盖缓冲;测试失败则整个片元被丢弃,颜色缓冲也不写入。

注意:深度测试 (ZTest)和深度写入(ZWrite)是两个独立的开关。 测试决定"此片元是否可见",写入决定"通过后是否更新深度缓冲"。 半透明渲染的核心矛盾,正是围绕这两个开关展开的。


Section 02

ZWrite On vs ZWrite Off --- 核心区别

属性 ZWrite On ZWrite Off
深度缓冲更新
后续物体是否被遮挡 会被遮挡(正确) 不会被遮挡
颜色混合(Blend) 通常不启用 必须配合 Blend 使用
典型用途 不透明物体、地面、建筑 玻璃、烟雾、火焰、粒子
渲染队列 Geometry (2000) Transparent (3000)
排序依赖 依赖深度缓冲自动排序 必须手动或 CPU 排序

Section 03

半透明为什么必须关闭深度写入

半透明的本质是颜色混合(Alpha Blending) :最终颜色 = 前景色 × α + 背景色 × (1−α)。 这个公式依赖"背景已经在颜色缓冲里"。若半透明物体写入深度,就会遮挡自己身后的物体, 导致背景被剔除、无法参与混合,透明效果就消失了。

核心规则

半透明物体(Alpha Blending)必须使用 ZWrite Off, 否则它会向深度缓冲写入一个不透明的深度值,把自己身后的物体全部"遮死",透明混合就无从谈起。

正确的 URP 半透明 Shader 配置

cs 复制代码
核心规则
半透明物体(Alpha Blending)必须使用 ZWrite Off, 否则它会向深度缓冲写入一个不透明的深度值,把自己身后的物体全部"遮死",透明混合就无从谈起。
正确的 URP 半透明 Shader 配置
ShaderLab
TransparentSurface.shader
Shader "Custom/URP_Transparent"
{
    Properties
    {
        _BaseColor ("Base Color", Color) = (1,1,1,0.5)
        _BaseMap   ("Albedo",    2D)    = "white" {}
    }
    SubShader
    {
        // ① 渲染队列必须设为 Transparent
        Tags
        {
            "RenderType"      = "Transparent"
            "Queue"           = "Transparent"  // 3000,在不透明之后渲染
            "RenderPipeline"  = "UniversalPipeline"
        }
        Pass
        {
            // ② 混合模式:标准 Alpha Blending
            Blend SrcAlpha OneMinusSrcAlpha
            // ③ 关键:关闭深度写入
            ZWrite Off
            // ④ 深度测试保持开启(读取不透明物体的深度)
            ZTest  LEqual
            // ⑤ 双面渲染(玻璃类效果去掉背面剔除)
            Cull   Off
            // HLSL 程序 ... (省略)
        }
    }
}

要点

ZWrite Off 只是"不写入",但仍然可以读取深度缓冲(ZTest 仍起效)。 因此半透明物体依然会被不透明物体正确遮挡------它只是不阻断身后的其他东西。


Section 04

关闭后的代价:粒子系统互相穿插

当所有粒子都使用 ZWrite Off,它们在深度缓冲层面"彼此透明"。 两个粒子系统同时绘制时,谁后画谁就叠在上面------但后画者同样不写深度, 下一帧先画者又可能叠到上面,产生**闪烁穿插(Z-fighting for Transparents)**的视觉问题。

穿插的根本原因

Transparent 队列中的物体,Unity 默认按物体包围盒中心到相机的距离 从远到近排序(Painter's Algorithm)。 但粒子系统是"一个 Renderer 对应多个 Billboard 粒子",整个系统只有一个深度值参与排序。 当两个粒子系统的位置非常接近甚至重叠时,任何一帧微小的相机移动都可能让排序反转, 造成帧间闪烁。粒子系统内部的粒子彼此之间也没有逐粒子的深度比较。

注意

粒子系统"穿插"与传统的"Z-fighting"是两个不同问题: 前者是多个半透明 Renderer 排序不稳定;后者是两个不透明面片深度值精度相近造成交替可见。 解决方案也完全不同。


Section 05

CPU 端距离排序 与 Order in Layer

方案一:Particle System 内置排序

Unity Particle System 的 Renderer 模块提供了 Sort Mode 属性, 可以对同一个粒子系统内部的粒子按距离排序:

Sort Mode 说明 性能
None 不排序(默认),按生成顺序绘制 最快
By Distance 每帧 CPU 计算每个粒子到相机距离并排序 中等(粒子数 > 500 开始明显)
Oldest in Front 生命周期最长的粒子先画(模拟烟雾老粒子在底)
Youngest in Front 最新生成的粒子最后画
cs 复制代码
using UnityEngine;
// 运行时动态修改粒子排序模式
public class ParticleSortSetup : MonoBehaviour
{
    void Start()
    {
        var ps  = GetComponent<ParticleSystem>();
        var psr = ps.GetComponent<ParticleSystemRenderer>();
        // 开启逐粒子距离排序
        psr.sortMode = ParticleSystemSortMode.Distance;
        // sortingFudge:正值让此系统整体靠后绘制
        // 对多系统穿插问题可手动微调
        psr.sortingFudge = 5f;
    }
}

方案二:Order in Layer(排序图层)

Sorting Layer + Order in Layer 是 Unity 的 2D/粒子排序系统, 适用于同一 3D 位置的多个粒子系统需要稳定绘制顺序的场景。 数值越大,越晚绘制(越靠近屏幕前方)。

cs 复制代码
using UnityEngine;
public class ParticleLayerOrder : MonoBehaviour
{
    [SerializeField] int orderInLayer = 10;
    void Awake()
    {
        var renderer = GetComponent<ParticleSystemRenderer>();
        // 设置排序图层名称(需在 Tags & Layers 中预先定义)
        renderer.sortingLayerName  = "FX";
        // 同图层内的精细顺序
        renderer.sortingOrder      = orderInLayer;
    }
}

方案三:sortingFudge(微调偏移)

ParticleSystemRenderer.sortingFudge 是一个纯粹的"数值偏移", 会被加到该粒子系统到相机的距离计算结果上,从而影响在 Transparent 队列中的排序位置。 正值 = 人为增大距离 = 更先绘制(靠后显示);负值 = 更后绘制(靠前显示)。 适合做轻量微调,而不需要修改 Sorting Layer。

推荐组合策略

对于复杂特效(烟+火+火花+UI),建议:

① 用 Sorting Layer 划分大类(地面FX / 角色FX / 界面FX);

② 用 Order in Layer 控制同类内的顺序;

③ 用 sortingFudge 做同 Order 内的微调;

④ 性能允许时再开启 Sort Mode: By Distance 解决单系统内部粒子排序。


Section 06

故意开启 ZWrite 的特效场景

并非所有特效都要关闭深度写入。某些情况下,刻意让特效写入深度缓冲可以制造更真实的遮挡关系, 或解决特定的排序问题。以下是三种常见场景:

场景一:遮挡后方粒子的"硬壳"效果

爆炸中心通常有一个不透明的核心火球,希望它遮住自己身后的烟雾粒子。 将核心火球 Shader 设为 ZWrite On,队列设为 Transparent-1(先于普通半透明绘制), 即可让它向深度缓冲写入一个"硬遮挡",后续烟雾在该区域的像素会被深度测试丢弃。

场景二:软粒子(Soft Particles)的深度采样

URP 的 Soft Particles 功能需要读取不透明物体的深度(来自 _CameraDepthTexture) 计算粒子与场景的交叉处软化值。这要求不透明物体必须先写入深度缓冲, 而粒子自身仍然 ZWrite Off。两者分开,互不干扰。

场景三:"伪不透明"蒙版粒子

某些低多边形风格游戏使用 AlphaTest 粒子(透明度低于阈值的像素直接丢弃,高于阈值的完全不透明)。 此时可以 ZWrite On + AlphaToMask On,让粒子参与深度排序,避免穿插。

cs 复制代码
Shader "Custom/URP_FX_OpaqueCore"
{
    SubShader
    {
        Tags
        {
            "RenderType"  = "Transparent"
            // 比普通半透明(3000)早一个单位绘制
            "Queue"       = "Transparent-1"
        }
        Pass
        {
            // ① 先写入深度,为后续粒子建立遮挡关系
            ZWrite On
            ZTest  LEqual
            // ② 仍然混合(半透明边缘),但核心区域 alpha≈1
            Blend SrcAlpha OneMinusSrcAlpha
            // HLSL 程序 ...
        }
    }
}
cs 复制代码
Shader "Custom/URP_FX_OpaqueCore"
{
    SubShader
    {
        Tags
        {
            "RenderType"  = "Transparent"
            // 比普通半透明(3000)早一个单位绘制
            "Queue"       = "Transparent-1"
        }
        Pass
        {
            // ① 先写入深度,为后续粒子建立遮挡关系
            ZWrite On
            ZTest  LEqual
            // ② 仍然混合(半透明边缘),但核心区域 alpha≈1
            Blend SrcAlpha OneMinusSrcAlpha
            // HLSL 程序 ...
        }
    }
}
ShaderLab
AlphaTestParticle.shader --- AlphaToMask
Shader "Custom/URP_AlphaTest_Particle"
{
    Properties
    {
        _Cutoff ("Alpha Cutoff", Range(0,1)) = 0.5
    }
    SubShader
    {
        Tags { "Queue" = "AlphaTest"  /* 2450, 不透明之后半透明之前 */ }
        Pass
        {
            // AlphaTest 可以写深度,参与正常不透明排序
            ZWrite On
            // 利用 MSAA 把 Alpha 映射为多重采样遮罩,边缘更平滑
            AlphaToMask On
            HLSLPROGRAM
            // ... fragment shader 中使用 clip(alpha - _Cutoff)
            ENDHLSL
        }
    }
}

注意:ZWrite On + Blend 的矛盾

同时开启 ZWrite OnBlend SrcAlpha OneMinusSrcAlpha 时, 深度写入的是"当前面片的深度",而颜色则按 alpha 混合。 这意味着即使边缘几乎透明,深度缓冲里也会留下该面片的深度值。 后续绘制与该面片深度相近的半透明物体可能被错误裁剪。 仅在核心区域完全不透明时推荐这种用法


Section 07

完整 Shader 代码示例

下面是一个支持动态切换 ZWrite 模式 的 URP 粒子 Shader, 可以通过 Material 属性在 Inspector 中控制: 0 = ZWrite Off(标准半透明),1 = ZWrite On(遮挡型特效)。

cs 复制代码
注意:ZWrite On + Blend 的矛盾
同时开启 ZWrite On 和 Blend SrcAlpha OneMinusSrcAlpha 时, 深度写入的是"当前面片的深度",而颜色则按 alpha 混合。 这意味着即使边缘几乎透明,深度缓冲里也会留下该面片的深度值。 后续绘制与该面片深度相近的半透明物体可能被错误裁剪。 仅在核心区域完全不透明时推荐这种用法。
Section 07
完整 Shader 代码示例
下面是一个支持动态切换 ZWrite 模式的 URP 粒子 Shader, 可以通过 Material 属性在 Inspector 中控制: 0 = ZWrite Off(标准半透明),1 = ZWrite On(遮挡型特效)。

ShaderLab + HLSL
URP_ParticleFX.shader
Shader "Custom/URP_ParticleFX"
{
    Properties
    {
        _BaseMap    ("Particle Texture", 2D)           = "white" {}
        _BaseColor  ("Color",           Color)        = (1,1,1,1)
        _SoftNear   ("Soft Near Fade",  Float)       = 0.1
        [Enum(Off, 0, On, 1)] _ZWrite ("ZWrite", Float) = 0
        [Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 5
        [Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 10
    }
    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
            "RenderType"     = "Transparent"
            "Queue"          = "Transparent"
        }
        Pass
        {
            Name "ForwardUnlit"
            Tags { "LightMode" = "UniversalForward" }
            Blend  [_SrcBlend] [_DstBlend]
            ZWrite [_ZWrite]
            ZTest  LEqual
            Cull   Off
            HLSLPROGRAM
            #pragma vertex   ParticleVert
            #pragma fragment ParticleFrag
            #pragma multi_compile_particles
            #pragma multi_compile_fog
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
            TEXTURE2D(_BaseMap);  SAMPLER(sampler_BaseMap);
            CBUFFER_START(UnityPerMaterial)
                float4 _BaseColor;
                float4 _BaseMap_ST;
                float  _SoftNear;
            CBUFFER_END
            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv         : TEXCOORD0;
                float4 color      : COLOR;
            };
            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float2 uv          : TEXCOORD0;
                float4 color       : COLOR;
                float4 screenPos   : TEXCOORD1;
            };
            Varyings ParticleVert(Attributes IN)
            {
                Varyings OUT;
                VertexPositionInputs vpi = GetVertexPositionInputs(IN.positionOS.xyz);
                OUT.positionHCS = vpi.positionCS;
                OUT.uv          = TRANSFORM_TEX(IN.uv, _BaseMap);
                OUT.color       = IN.color;
                // 软粒子需要屏幕坐标来采样深度纹理
                OUT.screenPos   = ComputeScreenPos(vpi.positionCS);
                return OUT;
            }
            half4 ParticleFrag(Varyings IN) : SV_Target
            {
                half4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
                half4 col      = texColor * _BaseColor * IN.color;
                // ── 软粒子:边缘与场景几何体接触处淡出 ──
                float2 screenUV   = IN.screenPos.xy / IN.screenPos.w;
                float  sceneDepth = LinearEyeDepth(
                                        SampleSceneDepth(screenUV),
                                        _ZBufferParams);
                float  partDepth  = IN.screenPos.w;
                float  softFade   = saturate((sceneDepth - partDepth) / _SoftNear);
                col.a *= softFade;
                return col;
            }
            ENDHLSL
        }
    }
}

Section 08

决策速查表

根据你的特效需求,快速查找应该使用的配置组合:

特效类型 ZWrite Queue Sort 方案 典型问题
标准烟雾 / 气体 Off Transparent (3000) By Distance + Fudge 多系统穿插,调 sortingFudge
火焰粒子 Off Transparent (3000) Order in Layer 烟火层次,Order 分级
爆炸核心火球 On Transparent-1 (2999) ---(不透明遮挡) 注意边缘 alpha 问题
玻璃 / 水面 Off Transparent (3000) ---(单面片无需排序) 背面渲染顺序 / 双面 Cull Off
低多边形叶片 (AlphaTest) On AlphaTest (2450) GPU 深度排序 开 AlphaToMask 改善锯齿
UI 血量爆发特效 Off Overlay / UI (4000+) Sorting Layer: UI Canvas 层级配合
贴地光圈 / Decal Off Transparent (3000) URP Decal Projector URP Decal Renderer Feature
软粒子(Soft Particles) Off Transparent (3000) By Distance 需在 URP Asset 开启 Depth Texture

软粒子配置提示

软粒子(Soft Particles)需要在 URP Renderer Asset 中开启 Depth Texture , 否则 SampleSceneDepth() 采样结果为 0,softFade 恒为 0,粒子完全透明消失。 路径:Project Settings → Graphics → URP Asset → Depth Texture ✓

相关推荐
张老师带你学4 小时前
宇宙飞船完整Unity项目
科技·游戏·unity·游戏引擎·模型
mxwin4 小时前
Unity URP 下的流体模拟 深入解析 Navier-Stokes 方程与浅水方程的数学原理
unity·游戏引擎
mxwin7 小时前
Unity Shader 深度重建世界坐标
unity·游戏引擎·shader
雪儿waii8 小时前
Unity 中继承(父类子类)用法详解
unity·游戏引擎
总写bug的程序员8 小时前
用 AI 蒸馏球员的思维操作系统:qiuyuan-skill 技术解析
人工智能·unity·游戏引擎
mxwin11 小时前
Unity Shader 预乘 Alpha 完全指南 解决半透明纹理边缘黑边问题,让你的 UI 渲染更干净
unity·游戏引擎
mxwin11 小时前
Unity URP 软粒子(Soft Particles)完全指南
unity·游戏引擎·shader
mxwin12 小时前
Unity Shader 深度偏移Depth Bias / Offset 完全指南
unity·游戏引擎·shader
星河耀银海12 小时前
Unity基础:UI组件详解:Button按钮的点击事件绑定
ui·unity·lucene