尧图精选

UGUI与粒子特效层级问题详解:排序机制与六套实用解决方案

🕒 发布时间:2026/10/1 5:56:07 📁 来源:尧图网络
做Unity项目的人几乎都遇到过这个现象战斗结算时金币从宝箱里喷出来粒子特效明明挂在UI节点后面结果却被结算面板结结实实地盖住或者反过来转场特效想让粒子铺满全屏盖住所有UI它却老老实实躲在UGUI底下。我第一次被这个问题折磨是在一个卡牌项目的抽卡界面抽到金色传说全屏粒子炸开结果角色立绘和按钮全都浮在粒子上面本来该有的仪式感直接归零。那天晚上我把Sorting Layer、Order in Layer、Canvas层级翻来覆去调了四五个小时最后才发现问题根本不在这几个参数上。这篇不打算只给结论我会把UGUI和粒子特效之间这套排序逻辑讲透再把实际项目中能用、用过的几套方案连同它们的代价一起摆出来。适合刚被排序问题折磨的新人也适合已经在用某些方案但偶尔踩坑的项目组参考。1. 问题从哪来UGUI与粒子特效的渲染顺序之争要解决层级问题第一步不是改参数而是先搞明白UGUI和粒子系统在Unity渲染管线里各自是什么身份。1.1 先搞清楚UGUI的渲染真相UGUI最终会被拼成一个或多个Mesh交给Canvas Renderer去渲染。这个Mesh和你在场景里放的一个Cube本质上没有区别都是网格都需要材质和Shader。但UGUI有几个非常特殊的习惯第一UI的Shader默认在Transparent队列RenderQueue3000也就是说UI不是不透明物体它走的是透明渲染那一套流程。第二UI默认ZWrite Off不会往深度缓冲里写深度。这意味着UI不会挡住任何后来的物体也不会被早先写入的深度信息干扰前提是测试通过。第三Canvas有三个渲染模式Screen Space Overlay、Screen Space Camera、World Space。其中Overlay模式比较特殊它会把UI以屏幕空间四边形的方式在相机渲染的最后阶段直接盖到整个屏幕上这个行为几乎不受Sorting Layer影响——很多人调Sorting Order发现没反应基本都是栽在这里。1.2 粒子系统在渲染管线里的真实位置粒子系统本质上是动态生成的Mesh由ParticleSystem Renderer组件负责渲染。它和UI最大的区别在于粒子默认走场景物体的渲染流程Shader用的也是Transparent队列3000为主的透明Shader。这里出现了一个关键矛盾UI在3000队列粒子也在3000队列两者在同一个RenderQueue里那谁先谁后Unity在同一个队列内部的排序规则是先比Sorting Layer再比Order in Layer如果这些都一样就按深度、距离、实例化顺序来决定。问题在于UGUI在默认情况下并不会老老实实参与这套比较尤其是Overlay模式下Canvas的渲染有它自己的一套置顶逻辑。所以你经常会看到粒子的Sorting Order已经调成999了还是被一个默认Canvas盖住。这根本不是数值不够大的问题而是两套排序逻辑压根没有交接。1.3 解决层级问题前必须先想清楚的三个变量根据我这些年的经验处理UGUI和粒子层级之前先确认三件事能省掉后面一大半排查时间。第一Canvas是什么渲染模式。如果项目UI用的是Screen Space Overlay你就要做好心理准备常规的Sorting Layer调整很可能无效得从RenderQueue或者相机分层下手。如果是Screen Space Camera那么Sorting Layer和Order in Layer这套体系是可以正常工作的。第二粒子Shader是什么队列。打开粒子的材质看Shader面板里的Queue标签是多少。大部分默认粒子Shader是Transparent3000如果UI也是3000那它们在同一层里互相竞争如果把粒子Shader改成Overlay4000它就会在所有3000队列的物体画完之后再画稳稳地出现在UI上方。第三粒子和UI是否真的需要互相遮挡。很多效果其实只是粒子出现在UI上方一小块区域并不需要它和UI做深度上的真实交错。这种情况下用相机分层或者RenderTexture做局部叠加比改Shader要安全得多也不会导致粒子在别的地方出现奇怪的穿透。这三个变量定了方案基本就能定下来。2. 六个可用方案的适用范围与代价对照接下来把业界常用的六种方案摆在一起做个速览。每种方案我都标了适用场景和需要付出的代价方便你按项目情况选。这里先给结论后面章节会展开讲其中几个。方案适用场景是否改Shader性能代价踩坑点Sort设置Sorting Layer/Order in LayerScreen Space Camera模式下的UI 粒子排序否极低对Overlay模式无效改粒子Shader的RenderQueue为Overlay个别特效需要盖住全屏UI是低粒子之间遮挡可能乱套相机分层特效相机需要粒子在整个UI之上/之下的固定层否中多一个相机相机Clear Flags、跟随问题RenderTexture RawImage粒子作为UI局部元素被Mask裁剪否但需要额外相机渲染到RT高移动端慎用分辨率匹配问题粒子直接挂在Canvas下面UI粒子、依附UI位置移动看情况低默认还是会被UI盖需要配合改Queue或者官方UIParticle组件官方/开源UIParticle组件需要粒子与UI深度集成、被Mask裁剪、跟随UI否低~中对粒子模块支持有限某些高阶粒子能力会失效先说我最常用的判断逻辑如果只是想让某几个特效盖在UI上面改Shader Queue如果整个项目的层级结构很清晰特效层是固定的用相机分层如果粒子需要被UI的Mask裁剪让它像真正的UI元素一样工作那只能用UIParticle或RenderTexture。实际项目里最怕的是把几种方案混着用一会儿改这个Canvas的Sorting Order一会儿又改那个粒子的Queue最后整个项目层级逻辑变成一团乱麻没人敢动。3. 最推荐的Shader改写方案逐层穿透的实现与细节如果你只是需要一个特效偶尔盖住UI比如抽卡金光、升级闪光、提示数字飘字那我强烈推荐直接改粒子的Shader把RenderQueue提到Overlay。3.1 改造的核心思路RenderQueue ZTest ZWrite先讲一个容易被忽略的知识点Unity的渲染队列是严格按数字从小到大画的。不透明物体在1000-2000Transparent在3000Overlay在4000。当一个物体被放进4000队列它会在所有3000队列的东西画完之后才开始画。UGUI默认就在3000所以只要把粒子材质改成4000粒子就天然画在UI后面不是是画在UI的后面不对是画在UI的之后。后画的覆盖先画的所以粒子会出现UI上方。这里要特别注意ZTest和ZWrite的设置。粒子Shader大部分默认ZWrite Off也就是说粒子不往深度缓冲里写东西这是好事——它不会因为写入了深度而导致后面其他透明物体出现异常。但如果你遇到粒子画出来了但别的东西消失的奇怪现象先检查是不是有哪个粒子Shader开了ZWrite On把深度缓冲污染了。ZTest我建议保持LEqual不要图省事改成Always。ZTest Always意味着粒子无视深度测试无论前面挡了什么墙、什么模型它都直接画出来。这个行为在盖住UI这个需求下看起来没问题但它会连带影响粒子和其他3D物体的遮挡关系可能让你的火焰穿墙而过非常出戏。只用QueueOverlay就足以让粒子显示在UI之上不需要牺牲ZTest。3.2 一段可以直接上线的粒子Shader这里给一份我项目里常用的基础版粒子ShaderAlpha混合模式适用于大多数UI盖板特效Shader Custom/ParticleOverlay { Properties { _MainTex (Particle Texture, 2D) white {} _TintColor (Tint Color, Color) (1,1,1,1) } SubShader { Tags { Queue Overlay RenderType Transparent IgnoreProjector True PreviewType Plane } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off ZTest LEqual Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_ST; fixed4 _TintColor; struct appdata { float4 vertex : POSITION; float4 color : COLOR; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 uv : TEXCOORD0; }; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); o.color v.color; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv) * i.color * _TintColor; return col; } ENDCG } } Fallback Off }代码里的关键点只有一个Tags里的Queue Overlay这就是让粒子上浮到UI之上的核心。如果你的粒子是加色混合比如火焰、流光把Blend SrcAlpha OneMinusSrcAlpha改成Blend One One就行。如果粒子不想受项目里全局雾效影响可以加一句IgnoreProjector True上面已经有了。Shader改完之后在你的粒子材质上换成这个Shader粒子的Renderer组件什么都不用动直接在场景里跑一下应该就能看到粒子盖住UI了。多个粒子特效之间如果出现互相遮挡顺序不对的问题到每个ParticleSystem Renderer上调整Order in Layer即可RenderQueue一样的情况下Order in Layer越大越靠后画、显示越靠前。3.3 常见误区数值调了但视觉没变化这个方案我自己刚上手时也翻过车把Shader的Queue改成Overlay之后粒子确实盖住UI了但粒子本身变得透透的叠在UI上像一层薄雾颜色完全不对。后来查了一圈发现是因为粒子系统里启用了Soft Particles软粒子模块。这个模块会根据粒子与场景深度缓冲的距离来调整透明度目的是让粒子在碰到场景物体边缘时柔化过渡。但我们的UI不写深度深度缓冲里的信息和UI没关系粒子在UI上方时Soft Particles会误判粒子贴着某个物体把透明度压得很低。所以用Shader方案时记得在ParticleSystem上关掉Soft Particles模块或者不勾选Use Depth Buffer。这个坑很少被写在文档里但我猜十个用Shader方案的人里至少有三四个会遇到。另外还有一个容易忽视的问题如果项目里用了URP或者HDRPShader要对应改成URP/HDRP版本直接拿内置管线的Shader挂上去会全线变粉。URP里多半要用Shader Graph或者改Universal Render Pipeline/Particle/Unlit的RenderQueue操作思路一致但要注意不同管线对RenderQueue的入口不太一样。4. 相机分层与Depth设置被低估的“伪3D”排序法Shader方案虽然方便但有一个硬伤它改变的是粒子本身的渲染方式如果粒子需要在多个层级之间切换比如同一个特效有时在UI上方有时要被UI遮挡光改Shader就做不到了。这时候相机分层是一种更工程化的选择。4.1 双相机的搭建流程相机分层的原理很简单一个相机负责渲染场景和UI另一个相机专门渲染粒子特效通过两个相机的Depth值决定谁后画。后画的相机会覆盖先画的相机所以只要把特效相机的Depth调大粒子就会盖住UI。具体操作分四步。第一步新建一个层命名为UIParticle把需要盖住UI的粒子全部放到这一层。第二步新建一个相机命名为EffectCameraCulling Mask只勾选UIParticle层Clear Flags设为Depth OnlyDepth值设为大于UI相机比如UI相机Depth是0特效相机Depth是1。第三步主相机的设置保持不变。如果UI用的是Screen Space Camera模式那就把UICamera指定为主相机Canvas的Render Camera指向它。第四步把特效相机的Tag清空不要让它变成MainCamera避免一些第三方插件或代码获取主相机时取到错误对象。这样设置完成之后特效相机因为Depth更大会在主相机之后渲染且只渲染它看到的那一层粒子。由于Clear Flags是Depth Only它不会清空屏幕上已有的画面所以粒子是叠加在UI之上的。4.2 粒子要跟随UI元素移动时怎么处理相机分层在静态场景里很好用但一旦粒子需要跟随UI元素移动事情就变得微妙起来。比如一个升级特效粒子要从屏幕下方的按钮位置飞到屏幕中央。很多人的第一反应是把粒子物体挂在UI的某个节点下面然后让特效相机去渲染它。这在Screen Space Camera模式下是可行的因为粒子在世界空间里有实际坐标。但需要注意粒子挂到Canvas节点下之后它的世界坐标会受到Canvas的缩放影响特别是当Canvas的Canvas Scaler设置为Scale With Screen Size时Canvas下的物体世界坐标和UI的RectTransform坐标之间差了一个缩放因子。我的做法是写一个简单的坐标同步脚本在Update里把粒子的世界位置设置为UI元素的RectTransform.position然后通过RectTransformUtility.WorldToScreenPoint之类的转换计算出粒子应该在的世界坐标再赋给粒子物体。这个脚本逻辑不复杂但可以省去在Inspector里手动对齐的痛苦。4.3 相机分层方案的性能账每次提相机分层总有人担心性能。我的看法是多一个相机确实多了一份Culling和Draw Call的开销但这个开销是可以控制的。特效相机只渲染特效层而特效层里的物体数量通常远少于主场景Culling的开销很低。真正要警惕的是overdraw如果粒子特效叠加在全屏UI上移动端GPU的填充率压力会成倍增加尤其是一些高分辨率手机上全屏粒子加上全屏UI很容易掉帧。所以在实际项目中我通常建议特效相机只在需要显示特效的那一刻启用特效播放完立刻禁用或直接销毁。不要图省事让特效相机常驻常驻的后果就是你得一直为它付Culling和渲染的代价哪怕当前屏幕上根本没有粒子。5. 踩坑实录与排查链路调了层级但没反应的排查顺序最后这部分写给已经试过各种方案、但特效依然在错误层级的同学。我梳理一套自己的排查顺序按这个顺序走基本十分钟内能定位问题。5.1 五步排查法第一步确认Canvas是不是Overlay模式。如果是Overlay直接放弃Sorting Layer方案转用Queue或相机分层。这一步可以筛掉一半的问题。第二步检查粒子Shader的RenderQueue。选中粒子材质看Shader面板上的Queue显示。如果还是Transparent那它和UI在同一队列里层级主要取决于Sorting Order如果已经被某个公共Shader覆盖成不透明的2000队列那它可能被UI盖住是因为不透明物体先画了。确保粒子Shader的Queue和你的预期一致。第三步检查粒子的Renderer组件上有没有勾选Sorting Layer并设置了非常大的Order in Layer。在Screen Space Camera模式下粒子Order in Layer必须大于UI Canvas的SortingOrder否则层级还是不对。很多项目会默认把所有UI放在同一个Canvas里这个Canvas的SortingOrder一般是0你把粒子的Order调成大于0通常就能生效。第四步检查场景里有没有多个Canvas它们的SortingOrder分别是什么。多个Canvas之间的层级是严格按SortingOrder走的如果有一个隐藏的Canvas挂在某个父节点下它的SortingOrder可能覆盖了你的粒子。尤其是嵌套Canvas场景确认子Canvas的Override Sorting有没有勾选勾选之后它会脱离父Canvas的排序约束形成一个新的分支这个分支的SortingOrder可能比你预期的更大。第五步如果以上都正常但还不行那就得怀疑是不是深度缓冲的问题了。检查粒子的Shader里ZWrite是不是开了。如果开了ZWrite On粒子会向深度缓冲写入深度可能会影响后续粒子的叠加导致一部分粒子被另外一些粒子吃掉。一般来说粒子Shader保持ZWrite Off是比较安全的。5.2 一个调了设置但不生效的完整排查案例去年有个同事找我说他在UI上面加了一个护盾特效粒子材质已经换成了Overlay队列的Shader粒子也设了很高的Order in Layer但运行起来护盾还是被UI界面完全盖住。我先问了他Canvas的模式他说是Screen Space Overlay。到这里其实已经可以定位了但为了让他理解得更透我带着他把两个关键点验证了一遍。第一把Canvas临时切到Screen Space Camera模式指定主相机为Render Camera粒子的Order in Layer设为1Canvas的SortingOrder设为0护盾立刻就显示在UI上方了。这说明问题是Shader吗也不是他用的Shader已经是Overlay队列了。问题在于Overlay模式下Canvas渲染是直接在屏幕空间做的它不参与场景物体的排序体系所以Sorting Order对Overlay模式无效。而他的粒子Shader是Overlay队列其实已经比UI的3000大了理论上应该后画——但实际运行还是被盖住我让他检查了一下是不是有几个按钮上的Image也开了很高的SortingOrder或者EventSystem里勾选了强制置顶之类的选项。查了一圈发现他的UI根节点上挂着一个Canvas Group里面套了一层全屏半透明遮罩这个遮罩的Canvas也单独设置了SortingOrder为10。粒子的SortingOrder设了5虽然粒子Shader队列是4000但多个Canvas之间的层级关系优先于RenderQueue的先后顺序被遮罩盖住了。所以最后的结果是把所有全屏遮罩的Canvas SortingOrder收敛到一个统一管理脚本里粒子SortingOrder改成比所有Canvas都大问题解决。这个案例想说明一个道理层级问题往往是多种排序机制叠加出来的结果RenderQueue、SortingOrder、Canvas嵌套、遮罩层级都可能参与其中。排查的时候不要只盯一个参数最好把自己的场景按相机→队列→SortingLayer/Order→深度这几个维度逐层梳理一遍。5.3 粒子与UI Mask、GraphicRaycaster的配合问题除了显示顺序还有两类问题容易被误认为是层级问题其实它们是别的机制。一类是Mask裁剪。如果你想把粒子限制在某个UI区域内部显示比如小地图范围里的特效普通粒子Shader不会受UI Mask的模板缓冲影响粒子会无视Mask直接画到屏幕任意位置。这时候需要把粒子渲染进RenderTexture再用RawImage放进Mask里或者直接用UIParticle组件让粒子真正参与UI渲染管线。RenderTexture方案在移动端要特别注意纹理分辨率RawImage的尺寸变化时要重新分配RT。另一类是点击穿透。粒子盖在UI按钮上面时按钮还能不能点默认情况下粒子系统本身不会拦截UI的点击因为GraphicRaycaster只检测带有Graphic组件的目标。但如果粒子上挂了Collider并且场景里有PhysicsRaycaster那粒子就可能拦截到UI事件。如果你发现按钮突然点不动了先去检查粒子上是不是多了Collider。做完这些排查你会发现UGUI和粒子特效的显示层级问题其实没有那么玄学它无非是一套谁先画、谁后画、画在哪里的规则组合。搞清楚规则剩下的就是选方案了。根据我个人的习惯小项目优先用Shader方案快速见效大项目还是建议上相机分层或者UIParticle把特效层级纳入统一的框架管理后面迭代才不会越改越乱。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →