UPL9-7——优化粒子系统
UPL9-7——优化粒子系统
粒子系统为什么会带来性能消耗
-
CPU 消耗:
粒子系统中,大多数模块都是 CPU 驱动模拟的,每帧会做很多工作,比如:
粒子的生命周期管理、位置、速度、加速度、物理碰撞、回调函数调用、排序、包围盒更新、剔除、脚本交互 等等内容
这些内容需要 CPU 每帧计算,那么它自然会带来 CPU 消耗
若不同粒子之间的材质和网格不同,还会打断批处理,产生额外 DrawCall
如果不使用批处理方案,在 DrawCall 上也存在更多开销 -
GPU消耗:
粒子能被看到肯定需要渲染,那么当粒子被提交到 GPU 渲染时:
顶点阶段的计算、片元阶段的计算、纹理采样、透明混合、阴影、光照影响都会给 GPU 带来消耗
尤其是半透明粒子容易造成 OverDraw,会显著增加 GPU 负担
由于粒子系统的表现特殊性,往往需要依赖 大量粒子叠加 来表现复杂效果
如果不加控制,粒子数量增长会在 CPU、GPU 两方面同时放大开销
因此需要特别关注粒子系统的性能影响与优化
粒子系统主要优化思路
CPU 侧优化思路
CPU 侧:优化目标 —— 少算粒子
- 降低每帧要处理的粒子数
- 降低每个粒子要计算的内容
主要优化点:
- 减少并发量
- 减少工作模块
- 减少排序开销
- 减少脚本调用
- 利用剔除
- 利用对象池
- 控制 DrawCall
等等
减少粒子数量的并发
CPU 每帧都要模拟所有存活粒子的状态,比如 位置、速度、旋转、颜色、生命周期 等等
我们可以通过修改粒子系统上的一些参数来减少粒子数量的并发量
-
缩短 开始生命时间(Start Lifetime)
可以降低并发粒子数量

-
控制 发射频率(Emission 中的 Rate over Time)
可以避免过高的发射率,导致单位时间内粒子数过多

-
限制 最大粒子数 (Max Particles)
防止粒子峰值无限增长

-
使用 LOD 或 距离剔除
远处降低发射率甚至关闭
等等
减少高开销模块的使用
粒子系统中有各种各样的模块,增加模块的使用会增加计算量从而增加开销,因此对于以下高开销模块,应该谨慎使用:
-
碰撞模块(Collision)
粒子碰撞检测通常在 CPU 上进行,代价极高,我们应该能不用就不用
如果一定要使用,尽量用简单的碰撞器进行检测,并且结合碰撞层矩阵进行优化
关闭其中的 发送碰撞信息(Send Collision Messages)勾选项
-
触发器模块(Triggers)
触发需要逐粒子检测,频繁的调用回调函数
如果一定要使用,要严格限制触发条件,仅在需要时启用
-
噪声扰动模块(Noise)
每帧采样噪声函数开销大
如果一定要用,应该降低频率,减少强度
或者用随机值一次性赋值,避免每帧采样
-
拖尾模块(Trails)
拖尾效果会产生额外顶点,顶点数会呈指数级上升
如果一定要使用,可以调大其中的 最小顶点距离(Minimum Vertex Distance)
通过生命时间限制 最大轨迹长度
-
子发射器模块(Sub Emitters)
如果利用子发射器在爆炸里再爆炸会产生指数级的粒子数,
我们应该尽量不使用它,或只在关键效果触发,减少层级
等等
减少脚本和排序开销
-
脚本开销:
-
减少粒子 碰撞和触发 回调函数的使用
继承
MonoBehaviour的脚本中,可以通过以下两个函数进行粒子的碰撞和触发监听-
OnParticleCollision()(粒子开启Collision模块时会进入回调) -
OnParticleTrigger(粒子开启Trigger模块时会进入回调)
这两个函数,单个粒子都可能触发,可能造成成千上万次的调用,导致 CPU 性能开销极大
他们非常不适合大规模高发射率的粒子使用,如果一定要使用,可以结合 Unity 的多线程 Job System 使用 -
-
减少粒子组件中
GetParticles() 和SetParticles()方法的使用-
GetParticles() 会把当前存活的所有粒子数据(位置、速度、颜色、大小、剩余寿命等)拷贝到一个数组或List -
SetParticles()则把你修改后的粒子数据写回去,覆盖粒子系统中的粒子状态
每次调用都会进行托管数组和原生引起内部数据的拷贝,非常耗性能
千万不要每帧频繁调用它们,否则甚至会导致 CPU 卡死
如果一定要使用,可以结合 Unity 的多线程 Job System 使用 -
-
-
排序开销:
粒子往往都是半透明渲染的,半透明物体需要按照 从远到近 的顺序绘制,否则会出现混合错误,
所以 Unity 在渲染粒子前,CPU 需要对所有粒子进行 基于深度的排序,
这个过程就会带来开销,我们应该减少排序带来的开销-
减少需要排序的粒子数量,即减少粒子数量
-
避免排序,能使用不透明就使用不透明的粒子
-
减少排序的频率和精度
可以利用 排序偏移系数(Renderer → Sorting Fudge),解决排序不稳定的问题
等等
-
利用剔除和对象池
-
剔除:

-
模拟空间参数(Simulation Space)
设置为 Local 空间更容易整体剔除掉
-
停止动作参数(Stop Action)
表示粒子播放结束时的处理方式
一次性特效记得设为Disable 或Destroy,避免播放完还在后台模拟
如果使用了对象池,可以选Disable
等等
-
-
对象池:
频繁会使用的粒子,应该对象池化,可以有效的避免 GC 和 CPU 创建销毁的成本
减少 DrawCall
多个粒子系统尽量用相同材质或贴图图集,对于网格相同的粒子,可以使用 GPU Instancing 减小 DrawCall
GPU 侧优化思路
GPU 侧:优化目标 —— 少算像素
- 降低屏幕上计算的像素数
- 降低每个像素的计算的内容
主要优化点:
- 控制覆盖面积
- 控制着色器复杂度
- 控制几何复杂度
等等
控制 OverDraw
粒子大部分是半透明的,为了进行透明度混合,往往不会进行深度写入,导致多个粒子叠加时同一像素被反复计算,我们需要尽量减少这种计算
-
减少粒子数量和粒子尺寸
因为 粒子数量 * 粒子尺寸 决定了 覆盖面积
-
使用修剪透明度(Trim Alpha),裁剪掉贴图边缘的空透明区域
一般在美术导出图片前裁减掉无效透明区域,或用图集工具自动忽略无效透明边缘
比如一张图 512*512,但是其中的有效图形只占 128*128 的区域,完全可以把四周透明区域裁减掉 -
设置合理的 开始大小参数(Start Size)
避免大面积的满屏效果
-
使用 LOD
远处粒子直接换成雾面贴图或后处理效果的体积雾效果
等等
降低几何复杂度
粒子也需要被渲染才能被我们看到,降低几何复杂度,可以较少着色器中顶点处理部分的开销
-
使用广告牌粒子
广告牌效果一般只需要使用一个四边形即可
-
合理使用拖尾模块(Trails)
拖尾效果会把单个粒子拉成数十个顶点,会增加GPU顶点处理阶段的压力
我们可以调大拖尾效果的采样间距,限制最大顶点数 -
合适使用网格粒子
如果一定要使用网格粒子,也尽量使用低模
因为一个网格粒子可能就会包含数百顶点,会大幅增加顶点处理压力
减少片元着色开销
粒子也需要被渲染才能被我们看到,如果从渲染层面上,减少着色器中计算开销就可以降低粒子的开销
-
降低着色器复杂度
减少纹理采样,尽量用一张纹理即可,尽量避免使用法线、遮罩相关纹理
避免在片元着色器中产生分支,或使用高开销函数
大多数情况下不太需要给粒子投射阴影,受光影响也需谨慎使用 -
优化贴图
可以减少序列帧动画帧数,降低其分辨率
可以压缩贴图,移动端优先使用 ASTC、ETC2 等压缩格式
等等
替代手段
在一些表现效果中,我们应该尽量使用不那么消耗性能的手段去替代高消耗表现
比如:
-
能用不透明就不要使用半透明粒子
一些游戏中的弹壳、碎屑效果,我们可以使用几何渲染队列的粒子,不要使用半透明
-
后处理效果替代
大范围的雾、火焰,可以改为屏幕后处理效果去实现
-
使用 VFX Graph(2018.3 引入的一套基于节点的 GPU 粒子特效系统)
依赖于 SRP 管线,只支持 URP 和 HDRP 项目,内置渲染管线不支持
如果你的粒子效果有上万粒子,可以考虑使用它来制作
更多粒子系统优化方案
- 为移动应用优化粒子效果:https://learn.unity.com/tutorial/optimizing-particle-effects-for-mobile-applications
- 内置渲染管线中的粒子系统优化:https://docs.unity3d.com/6000.2/Documentation/Manual/particle-system-optimization.html
