UE5.8 Niagara VFX 性能优化:从 Switch 2 掉帧到流畅释放技能
去年冬天,我和老周负责的一款动作游戏正赶在 Switch 2 上跑验收。角色释放一个火系大招时,画面会从稳定的 30 帧直接掉到 19 帧。音效、打击感、镜头抖动都到位了,唯独这团火焰让主机喘不上气。
我截了 RenderDoc,打开 Niagara Debugger,发现单是这一招就同时存在:GPU 模拟的烟雾拖尾、CPU 模拟的火花碰撞、没有 LOD 的爆炸核心、开启了 Scene Depth Collision 的碎片。每一项单独看都不算离谱,叠加之后就把 GPU 和 CPU 两边都占满了。
那次排查持续了两周,最后把帧率稳回 30 帧,画质损失小到美术都认不出来。这篇文章把当时踩过的坑整理成一套 Niagara 性能优化思路,适用于 UE5.8,也兼容大部分 UE5 项目。文章里每一节都尽量给出具体数字和操作路径,方便直接搬到项目里用。
一、GPU Simulation 与 CPU Simulation 的选择
Niagara 里每一类发射器都要决定模拟跑在哪。CPU Simulation 走 Game Thread 或 Niagara 自己的 CPU Sim Thread,支持复杂逻辑、碰撞、事件、样条线读取,代价是 CPU 时间和同步开销。GPU Simulation 把粒子状态存在 GPU Buffer 里,适合海量同质粒子,能跑出几十万级别,但事件、碰撞、读取外部数据都受限。
我习惯用一条简单的原则:看得见形状变化、需要跟世界互动的粒子放 CPU;只要数量和运动、形状由材质主导的粒子放 GPU。
以那个火系大招为例:
- 爆炸核心的爆点冲击波:形状关键,需要控制半径和衰减,CPU 模拟,粒子数控制在 200 以内。
- 飞舞的火星:数量多、运动简单、材质负责发光和拖尾,GPU 模拟,峰值 8k。
- 烟雾余烬:数量大、寿命长、半透明叠加严重,GPU 模拟,配合 LOD 在远处降到 1k 以下。
- 地面烧焦碎片:需要弹起和碰撞,CPU 模拟,但只在命中瞬间生成 60 个,随后快速淡出。
这个分配看起来理所应当,实际做的时候很多美术同事会图省事全部勾选 GPU Simulate,因为编辑器里帧率依然高。问题出在真机上:GPU 模拟虽然省 CPU,但会占用 Compute Shader 时间和显存带宽,低端 GPU 吃不消。
architecture-beta
group gpu[GPU 侧]
service sim_gpu[GPU Simulation] in gpu
service mat_gpu[粒子材质渲染] in gpu
group cpu[CPU 侧]
service sim_cpu[CPU Simulation] in cpu
service collision[碰撞与事件] in cpu
group world[场景数据]
service depth[Scene Depth] in world
service sm[Static Mesh] in world
sim_gpu:mat_gpu
sim_cpu:collision
collision:depth
collision:sm上图展示了两种模拟路径各自依赖的资源。GPU 模拟基本只和 GPU 资源打交道,CPU 模拟则经常需要读世界信息做碰撞和事件。选错路径会把压力带到不该去的线程。
从量化角度看,单帧 GPU 模拟开销可以近似看成下面这个式子:
其中 是发射器里实际分配的粒子容量, 是当帧存活粒子, 是单个粒子的模拟常数, 是渲染常数。很多人只盯着 ,其实 在 GPU 模拟里会一直占用 Buffer,空槽位也会参与 Dispatch,所以容量开得太大同样浪费。
CPU 模拟的瓶颈则在于同步和查询。GPU 模拟只需要把 Buffer 交给渲染管线,CPU 模拟则要把每帧结果写回 Game Thread 或 Render Thread,这里有一道隐形的线程墙:
是 Scene Query 的单次开销,如果大量粒子同时做 Sweep 或 Line Trace,这一项会指数级放大。
二、粒子数量、发射率与生命周期的控制
火系大招掉帧的直接原因之一是单次 Burst 太大了。美术为了保证爆炸瞬间的饱满感,把 Burst Count 拉到 5000,生命周期 3 秒。结果 3 秒内每一帧都在处理 5000 个粒子的模拟和半透明排序,Switch 2 的 GPU 根本顶不住。
我的做法是把粒子数拆成三层:
- 核心爆点用少量大粒子,靠材质的高光、扭曲、自发光撑体积。200 个粒子就够了。大粒子虽然单个面积大,但数量少,排序和模拟压力都小。
- 填充层用中小粒子,承担颜色和密度。1500 个,生命周期 1.2 秒。这一层主要让玩家从视觉上觉得火球很厚实。
- 细节层用 GPU 模拟的火星和余烬,数量可以上万,但生命周期短到 0.4 秒。
三层叠加,玩家眼睛里依然是炽热的火球,但存活粒子数从 5000 压到 1700 左右。
发射率方面有一个常见误区:Spawn Rate 很高但 Lifetime 很短,总存活数似乎可控。问题在于 GPU 粒子的 spawn 也有固定开销,每一帧高频生成会触发大量粒子初始化,尤其在 GPU 模拟里 Initial Velocity、Initial Size、Color 都要在 Compute Shader 里算一遍。
我通常把关键数值限制在三个阈值内:
- CPU 模拟的发射器,单帧新增不超过 200 个。
- GPU 模拟的发射器,单帧新增不超过 4000 个。
- 同屏所有 Niagara 系统的总存活粒子数,Switch 2 上控制在 10000 以内,高端 PC 可以放宽到 50000。
生命周期不是越短越好。太短会让效果闪烁、廉价;太长会拖慢淡出阶段的资源占用。比较好的做法是:核心效果 0.5 到 1.2 秒,余烬和烟雾 1.5 到 3 秒,环境氛围效果 5 到 10 秒但必须配合 LOD 和 Culling。
三、LOD for VFX
Niagara 的 LOD 和 Static Mesh 的 LOD 思路一样:离相机远或者占屏幕像素小的时候,降低复杂度。只是很多人做特效时只在粒子数量上做 LOD,其实可以降级的东西很多。
一个完整的 VFX LOD 方案通常包括:
- 粒子数按距离阶梯下降。
- 材质复杂度降低,远处取消扭曲、次表面散射、深度偏移。
- 模拟频率降低,远距离使用 Half Resolution 或更新间隔拉长。
- 取消碰撞和事件,远距离粒子只做运动。
- 半透明叠加模式从 Additive 改为 Modulate 或完全取消。
flowchart TD
A[相机与特效距离] --> B{距离 < 5m?}
B -->|是| C[LOD0: 全量粒子+材质+碰撞]
B -->|否| D{距离 < 20m?}
D -->|是| E[LOD1: 粒子减半+简化材质]
D -->|否| F[LOD2: 仅保留核心轮廓+取消碰撞]上图是我们在火系大招里用的 LOD 决策流程。近距离看,火焰有扭曲、有火星弹跳、有光晕;20 米外只剩一团简化的发光体,但玩家几乎注意不到。
Niagara 系统设置里,LOD Distance 默认是线性的,我一般改成按屏幕大小驱动。因为同样 10 米远的特效,在角色正前方占屏幕 30% 和在屏幕边缘占 3%,观感完全不一样。UE5.8 支持通过 Niagara LOD 的 Screen Size 模式做这件事,设置后 LOD 切换会更符合实际像素开销。
LOD 切换时最忌讳跳变。如果一个火球在 19.9 米时还是全特效,到 20 米突然只剩一个光点,玩家会明显察觉。我的做法是在不同 LOD 之间做交叉淡出,或者让粒子数和材质参数随距离连续变化。这样即使切换阈值很激进,也不会穿帮。
LOD 还能和平台的 Scalability 绑定。Switch 2 上直接把最远 LOD 的触发距离缩短一半,让远处的特效更快进入简化状态。这部分后面会细讲。
四、Bounds 与 Culling
Niagara 系统默认会自动计算 Bounds,但自动 Bounds 在粒子快速移动、生命周期长或者范围很大的情况下很容易失控。Bounds 太大,会导致它始终处于相机视锥内,即便特效已经小到看不清,也拒绝被 Culling。
我们当时的问题是:火系大招的火星被弹飞到 20 米高,自动 Bounds 被拉到很大。角色释放技能后跑出一段距离,Bounds 仍然覆盖屏幕边缘,粒子系统持续模拟和渲染。把 Bounds 改成手动固定范围后,同屏活跃 Niagara 系统数下降了 18%。
设置 Bounds 时需要注意:
- 固定 Bounds 要在发射器里关掉 Auto Calculate Bound,手工输入一个 Box 或 Sphere 范围。
- Bounds 范围要覆盖特效最大扩散范围,但别留太多余量。
- 对于高速移动的粒子,比如箭矢尾迹,Bounds 中心要跟随发射器本身,而不是让历史粒子把它越拉越长。
- 多发射器系统里,每个发射器单独设 Bounds,避免一个拖尾的粒子把整体 Bounds 撑大。
sequenceDiagram
participant Cam as 相机视锥
participant Bounds as Niagara Bounds
participant Sim as 模拟/渲染
participant Cull as Culling 决策
Cam->>Bounds: 每帧检查相交
Bounds->>Cull: 返回可见性
Cull->>Sim: 不可见时暂停模拟
Cull->>Sim: 可见时恢复模拟
Sim->>Cam: 输出可见粒子这个时序图说明 Bounds 和 Culling 的协作关系。Bounds 只是几何盒,真正的 Culling 由渲染层决定。UE5.8 里 Niagara 支持一种叫 Sim Cull 的机制:当 Bounds 不在视锥内时,可以暂停模拟,而不是每帧都更新位置、速度和颜色。这对长生命周期特效尤其有用。
有个坑要避开:Bounds 不能设得太小。如果粒子实际超出了 Bounds,会出现渲染裁切,也就是火星飞到一半突然消失。调试时可以在 Niagara Debugger 里打开 Draw Bounds,逐个检查是否包裹严实。
五、材质复杂度对粒子的影响
材质是 Niagara 特效最容易被低估的瓶颈。一个粒子可能只有几百个,但如果材质里有复杂 Shader,照样能拖垮帧率。
我当时截帧分析,火系大招里有一个烟雾材质用了:
- 三张噪点纹理做扭曲
- 深度偏移
- 折射
- 次表面散射
- 动态参数控制颜色
在 PC 上看起来不错,Switch 2 上这一个材质就占了 1.8ms GPU 时间。最后改成:
- 一张噪点纹理加一张流程图
- 保留深度偏移但降低偏移量
- 取消折射,改成简单 Additive
- 取消次表面散射
改动后材质耗时降到 0.4ms,画面差别在 720p 屏幕上几乎可以忽略。
判断材质复杂度的几个维度:
- 纹理采样次数。每多一次采样就多一次显存读取。
- Shader Instruction 数量。Niagara 材质默认 Unlit,如果改成 Lit 或加入自定义光照模型,指令数会暴涨。
- 透明排序。半透明粒子需要按深度排序,粒子越多排序开销越大。
- Overdraw。大量 Additive 粒子叠在一起会反复读写同一个像素,Switch 2 这类带宽有限的机器最怕这个。
降低材质复杂度的具体做法:
- 远处 LOD 用单张纹理。
- 取消不必要的 World Position Offset。
- 少用 Custom Depth 和 Scene Color。
- 把多个纹理合并到一张 Atlas 里,减少采样次数。
- 用材质实例做参数化,而不是每个特效都写一个新材质。
graph LR
A[粒子材质] --> B[纹理采样]
A --> C[Shader 指令]
A --> D[Overdraw]
B --> E[显存带宽]
C --> F[ALU 开销]
D --> G[像素填充]
E --> H[Switch 2 瓶颈]
F --> H
G --> H这个图把材质复杂度拆解成三个主要开销项,最终都汇聚到 Switch 2 的瓶颈上。优化时优先砍带宽和填充,通常收益最大。
六、Collision 与 Scene Query 的开销
碰撞是 Niagara 特效里最贵的 CPU 特性之一。火系大招最初的版本给每个火星都开了 Collision,用 Scene Depth 做碰撞查询。Switch 2 上 Release 版本的 Scene Depth 分辨率是 720p,读一次还好,但 2000 个火星同时读,直接把 Render Thread 占满了。Release 版本下 Scene Depth 读取还有额外的同步开销,实际比编辑器里测出来的数字更糟。
我后来把碰撞分层处理:
- 200 个核心火花做精确 Collision,用 CPU 模拟加 Sphere Sweep。
- 1800 个填充火星不做碰撞,靠 Initial Velocity 和 Gravity 模拟下坠到地面的效果。
- 火星子系统只在生成后前 0.3 秒启用碰撞,之后关闭。
Collision 的查询方式也分几种成本:
- Scene Depth Collision:最便宜,因为 Depth Buffer 已经存在,但只适合做地面弹跳,精确度低。
- Plane Collision:固定平面,几乎零开销,适合做地面。
- Mesh Collision:用 Static Mesh 的物理代理,中等开销。
- Physics Collision:调用 Chaos 物理,最贵,但可以做复杂反弹。
选择原则是:能不用 Collision 就不用,能用 Plane 不用 Mesh,能用 Depth 不用 Physics。
另外 Collision 的 Query Frequency 可以调低。Niagara 默认每帧查询,改成每隔 2 到 4 帧查询一次,对高速运动的火星影响不大,但 CPU 时间能省一半。这个设置在 Collision Module 的 Query Frequency 参数里。
还有一种开销叫事件传播。CPU 模拟里,一个粒子发生碰撞后可以生成 Event,让其他发射器响应。Event 很灵活,但如果 Event 数量大,Game Thread 会忙于分发。我的做法是:单个粒子系统每帧事件数不超过 50 个,超出部分直接丢弃。
七、Niagara Scalability 与平台降级
UE 的 Scalability 系统允许不同画质等级加载不同参数。Niagara 在 UE5.8 里对 Scalability 的支持更细,可以在发射器级别根据平台覆盖 Spawn Count、Lifetime、LOD Distance、材质参数等。
我们项目分了四档:
- PC High
- PC Medium
- Switch 2 Docked
- Switch 2 Handheld
Docked 和 Handheld 虽然都是 Switch 2,但分辨率一个是 1080p 一个是 720p,GPU 预算差了一倍。Handheld 模式里我们把 Niagara 的全局预算降到 Docked 的 60%。
具体做法是改 Niagara Scalability 设置:
[/Script/Engine.NiagaraSettings]
+ScalabilitySettings=(PlatformSet={Mobile},QualityLevels=((QualityLevel=0,bEnableNiagara=True,MaxEmitterCount=8,MaxSystemCount=32,MaxParticleCount=4096)))
+ScalabilitySettings=(PlatformSet={Switch},QualityLevels=((QualityLevel=0,bEnableNiagara=True,MaxEmitterCount=12,MaxSystemCount=48,MaxParticleCount=8192)))
+ScalabilitySettings=(PlatformSet={Desktop},QualityLevels=((QualityLevel=0,bEnableNiagara=True,MaxEmitterCount=24,MaxSystemCount=128,MaxParticleCount=65536)))这段 ini 在 50 行以内,限制了不同平台的最大发射器数、系统数和粒子数。它不会让特效自动变简单,而是给 Niagara 一个全局预算上限,超出时优先裁剪距离远、优先级低的系统。
更细粒度的控制是在 Niagara System 的 System Properties 里,找到 Scalability Overrides。可以为每个画质等级单独覆盖:
- Max Instances:同屏最多几个实例。
- Max Time Without Rendering:脱离渲染多久后停止模拟。
- LOD Distance Scale:LOD 切换距离缩放。
- Cull Distance:多远之外直接不渲染。
我们给火系大招在 Handheld 模式做了这些覆盖:
- Max Instances 从 4 降到 2。
- LOD0 触发距离从 5 米降到 3 米。
- 取消最远 20% 的火星发射器。
- Cull Distance 从 50 米降到 35 米。
这些调整让 Handheld 模式的大招 GPU 时间从 8.2ms 降到 4.1ms,帧率回到 30 帧。
architecture-beta
group platform[平台层]
service pc[PC High] in platform
service switch_docked[Switch Docked] in platform
service switch_hand[Switch Handheld] in platform
group scalability[Scalability 层]
service global[Niagara 全局预算] in scalability
service lod[LOD 距离缩放] in scalability
service cull[Cull Distance] in scalability
group vfx[VFX 层]
service fire[火系大招] in vfx
service smoke[烟雾] in vfx
service spark[火星] in vfx
pc:global
switch_docked:global
switch_hand:global
global:lod
lod:cull
cull:fire
cull:smoke
cull:spark这个架构图展示了平台层、Scalability 层和具体 VFX 层的映射关系。每一档平台先决定全局预算,再通过 LOD 和 Cull 作用到单个特效上。
八、常用优化检查清单
最后把上面这些整理成一份检查清单,每次验收特效前过一遍:
模拟路径
- [ ] 核心形状控制放在 CPU 模拟。
- [ ] 大量同质粒子放在 GPU 模拟。
- [ ] 混合模式粒子避免 CPU/GPU 之间频繁同步。
- [ ] GPU 模拟发射器的 Max Particles 没有过度分配。
数量与生命周期
- [ ] 单帧新增粒子数:CPU < 200,GPU < 4000。
- [ ] 同屏存活粒子数:Switch 2 < 10000,PC < 50000。
- [ ] 生命周期分档:核心 0.5-1.2s,填充 1.5-3s,氛围 5-10s 但带 LOD。
- [ ] Burst 数量按距离和平台分级。
LOD
- [ ] 至少设置 3 级 LOD。
- [ ] LOD 切换由屏幕大小驱动,而不是纯距离。
- [ ] 远距离取消碰撞、折射、次表面散射。
- [ ] LOD 切换有交叉淡出或连续过渡。
Bounds 与 Culling
- [ ] 关键特效使用固定 Bounds。
- [ ] Bounds 包裹实际粒子范围,不留过大余量。
- [ ] 开启 Sim Cull,不可见时暂停模拟。
- [ ] Niagara Debugger 中确认 Bounds 显示正常。
材质
- [ ] 材质纹理采样数不超过 3 张。
- [ ] 远距离 LOD 材质取消扭曲和折射。
- [ ] 避免在半透明粒子材质里使用 Lit 模型。
- [ ] 检查 Overdraw,密集区域用 Modulate 替代 Additive。
碰撞与事件
- [ ] 能用 Plane Collision 就不用 Mesh。
- [ ] 能用 Scene Depth 就不用 Physics。
- [ ] Collision Query Frequency 调到 2-4 帧一次。
- [ ] 单个系统每帧事件数不超过 50。
Scalability
- [ ] 各平台 Niagara 全局预算已配置。
- [ ] 每个 Niagara System 有平台覆盖参数。
- [ ] Handheld 模式额外降低 LOD 距离和 Cull Distance。
- [ ] 验收时使用目标平台真机,而不是只看编辑器。
结语
火系大招最后能在 Switch 2 上稳定跑 30 帧,靠的不是某一个神级优化,而是把每个环节的预算都算清楚。粒子数、生命周期、模拟路径、材质、碰撞、LOD、Bounds、Scalability,每一项都只砍掉一点,整体就能省出一半时间。
做 Niagara 优化最怕两种极端:一种是完全不管,靠着 PC 性能硬撑;另一种是过度优化,把特效砍得毫无表现力。两者都会让项目在最后阶段吃苦头。真正有用的做法是先拿 RenderDoc 和 Niagara Debugger 把瓶颈找出来,再决定从哪下手。预算有限,钱要花在刀刃上。优化到最后你会发现,性能和美感的平衡点,往往比想象中更靠近简洁那一侧。粒子少了,单颗粒子反而能更醒目。