☰
UGUI与粒子特效层级问题详解:排序机制与六套实用解决方案
2026/10/1 5:56:03 网站建设 项目流程

做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队列(RenderQueue=3000),也就是说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是Transparent(3000),如果UI也是3000,那它们在同一层里互相竞争;如果把粒子Shader改成Overlay(4000),它就会在所有3000队列的物体画完之后再画,稳稳地出现在UI上方。

第三,粒子和UI是否真的需要互相遮挡。很多效果其实只是"粒子出现在UI上方一小块区域",并不需要它和UI做深度上的真实交错。这种情况下用相机分层或者RenderTexture做局部叠加,比改Shader要安全得多,也不会导致粒子在别的地方出现奇怪的穿透。

这三个变量定了,方案基本就能定下来。

2. 六个可用方案的适用范围与代价对照

接下来把业界常用的六种方案摆在一起做个速览。每种方案我都标了适用场景和需要付出的代价,方便你按项目情况选。这里先给结论,后面章节会展开讲其中几个。

方案适用场景是否改Shader性能代价踩坑点
Sort设置(Sorting Layer/Order in Layer)Screen 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-2000,Transparent在3000,Overlay在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物体的遮挡关系,可能让你的火焰穿墙而过,非常出戏。只用Queue=Overlay就足以让粒子显示在UI之上,不需要牺牲ZTest。

3.2 一段可以直接上线的粒子Shader

这里给一份我项目里常用的基础版粒子Shader,Alpha混合模式,适用于大多数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或者HDRP,Shader要对应改成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的粒子全部放到这一层。

第二步,新建一个相机,命名为"EffectCamera",Culling Mask只勾选UIParticle层,Clear Flags设为Depth Only,Depth值设为大于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设为1,Canvas的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,把特效层级纳入统一的框架管理,后面迭代才不会越改越乱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询