半透明物体为什么必须关闭深度写入?关闭后粒子系统为什么会互相穿插? 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 On 和 Blend 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 ✓