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

张开发
2026/4/21 2:08:38 15 分钟阅读

分享文章

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 02ZWrite On vs ZWrite Off — 核心区别属性ZWrite OnZWrite Off深度缓冲更新是否后续物体是否被遮挡会被遮挡正确不会被遮挡颜色混合Blend通常不启用必须配合 Blend 使用典型用途不透明物体、地面、建筑玻璃、烟雾、火焰、粒子渲染队列Geometry (2000)Transparent (3000)排序依赖依赖深度缓冲自动排序必须手动或 CPU 排序Section 03半透明为什么必须关闭深度写入半透明的本质是颜色混合Alpha Blending最终颜色 前景色 × α 背景色 × (1−α)。 这个公式依赖背景已经在颜色缓冲里。若半透明物体写入深度就会遮挡自己身后的物体 导致背景被剔除、无法参与混合透明效果就消失了。核心规则半透明物体Alpha Blending必须使用ZWrite Off 否则它会向深度缓冲写入一个不透明的深度值把自己身后的物体全部遮死透明混合就无从谈起。正确的 URP 半透明 Shader 配置核心规则 半透明物体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 默认按物体包围盒中心到相机的距离从远到近排序Painters Algorithm。 但粒子系统是一个 Renderer 对应多个 Billboard 粒子整个系统只有一个深度值参与排序。 当两个粒子系统的位置非常接近甚至重叠时任何一帧微小的相机移动都可能让排序反转 造成帧间闪烁。粒子系统内部的粒子彼此之间也没有逐粒子的深度比较。注意粒子系统穿插与传统的Z-fighting是两个不同问题 前者是多个半透明 Renderer 排序不稳定后者是两个不透明面片深度值精度相近造成交替可见。 解决方案也完全不同。Section 05CPU 端距离排序 与 Order in Layer方案一Particle System 内置排序Unity Particle System 的Renderer模块提供了Sort Mode属性 可以对同一个粒子系统内部的粒子按距离排序Sort Mode说明性能None不排序默认按生成顺序绘制最快By Distance每帧 CPU 计算每个粒子到相机距离并排序中等粒子数 500 开始明显Oldest in Front生命周期最长的粒子先画模拟烟雾老粒子在底快Youngest in Front最新生成的粒子最后画快using UnityEngine; // 运行时动态修改粒子排序模式 public class ParticleSortSetup : MonoBehaviour { void Start() { var ps GetComponentParticleSystem(); var psr ps.GetComponentParticleSystemRenderer(); // 开启逐粒子距离排序 psr.sortMode ParticleSystemSortMode.Distance; // sortingFudge正值让此系统整体靠后绘制 // 对多系统穿插问题可手动微调 psr.sortingFudge 5f; } }方案二Order in Layer排序图层Sorting LayerOrder in Layer是 Unity 的 2D/粒子排序系统 适用于同一 3D 位置的多个粒子系统需要稳定绘制顺序的场景。 数值越大越晚绘制越靠近屏幕前方。using UnityEngine; public class ParticleLayerOrder : MonoBehaviour { [SerializeField] int orderInLayer 10; void Awake() { var renderer GetComponentParticleSystemRenderer(); // 设置排序图层名称需在 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 OnAlphaToMask On让粒子参与深度排序避免穿插。Shader Custom/URP_FX_OpaqueCore { SubShader { Tags { RenderType Transparent // 比普通半透明3000早一个单位绘制 Queue Transparent-1 } Pass { // ① 先写入深度为后续粒子建立遮挡关系 ZWrite On ZTest LEqual // ② 仍然混合半透明边缘但核心区域 alpha≈1 Blend SrcAlpha OneMinusSrcAlpha // HLSL 程序 ... } } }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遮挡型特效。注意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决策速查表根据你的特效需求快速查找应该使用的配置组合特效类型ZWriteQueueSort 方案典型问题标准烟雾 / 气体OffTransparent (3000)By Distance Fudge多系统穿插调 sortingFudge火焰粒子OffTransparent (3000)Order in Layer烟火层次Order 分级爆炸核心火球OnTransparent-1 (2999)—不透明遮挡注意边缘 alpha 问题玻璃 / 水面OffTransparent (3000)—单面片无需排序背面渲染顺序 / 双面 Cull Off低多边形叶片 (AlphaTest)OnAlphaTest (2450)GPU 深度排序开 AlphaToMask 改善锯齿UI 血量爆发特效OffOverlay / UI (4000)Sorting Layer: UICanvas 层级配合贴地光圈 / DecalOffTransparent (3000)URP Decal ProjectorURP Decal Renderer Feature软粒子Soft ParticlesOffTransparent (3000)By Distance需在 URP Asset 开启 Depth Texture软粒子配置提示软粒子Soft Particles需要在 URP Renderer Asset 中开启Depth Texture 否则SampleSceneDepth()采样结果为 0softFade 恒为 0粒子完全透明消失。 路径Project Settings → Graphics → URP Asset → Depth Texture ✓

更多文章