
简介这份资源聚焦Unity开发中粒子特效与UI层级冲突的常见难题面向需要将粒子效果嵌入界面、并希望像UGUI一样灵活控制层级的开发者。内容围绕BakeMesh方案展开对比了RenderTexture渲染与多Canvas调整Sort in Layer两种传统思路的优劣重点说明BakeMesh可上下调整层级排序、接受Mask裁剪的特性并附有可直接运行的演示工程。压缩包共84个文件包含15个cs脚本、6个md说明文档、4个asmdef程序集定义、3个unity场景、2个prefab预制体以及shader、材质、rendertexture等资源整体约758KB结构清晰便于快速查阅。目前已有782人学习下载。通过包内Demo与UIParticle脚本读者可掌握在特效父物体上挂载组件即可完成层级管理的用法理解粒子与UI穿插场景下的排序与遮罩处理思路适合具备一定Unity基础、正在处理界面特效层级问题的开发者参考。1. BakeMesh 粒子层级问题解决为什么你的粒子特效一烘焙就“糊成一团”如果你做过 Unity 里的粒子特效烘焙大概率遇到过这个场景场景里粒子系统跑得好好的层级分明、拖尾清晰、发光粒子该亮的地方亮结果一执行 BakeMesh出来的 Mesh 要么所有粒子挤在同一个渲染层里要么拖尾和主体糊成一坨甚至部分粒子直接消失。这不是玄学这是粒子系统在烘焙到 Mesh 时层级信息丢失导致的典型问题。BakeMesh 粒子层级问题解决的核心就是搞清楚粒子系统的渲染顺序、Sorting Layer、Order in Layer 以及子发射器之间的层级关系在烘焙成静态 Mesh 时如何被正确保留或重建。这篇文章面向的是已经用过 ParticleSystem 但被烘焙结果坑过的开发者不管你是做特效优化、做性能降级方案还是要把粒子效果导出给不支持粒子系统的管线用下面的内容都能直接照着复现。2. 粒子层级到底丢在哪从 ParticleSystem 到 Mesh 的数据断点2.1 粒子渲染层级的三个来源Unity 粒子系统的层级表现实际上由三套机制共同决定很多人只盯着其中一套结果烘焙出来就翻车。第一套是 Renderer 的 Sorting Layer 和 Order in Layer。这决定了整个粒子系统作为一个渲染单元和其他 Renderer 之间的先后关系。第二套是粒子系统内部的渲染顺序由 ParticleSystemRenderer 的 Sort Mode 控制常见的有 By Distance、Youngest First、Oldest First还有按自定义轴排序。第三套是子发射器Sub Emitter和粒子之间的层级比如一个粒子死亡后触发的子发射器它的粒子应该渲染在父粒子上面还是下面。BakeMesh 做的事情是把当前帧的粒子状态快照成一个静态 Mesh。问题在于MeshRenderer 只有一个 Sorting Layer 和 Order in Layer它没有办法表达“这个 Mesh 里的三角形 A 要渲染在三角形 B 上面”这种粒子级别的层级关系。所以当你直接对 ParticleSystemRenderer 调用 BakeMesh得到的 Mesh 虽然顶点位置对了但渲染顺序信息已经丢了。2.2 BakeMesh 的 API 行为与常见误用先看一段最常见的错误写法// 错误示范直接烘焙层级信息完全丢失 ParticleSystemRenderer psr GetComponentParticleSystemRenderer(); Mesh bakedMesh new Mesh(); psr.BakeMesh(bakedMesh, true); // 第二个参数 useTransform 传 true GameObject go new GameObject(BakedParticle); MeshFilter mf go.AddComponentMeshFilter(); mf.mesh bakedMesh; MeshRenderer mr go.AddComponentMeshRenderer(); mr.material psr.sharedMaterial;这段代码能跑Mesh 也能出来但你会发现两个问题。第一所有粒子三角形在 Mesh 里的顶点顺序是按照粒子系统内部数组顺序排列的不是按照视觉层级排列的。第二如果你原本有多个粒子系统叠加每个都这样烘焙它们之间的 Order in Layer 全部变成默认值渲染顺序完全乱掉。正确的思路是烘焙之前先记录层级信息烘焙之后重建层级。具体来说需要记录每个粒子系统的 Sorting Layer、Order in Layer以及粒子系统内部的 Sort Mode 对应的实际渲染顺序。如果 Sort Mode 是 By Distance还需要记录相机位置因为烘焙后的 Mesh 是静态的相机一动原本的排序就失效了。2.3 用自定义顶点色通道保留粒子层级一个在实际项目中验证过的做法是在烘焙时把粒子的层级信息写入 Mesh 的顶点色通道或者 UV2、UV3 通道然后在 Shader 里根据这个信息做排序或者做深度偏移。// 烘焙时把粒子层级写入 UV2.x ParticleSystemRenderer psr GetComponentParticleSystemRenderer(); Mesh bakedMesh new Mesh(); psr.BakeMesh(bakedMesh, true); Vector2[] uvs bakedMesh.uv2; ParticleSystem.Particle[] particles new ParticleSystem.Particle[ psr.particleCount]; int count psr.GetParticles(particles); // 按粒子系统内部的排序规则计算层级值 // 这里以 Youngest First 为例越新的粒子层级越高 for (int i 0; i uvs.Length; i) { // 每个粒子对应若干顶点需要根据顶点数做映射 int particleIndex i / verticesPerParticle; float layerValue (float)particleIndex / count; uvs[i] new Vector2(layerValue, 0); } bakedMesh.uv2 uvs;这段代码的关键在于 verticesPerParticle 这个值。粒子烘焙成 Mesh 后每个粒子通常对应 4 个顶点两个三角形组成一个 quad但如果你的粒子用了 Mesh 类型的渲染模式顶点数就不固定了。所以实际使用时需要先根据 bakedMesh.vertexCount 和 particleCount 算出每个粒子的平均顶点数或者更稳妥的做法是遍历粒子系统的 particles 数组根据每个粒子的 position 去匹配 Mesh 顶点。参数说明uv2.x 存层级值范围 0 到 10 表示最底层1 表示最顶层。在 Shader 里可以用这个值做 Z 偏移或者配合 Stencil Buffer 做分层渲染。如果层级超过 256 层可以考虑用 uv2.x 和 uv2.y 组合成一个 16 位整数。2.4 多粒子系统叠加时的层级重建单个粒子系统的层级问题解决后多粒子系统叠加是下一个坑。假设场景里有三个粒子系统火焰、火星、烟雾。火焰在最底层火星在中间烟雾在最上层。烘焙成三个 Mesh 后你需要手动设置三个 MeshRenderer 的 Sorting Layer 和 Order in Layer。// 烘焙后重建多粒子系统的层级 public class BakedParticleLayerFix : MonoBehaviour { [System.Serializable] public class ParticleLayerConfig { public ParticleSystem ps; public string sortingLayer; public int orderInLayer; } public ParticleLayerConfig[] configs; void BakeAll() { foreach (var config in configs) { ParticleSystemRenderer psr config.ps.GetComponentParticleSystemRenderer(); Mesh mesh new Mesh(); psr.BakeMesh(mesh, true); GameObject go new GameObject(config.ps.name _Baked); go.transform.SetParent(transform); MeshFilter mf go.AddComponentMeshFilter(); mf.mesh mesh; MeshRenderer mr go.AddComponentMeshRenderer(); mr.sharedMaterial psr.sharedMaterial; mr.sortingLayerName config.sortingLayer; mr.sortingOrder config.orderInLayer; } } }这段代码的逻辑是在 Inspector 里手动配置每个粒子系统烘焙后的 Sorting Layer 和 Order in Layer然后批量烘焙。注意 mr.sortingOrder 的赋值它对应的是 Renderer.sortingOrder不是 ParticleSystemRenderer 的 orderInLayer。这两个值在粒子系统上可能不一样烘焙前要确认清楚。3. 避坑指南BakeMesh 粒子层级问题排查清单3.1 烘焙后粒子全部变成同一层现象烘焙出来的 Mesh 渲染时所有粒子看起来都在同一个深度原本前后遮挡的关系完全消失。原因BakeMesh 生成的 Mesh 把所有粒子三角形合并到一个 Mesh 里MeshRenderer 只有一个 sortingOrder无法表达粒子级别的层级。解决用 2.3 节的方法把层级信息写入顶点色或 UV 通道然后在 Shader 里根据这个值做深度偏移。如果不想改 Shader另一个办法是把每个粒子单独烘焙成一个 Mesh每个 Mesh 挂一个 MeshRenderer手动设置 sortingOrder。这个做法顶点数多的时候性能很差只适合粒子数少于 50 的场景。3.2 拖尾粒子烘焙后断裂现象Trail 模块生成的拖尾烘焙后变成一段一段的碎片或者直接消失。原因Trail 粒子的顶点生成方式和普通粒子不同BakeMesh 对 Trail 的支持取决于 Unity 版本和粒子系统的配置。部分版本里Trail 的顶点不会被 BakeMesh 正确捕获。解决如果必须烘焙拖尾先把 Trail 模块的 Ribbon Mode 改成 Strip或者用 Mesh 类型的粒子手动模拟拖尾。另一个做法是烘焙前先调用 particleSystem.Simulate 让拖尾完全展开再 BakeMesh。注意 Simulate 的参数要设成足够长的时间确保拖尾完全生成。3.3 子发射器粒子层级错乱现象父粒子死亡后触发的子发射器粒子烘焙后渲染在父粒子下面或者和父粒子混在一起。原因子发射器的粒子在 BakeMesh 时会被合并到同一个 Mesh 里原本的父子层级关系丢失。解决把父粒子系统和子发射器分开烘焙成两个 Mesh然后根据子发射器的触发时机手动设置两个 MeshRenderer 的 sortingOrder。如果子发射器的粒子是动态触发的烘焙只能捕获当前帧的状态需要考虑是否真的适合烘焙成静态 Mesh。3.4 烘焙后粒子颜色变暗或变亮现象烘焙出来的 Mesh 颜色和原始粒子效果不一致整体偏暗或者偏亮。原因粒子系统的材质可能用了 Vertex Color 或者 Custom Vertex StreamsBakeMesh 不会自动把这些数据写入 Mesh 的顶点色通道。解决烘焙后手动把粒子的颜色写入 Mesh.colors。如果粒子用了 Custom Vertex Streams需要根据 Stream 的类型把数据写入对应的 UV 通道或顶点色通道。具体做法是遍历 particles 数组读取 particle.GetCurrentColor()然后根据顶点索引映射到 Mesh.colors。3.5 烘焙后 Mesh 顶点数爆炸现象粒子数不多但烘焙出来的 Mesh 顶点数非常大导致内存和渲染压力飙升。原因BakeMesh 会把每个粒子的所有顶点都写入 Mesh包括不可见的粒子。如果粒子系统开了 Sub Emitter 或者 Trail顶点数会成倍增加。解决烘焙前先调用 particleSystem.GetParticles 获取当前活跃粒子只烘焙活跃粒子。另一个做法是烘焙后做一次顶点合并把相同位置的顶点合并掉。如果粒子数超过 1000建议不要用 BakeMesh改用 GPU Instancing 或者 VFX Graph 的方案。4. 进阶技巧用 Shader 做烘焙后粒子的层级排序4.1 在 Shader 里读取 UV2 层级值做深度偏移烘焙时把层级值写入 uv2.x 后Shader 里可以这样用// 在顶点着色器里根据 uv2.x 做深度偏移 float layerValue input.uv2.x; float depthOffset (layerValue - 0.5) * _LayerDepthScale; output.positionCS UnityObjectToClipPos(input.positionOS); output.positionCS.z depthOffset;_LayerDepthScale 是一个可调参数控制层级之间的深度差。这个值太大会导致粒子穿模太小则层级区分不明显。一般设成 0.001 到 0.01 之间根据场景的深度范围调整。4.2 用 Stencil Buffer 做精确分层如果深度偏移不够精确可以用 Stencil Buffer。每个层级分配一个 Stencil 值渲染时先渲染底层再渲染上层上层通过 Stencil 测试决定是否覆盖下层。// 底层粒子 Shader Stencil { Ref 1 Comp Always Pass Replace } // 上层粒子 Shader Stencil { Ref 1 Comp NotEqual Pass Keep }这个做法的好处是层级关系绝对精确不受深度偏移的影响。缺点是 Stencil Buffer 的位数有限通常只有 8 位最多支持 256 个层级。对于大多数粒子特效来说够用了。4.3 烘焙后粒子的动态排序如果烘焙后的 Mesh 需要根据相机位置动态调整层级可以在 CPU 端每帧计算每个粒子的深度然后更新 Mesh 的 uv2 通道。这个做法性能开销较大只适合粒子数少于 200 的场景。void UpdateLayerByCamera(Camera cam, Mesh mesh, ParticleSystem ps) { Vector3[] vertices mesh.vertices; Vector2[] uvs mesh.uv2; ParticleSystem.Particle[] particles new ParticleSystem.Particle[ps.particleCount]; int count ps.GetParticles(particles); for (int i 0; i count; i) { float depth cam.WorldToViewportPoint( particles[i].position).z; // 根据深度计算层级值越近层级越高 float layerValue 1.0f - depth / cam.farClipPlane; // 映射到该粒子对应的顶点 for (int j 0; j verticesPerParticle; j) { int index i * verticesPerParticle j; if (index uvs.Length) uvs[index].x layerValue; } } mesh.uv2 uvs; }这段代码每帧更新 uv2然后 Shader 根据 uv2 做深度偏移。注意 verticesPerParticle 需要根据实际烘焙结果计算不能硬编码成 4。4.4 一个验证层级是否正确的小技巧烘焙完成后把 MeshRenderer 的材质换成一张纯色贴图然后从侧面观察。如果层级正确你应该能看到粒子之间有明显的深度差前面的粒子会遮挡后面的粒子。如果所有粒子都在同一个平面上说明层级信息没有正确写入或者 Shader 没有正确读取。另一个验证方法是把相机设成正交投影从正上方往下看然后逐个禁用 MeshRenderer观察每一层粒子的渲染顺序是否符合预期。这个做法虽然笨但能快速定位是哪一层出了问题。我在实际项目里踩过最深的坑是烘焙前忘了记录粒子系统的 Sort Mode结果烘焙后所有粒子按顶点索引排序和原始效果完全对不上。后来养成的习惯是烘焙前先把 ParticleSystemRenderer 的所有参数打印一遍包括 sortingLayerID、sortingOrder、sortMode、renderMode确认没有遗漏再执行 BakeMesh。这个习惯帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取