UPL9-7——优化粒子系统

粒子系统为什么会带来性能消耗

  • CPU 消耗:

    粒子系统中,大多数模块都是 CPU 驱动模拟的,每帧会做很多工作,比如:
    粒子的生命周期管理、位置、速度、加速度、物理碰撞、回调函数调用、排序、包围盒更新、剔除、脚本交互 等等内容
    这些内容需要 CPU 每帧计算,那么它自然会带来 CPU 消耗
    若不同粒子之间的材质和网格不同,还会打断批处理,产生额外 DrawCall
    如果不使用批处理方案,在 DrawCall 上也存在更多开销

  • GPU消耗:

    粒子能被看到肯定需要渲染,那么当粒子被提交到 GPU 渲染时:
    顶点阶段的计算、片元阶段的计算、纹理采样、透明混合、阴影、光照影响都会给 GPU 带来消耗
    尤其是半透明粒子容易造成 OverDraw,会显著增加 GPU 负担

由于粒子系统的表现特殊性,往往需要依赖 大量粒子叠加 来表现复杂效果
如果不加控制,粒子数量增长会在 CPU、GPU 两方面同时放大开销
因此需要特别关注粒子系统的性能影响与优化

粒子系统主要优化思路

CPU 侧优化思路

CPU 侧:优化目标 —— 少算粒子

  • 降低每帧要处理的粒子数
  • 降低每个粒子要计算的内容

主要优化点:

  • 减少并发量
  • 减少工作模块
  • 减少排序开销
  • 减少脚本调用
  • 利用剔除
  • 利用对象池
  • 控制 DrawCall

等等

减少粒子数量的并发

CPU 每帧都要模拟所有存活粒子的状态,比如 位置、速度、旋转、颜色、生命周期 等等
我们可以通过修改粒子系统上的一些参数来减少粒子数量的并发量

  1. 缩短 开始生命时间(Start Lifetime)

    可以降低并发粒子数量

    image

  2. 控制 发射频率(Emission 中的 Rate over Time)

    可以避免过高的发射率,导致单位时间内粒子数过多

    image

  3. 限制 最大粒子数 (Max Particles)

    防止粒子峰值无限增长

    image

  4. 使用 LOD 或 距离剔除

    远处降低发射率甚至关闭

等等

减少高开销模块的使用

粒子系统中有各种各样的模块,增加模块的使用会增加计算量从而增加开销,因此对于以下高开销模块,应该谨慎使用:

  1. 碰撞模块(Collision)

    粒子碰撞检测通常在 CPU 上进行,代价极高,我们应该能不用就不用
    如果一定要使用,尽量用简单的碰撞器进行检测,并且结合碰撞层矩阵进行优化
    关闭其中的 发送碰撞信息(Send Collision Messages)勾选项

    image

  2. 触发器模块(Triggers)

    触发需要逐粒子检测,频繁的调用回调函数
    如果一定要使用,要严格限制触发条件,仅在需要时启用

    image

  3. 噪声扰动模块(Noise)

    每帧采样噪声函数开销大
    如果一定要用,应该降低频率,减少强度
    或者用随机值一次性赋值,避免每帧采样

    image

  4. 拖尾模块(Trails)

    拖尾效果会产生额外顶点,顶点数会呈指数级上升
    如果一定要使用,可以调大其中的 最小顶点距离(Minimum Vertex Distance)
    通过生命时间限制 最大轨迹长度

    image

  5. 子发射器模块(Sub Emitters)

    如果利用子发射器在爆炸里再爆炸会产生指数级的粒子数,
    我们应该尽量不使用它,或只在关键效果触发,减少层级

    image

等等

减少脚本和排序开销

  • 脚本开销:

    1. 减少粒子 碰撞和触发 回调函数的使用

      继承 MonoBehaviour 的脚本中,可以通过以下两个函数进行粒子的碰撞和触发监听

      • OnParticleCollision()​(粒子开启 Collision 模块时会进入回调)
      • OnParticleTrigger​(粒子开启 Trigger 模块时会进入回调)

      这两个函数,单个粒子都可能触发,可能造成成千上万次的调用,导致 CPU 性能开销极大
      他们非常不适合大规模高发射率的粒子使用,如果一定要使用,可以结合 Unity 的多线程 Job System 使用

    2. 减少粒子组件中 GetParticles()​ 和 SetParticles() 方法的使用

      • GetParticles()​ 会把当前存活的所有粒子数据(位置、速度、颜色、大小、剩余寿命等)拷贝到一个数组或 List
      • SetParticles() 则把你修改后的粒子数据写回去,覆盖粒子系统中的粒子状态

      每次调用都会进行托管数组和原生引起内部数据的拷贝,非常耗性能
      千万不要每帧频繁调用它们,否则甚至会导致 CPU 卡死
      如果一定要使用,可以结合 Unity 的多线程 Job System 使用

  • 排序开销:

    粒子往往都是半透明渲染的,半透明物体需要按照 从远到近 的顺序绘制,否则会出现混合错误,
    所以 Unity 在渲染粒子前,CPU 需要对所有粒子进行 基于深度的排序,
    这个过程就会带来开销,我们应该减少排序带来的开销

    1. 减少需要排序的粒子数量,即减少粒子数量

    2. 避免排序,能使用不透明就使用不透明的粒子

    3. 减少排序的频率和精度

      可以利用 排序偏移系数(Renderer → Sorting Fudge),解决排序不稳定的问题

    等等

利用剔除和对象池

  • 剔除:

    image

    1. 模拟空间参数(Simulation Space)

      设置为 Local 空间更容易整体剔除掉

    2. 停止动作参数(Stop Action)

      表示粒子播放结束时的处理方式
      一次性特效记得设为 Disable​ 或 Destroy​,避免播放完还在后台模拟
      如果使用了对象池,可以选 Disable

    等等

  • 对象池:

    频繁会使用的粒子,应该对象池化,可以有效的避免 GC 和 CPU 创建销毁的成本

减少 DrawCall

多个粒子系统尽量用相同材质或贴图图集,对于网格相同的粒子,可以使用 GPU Instancing 减小 DrawCall

GPU 侧优化思路

GPU 侧:优化目标 —— 少算像素

  • 降低屏幕上计算的像素数
  • 降低每个像素的计算的内容

主要优化点:

  • 控制覆盖面积
  • 控制着色器复杂度
  • 控制几何复杂度

等等

控制 OverDraw

粒子大部分是半透明的,为了进行透明度混合,往往不会进行深度写入,导致多个粒子叠加时同一像素被反复计算,我们需要尽量减少这种计算

  1. 减少粒子数量和粒子尺寸

    因为 粒子数量 * 粒子尺寸 决定了 覆盖面积

  2. 使用修剪透明度(Trim Alpha),裁剪掉贴图边缘的空透明区域

    一般在美术导出图片前裁减掉无效透明区域,或用图集工具自动忽略无效透明边缘
    比如一张图 512*512,但是其中的有效图形只占 128*128 的区域,完全可以把四周透明区域裁减掉

  3. 设置合理的 开始大小参数(Start Size)

    避免大面积的满屏效果

  4. 使用 LOD

    远处粒子直接换成雾面贴图或后处理效果的体积雾效果

等等

降低几何复杂度

粒子也需要被渲染才能被我们看到,降低几何复杂度,可以较少着色器中顶点处理部分的开销

  1. 使用广告牌粒子

    广告牌效果一般只需要使用一个四边形即可

  2. 合理使用拖尾模块(Trails)

    拖尾效果会把单个粒子拉成数十个顶点,会增加GPU顶点处理阶段的压力
    我们可以调大拖尾效果的采样间距,限制最大顶点数

  3. 合适使用网格粒子

    如果一定要使用网格粒子,也尽量使用低模
    因为一个网格粒子可能就会包含数百顶点,会大幅增加顶点处理压力

减少片元着色开销

粒子也需要被渲染才能被我们看到,如果从渲染层面上,减少着色器中计算开销就可以降低粒子的开销

  1. 降低着色器复杂度

    减少纹理采样,尽量用一张纹理即可,尽量避免使用法线、遮罩相关纹理
    避免在片元着色器中产生分支,或使用高开销函数
    大多数情况下不太需要给粒子投射阴影,受光影响也需谨慎使用

  2. 优化贴图

    可以减少序列帧动画帧数,降低其分辨率
    可以压缩贴图,移动端优先使用 ASTC、ETC2 等压缩格式

等等

替代手段

在一些表现效果中,我们应该尽量使用不那么消耗性能的手段去替代高消耗表现

比如:

  1. 能用不透明就不要使用半透明粒子

    一些游戏中的弹壳、碎屑效果,我们可以使用几何渲染队列的粒子,不要使用半透明

  2. 后处理效果替代

    大范围的雾、火焰,可以改为屏幕后处理效果去实现

  3. 使用 VFX Graph(2018.3 引入的一套基于节点的 GPU 粒子特效系统)

    依赖于 SRP 管线,只支持 URP 和 HDRP 项目,内置渲染管线不支持
    如果你的粒子效果有上万粒子,可以考虑使用它来制作

更多粒子系统优化方案

  1. 为移动应用优化粒子效果:https://learn.unity.com/tutorial/optimizing-particle-effects-for-mobile-applications
  2. 内置渲染管线中的粒子系统优化:https://docs.unity3d.com/6000.2/Documentation/Manual/particle-system-optimization.html