Niagara 美术优化规范(通用版)
Niagara 美术优化规范(通用版)
0. 为什么需要这份规范
在大多数因 Niagara 导致性能问题的项目中,常见现象并不是"GPU 模拟太慢",而是:
- 渲染线程长时间阻塞在等待 Niagara 系统创建渲染缓冲(
WaitForGatherDynamicMeshElements); - 单帧内大量
CreateRHIBuffer调用; - 关卡里同一资产被复制粘贴几十上百个实例;
- 视野外、远距离的 Ambient 系统没有被正确剔除,每帧仍在渲染线程上分配显存缓冲。
结论:Niagara 系统本身的 GPU 模拟开销通常并不大,真正的代价在"每个组件每帧都向渲染线程交活"上。 解决路径是 资产规范 + 运行时分级(Significance / Scalability) 双侧推进。本规范是美术资产侧的底盘。
美术侧每条规范都对应一类实测可观察的症状。 不是教条,而是项目里反复吃过的亏。
1. 性能预算(参考值)
下表给出的是 30 FPS / 33 ms 帧预算下的常见参考值。不同项目应根据目标平台与画面规模微调,任何超标都需要在 PR/提交时附上理由 + TA 签字。
| 项目 | 参考预算 | 说明 |
|---|---|---|
| 单关卡同屏活跃 NiagaraComponent 数 | ≤ 30 | 100+ 必然出问题 |
| 单系统 Emitter 数 | ≤ 4 | 超过强制走多 NS 拆分;emitter 过多直接放大 CreateRHIBuffer 次数 |
| 同一资产在场景中的实例数 | ≤ 30 | 超过务必走"内部多发射点"或 ISM/HISM 思路 |
| Ambient(环境氛围)系统单实例最大粒子数 | CPU ≤ 200,GPU ≤ 1000 | |
| Gameplay(招式/技能/拾取)系统单实例最大粒子数 | CPU ≤ 500,GPU ≤ 2000 | |
| 单系统 GPU 模拟时间 | ≤ 0.1 ms | 用 fx.Niagara.Stats 1 验证 |
| Distance Cull 距离(Ambient) | 40 m(4000 cm) | 默认无限是大坑 |
2. 命名规范(强制)
命名是规范的入口——错了名字,下面所有 EffectType / Scalability 自动化全部不命中。
| 前缀 | 含义 | 必须挂的 EffectType 类别 |
|---|---|---|
NS_Ambient_* | 环境氛围、不可交互、与玩法解耦 | Ambient/Background |
NS_Item_* | 拾取物、道具高亮 | Item |
NS_Skill_* / NS_Player_* / NS_Enemy_* | 玩法关键、招式命中、技能 | Gameplay Critical |
NS_Cinematic_* | 过场专用 | Cinematic |
NS_UI_* | UI 上的特效 | UI |
常见反面教材:
- 拼写错误(如
NS_Ambeint_*)——自动化扫描漏掉,资产看起来挂着分类,实际未被规则覆盖 - 没有任何分类前缀——无法挂 EffectType,全靠人记
- 把过场用的烟取名
NS_Ambient_CinematicSmoke——会被 Ambient 自动化误杀
自检: Content Browser 搜索 NS_ 看是否所有资产都有合规前缀。
3. NiagaraEffectType(强制)
每个 NiagaraSystem 必须挂 EffectType。EffectType 控制全局可扩展性策略,不挂等于裸奔。
3.1 项目应预置的 EffectType
| EffectType 用途 | 推荐字段 |
|---|---|
| Ambient / Background | Update Frequency = Low,Cull Reaction = Deactivate Immediate |
| Item | Update Frequency = Low/Medium,Cull Reaction = Deactivate Immediate |
| Gameplay Critical | Update Frequency = High,Cull Reaction = None / Pause Resume |
| Cinematic | Update Frequency = High,剧情期间不可剔除 |
| UI | Update Frequency = Low,配合 UI 渲染层级单独管理 |
3.2 EffectType 关键字段约定
| 字段 | Ambient/Item/UI 推荐值 | Gameplay/Cinematic 推荐值 | 解释(人话) |
|---|---|---|---|
Update Frequency | Low | High | Ambient 类按低频 tick 更新,节省 CPU |
Cull Reaction | Deactivate Immediate | Pause Resume | 这条最重要:Ambient 出视野立即关掉发射器,停止创建渲染缓冲;Gameplay 只暂停以便重新出现 |
不要把 Ambient 设置成
Pause Resume。Pause 状态下系统仍然在渲染线程持有 Render Proxy,每帧仍然会调GetDynamicMeshElements,渲染线程阻塞就是这么来的。
3.3 SystemScalabilitySettings(EffectType 内的距离/数量剔除)
由 TA 在 EffectType 资产 Details 面板里一次性配置。参考值:
Cull By Distance = true
Max Distance = 4000 cm (Ambient) / 8000 cm (Item) / 不勾 (Gameplay)
Cull Max Instance Count = true
Max Instance Count = 10 (Ambient) / 20 (Item) / 不限 (Gameplay)
含义: 同一类 Ambient FX 全场景同时只渲染最近的 N 个,超出的按距离自动剔除。
4. Bounds(包围盒)规范
Niagara 默认每帧动态计算包围盒,CPU 消耗单个不大,但成百上千个实例叠起来很可观。
规则
- 所有 Ambient 类 NS 必须开 Fixed Bounds:
System Properties → Bounds Mode = Fixed,初值 ±500 cm 立方 - Gameplay 类(飞行体、招式)若移动幅度不可预知,可用 Dynamic,但单帧粒子数必须 ≤ 1000
- 若某 Ambient NS 视觉被裁切(如大范围暴风雪),单独把那个 NS 改回 Dynamic,并写在资产说明里"此处保留 Dynamic Bounds,因为 X"
自检
打开 NS 资产 → 顶部预览窗口右键 → Show → Bounds,看绿框是否合理(不要远超粒子可见范围)。
5. Emitter 数量与渲染器(强制)
为什么单个 NS 一帧能触发几十次 CreateRHIBuffer?因为一个 NS 里塞了太多 emitter,每个 emitter 都有自己的 Renderer,每个 Renderer 都要分配自己的 Vertex/Index Buffer。
规则
- 单 NS 最多 4 个 emitter。多于 4 个时:
- 视觉相近的合并到一个 Mesh/Sprite Renderer 里(共享同一份 Material + Buffer)
- 或拆成多个独立 NS,让 EffectType 各自管
- 同一 NS 里相邻 emitter 优先共用 Material 和 Mesh:合并后引擎可以复用 buffer,buffer 创建次数才会塌下去
- 避免在 Ambient 类里使用 Ribbon Renderer:Ribbon 每帧都在重建顶点,是渲染线程负担最重的渲染器
一个判断流程
Emitter ≤ 4 ? ── 是 ──→ OK
└─ 否 ──→ 是不是有 emitter 的视觉差别 < 30%?
├─ 是 ──→ 合并到同一 Renderer
└─ 否 ──→ 拆成多个 NS,分别走 EffectType
6. 粒子参数规范
| 参数 | Ambient 推荐 | Gameplay 推荐 | 备注 |
|---|---|---|---|
Spawn Rate | ≤ 20 / 秒 | 视玩法 | 烟雾/水雾不需要每秒 100 个 |
Lifetime | ≤ 2 秒 | 视玩法 | 越长,同时存在的粒子越多 |
Max Particles | ≤ 200 (CPU) / 1000 (GPU) | ≤ 500 (CPU) / 2000 (GPU) | 必须设上限,不要留默认无限 |
Sim Target | CPU(Ambient 数量小)/ GPU(数量大) | 视场景 | GPU 更便宜但有上传开销 |
Local Space | 开(除非粒子需要在世界中拖尾) | 视玩法 | 关闭意味着每个粒子位置都需世界变换 |
Lifetime 留 5 秒、SpawnRate 100/秒 的默认 Ambient 是粒子总数失控的最常见原因。
7. 关卡放置规范
实际项目里很常见:美术为了视觉一处一个手放 NS,结果一张图里几十上百个实例全部走渲染线程。
规则
- 同一 NS 在同一关卡(World Partition Cell 内)实例数 ≤ 30
- 大于 30 时必须用 ISM/HISM 同概念合并:
- 优先方式:单个 NS 放一个,把"多个发射点"做成 NS 内部的 Spawn Module(用 Spawn Per Unit / Spawn Locations List)
- 次选方式:把多个 NS 实例放进同一个 Niagara Actor 蓝图,由 BP 在距离外统一 Deactivate
- 必须挂 EffectType 才能放进关卡——没挂的 Niagara Actor 在 PR Review 直接打回
- 过场专用 NS 不要起
NS_Ambient_前缀,会被自动化误杀
关卡审查 checklist
8. CPU vs GPU 模拟选择
| 选 CPU | 选 GPU |
|---|---|
| 粒子数 ≤ 200 | 粒子数 > 500 |
| 需要触发 Gameplay 事件(命中、Cue) | 纯视觉,不与玩法交互 |
| Ribbon 拖尾、Mesh Renderer | 大批 Sprite |
| 需要 Collision(CPU 路径精度更高) | 大数据量、对精度宽容 |
GPU 路径便宜在模拟,贵在每帧的 buffer 上传与同步。单个
NiagaraGPUSimulation自身耗时通常很低,但它在渲染线程的同步阻塞(WaitForGatherDynamicMeshElements)会把整个 RT 拖垮。所以少量 GPU 系统比大量 CPU 系统更危险。
9. 提交前自检 Checklist
每个新建/修改 Niagara 资产,提交版本控制前自查:
□ 命名前缀正确(§2)
□ 挂上对应 EffectType(§3)
□ Update Frequency / Cull Reaction 与 EffectType 类别匹配
□ Ambient 类已开 Fixed Bounds(§4)
□ Emitter ≤ 4(§5)
□ Max Particles 已设上限,未留默认无限(§6)
□ Lifetime ≤ 2 秒(Ambient)
□ 在编辑器视口测试:相机拉远 40m 后,stat Niagara → Active Count 下降
□ 在编辑器视口测试:Cull 触发时,仪表盘看不到 GetDynamicMeshElements 调用
□ 关卡放置实例数 ≤ 30(§7)
10. 验证方法
在编辑器或 PIE 里跑下面这些命令,每条命令对应规范的一类问题:
| 命令 | 看什么 | 健康范围 |
|---|---|---|
fx.Niagara.Stats 1 | Active / Culled NiagaraComponent 数 | Active ≤ 30,Culled 远 > Active |
stat Niagara | NiagaraSystem 模拟时间 | 所有 system 总和 < 2 ms |
stat unit | GameThread / RenderThread / GPU 时间 | RT < 20 ms(30 FPS 目标) |
stat gpu | NaniteVisBuffer / Translucency | 不要因 VFX 改动飙升 |
优化后的对比方法: 在做规范优化前后,对同一关卡、同一相机位置各采一次 Insights trace,对比下表关键指标的变化:
| 指标 | 关注重点 |
|---|---|
WaitForGatherDynamicMeshElements(RT 阻塞) | 是否显著下降 |
GameThreadWaitForTask | 是否同步降低 |
| 同屏活跃 NiagaraComponent | Active 数显著下降,Culled 数显著上升 |
| 平均帧时 / FPS | 直接观感 |
注:美术资产侧的优化通常能拿回大头,但要把整体 RT 压到 30 FPS 目标之内,通常还需要程序侧的 Significance/Scalability 接管 Niagara 组件。本规范的目标是把"美术资源能控的部分"全部做到位。
11. 常见反面教材
| 症状 | 问题根因 | 正确做法 |
|---|---|---|
单帧内同一 NS 触发 20+ 次 CreateRHIBuffer | emitter 过多 | 拆成 ≤ 4 emitter,相邻视觉合并 Renderer |
| 同一资产在关卡内放置 80+ 个实例 | 手放泛滥,未走 ISM 思路 | 单 NS 内做多发射点,关卡只放 1 个;或合并 + 距离 cull |
命名拼写错(如 Ambeint) | 自动化漏 | 改回正确前缀 |
无前缀分类(如 NS_BubbleOfSoup) | 无法挂 EffectType | 改名加上分类前缀 |
| 任何无 EffectType 的 Niagara Actor | 视野外仍然占 RT | 挂 EffectType,CullReaction = Deactivate Immediate |
12. FAQ
Q1:把 Cull Reaction 设成 Deactivate Immediate 后,玩家走近时粒子要重头开始播,会不会突兀?
A:会。所以只对 NS_Ambient_* 这类持续循环、视觉连续的 FX 用 Deactivate Immediate。一次性 Cinematic / 招式 FX 用 Pause Resume 或 None。
Q2:开了 Fixed Bounds,但有时候粒子从镜头外飘进来时被裁掉了,怎么办?
A:把 Fixed Bounds 调大(±1000 ~ ±2000 cm),或者针对该 NS 单独改回 Dynamic Bounds。不要全局回退。
Q3:单 NS 4 个 emitter 不够用怎么办?
A:99% 的情况是视觉相近的 emitter 没合并。打开两个 emitter 看 Renderer:用同一份 Material + Mesh / Sprite 时直接合并,差异通过 Particle Attribute 在 Material 里区分。
Q4:做的 Ambient 在视野外没消失怎么办?
A:检查 4 项:① 资产命名前缀是否 NS_Ambient_;② 是否挂了 Ambient 类 EffectType;③ EffectType 的 Cull Reaction 是否 Deactivate Immediate;④ EffectType 的 Cull By Distance 是否勾选且 Max Distance 合理。任一项漏掉都会失效。
Q5:程序说要做 Significance/Scalability 接 Niagara,那美术还要做这些吗?
A:要。运行时分级是动态调度,规范是资产侧底盘。资产乱掉时,运行时再聪明也只能把烂资产剔得稍微好一点。两侧叠加才能拿到目标帧率。
附录 A — 资源侧批处理思路(TA 视角)
本附录面向 TA / 工具人,美术不需要逐字读。给出一个可复用的资源侧批处理方案,适合在项目早期一次性把"机械活"做完。
A.1 适用场景
项目里已存在大量 Niagara 资产,但 EffectType 和 Fixed Bounds 都没规范挂上。手动逐个改不现实,需要写一个一次性的 Editor Python 脚本。
A.2 脚本应做的事
- 若目标 EffectType(如
FXT_Ambient_Background)不存在,自动创建并配置:UpdateFrequency = LowCullReaction = DeactivateImmediate
- 扫描指定目录下匹配命名前缀的 NiagaraSystem,绑定到该 EffectType
- 对每个命中的 NS 启用 Fixed Bounds(±500 cm 立方),消除每帧 CPU 包围盒重算
- SaveAsset 触发版本控制自动 checkout
A.3 脚本不应做、需要手工补的事
SystemScalabilitySettings数组里的 Distance Culling / MaxPerInstance 配置(嵌套 USTRUCT,脚本化易碎,详细原因见 A.6)- 逐 NS 的 Spawn Rate / Lifetime / Max Particles 调参(需要美术判断视觉等价)
- 多 emitter 合并(需要美术在 Niagara Editor 里合并视觉可等价的 emitter)
- 场景实例数削减(必须由关卡美术判断保留哪些)
A.4 推荐执行顺序
Step 1: 在 Unreal Editor 连接 Source Control
Step 2: 用 DRY_RUN 模式预览(脚本默认应开启 DRY_RUN = True)
观察 Output Log,确认命中的 NS 列表符合预期
Step 3: 关闭 DRY_RUN,正式执行
观察日志确认每个 NS 修改项,SaveAsset 成功后自动 checkout
Step 4: 手工补齐 EffectType 的 SystemScalabilitySettings(编辑器 Details 面板)
Step 5: 关卡侧削减实例数(关卡美术配合)
A.5 回滚
| 场景 | 操作 |
|---|---|
| 单个 NS 配置需回退 | Content Browser 右键该 NS → Source Control → Revert |
| 整批回退 | 版本控制工具批量 revert 对应目录 |
| 删除新建的 EffectType | 编辑器里右键 Delete 并 revert |
| 脚本本身 | 版本控制工具 revert |
A.6 为什么 SystemScalabilitySettings 不写进脚本
把 SystemScalabilitySettings 列为"手工补"是有意为之,原因如下:
1. TArray<FNiagaraSystemScalabilitySettings> 在 Python 里有"取出 → 修改不回写"的坑
Python 从 USTRUCT array 里 get 出来的 entry 是值拷贝,改完字段如果不显式 set 整个数组回去,原资产不会变。正确写法必须严格按下面顺序,漏哪一步都是 silent fail:
arr = effect_type.get_editor_property("system_scalability_settings") # 拷贝
entry = unreal.NiagaraSystemScalabilitySettings()
entry.set_editor_property("max_distance", 4000.0)
# ...
arr.append(entry)
effect_type.set_editor_property("system_scalability_settings", arr) # 必须回写
2. 每个标量都有配对的 bOverride_XXX,漏一个就静默失效
MaxDistance = 4000 单写不会生效,必须同时 bOverride_MaxDistance = true。同类配对至少 4 组:
bOverride_DistanceSettings↔CullByDistance/MaxDistancebOverride_InstanceCountSettings↔CullMaxInstanceCount/MaxInstances
漏哪个都看不出来——值进了资产,引擎运行时直接当 0/false 用。
3. FNiagaraPlatformSet 极不友好
每条 SystemScalabilitySettings 都必填 FNiagaraPlatformSet(决定 override 作用在哪些平台/Quality Level)。结构是 bitmask + CVar 条件列表。Python 想正确构造它:要么用专用辅助函数(多数 UE 版本未暴露 Python binding),要么 raw bit 拼。错一位就是"override 配了但永远不命中"。
4. 命名漂移风险
顶层 enum(update_frequency、cull_reaction)在 UE 各版本稳定。Scalability 嵌套字段在小版本间命名小改过几次(b_cull_by_distance vs cull_by_distance,max_instances vs max_instance_count)。脚本一旦升 UE 版本就会 silent fail。
5. 一次性配置,批处理收益为零
脚本能省事的是"几十/上百个 NS 资产逐个改"那种 O(N) 工作。每类 EffectType 全项目通常只有 1 个,编辑器里点 2 分钟完事。脚本化的调试成本(要把 1/2/3 条全测一遍)远高于手工配的成本。
6. 数值是判断题,不是机械题
MaxDistance、MaxInstanceCount 是基于实测调出来的初始值,TA 大概率要根据实机反复调。让其在 Details 面板看着 Scalability Visualizer 调,比 commit 死值合理。
如果未来确实要脚本化 —— 可参考下面的 helper,但落地前要先确认两个开放问题:
def add_scalability_override(effect_type, *,
platform_set=None, # None = All Platforms
max_distance=4000.0,
max_instances=10):
arr = effect_type.get_editor_property("system_scalability_settings") or []
s = unreal.NiagaraSystemScalabilitySettings()
if platform_set is not None:
s.set_editor_property("platforms", platform_set)
# 4 对 override flag + value 必须成对设置
s.set_editor_property("b_override_distance_settings", True)
s.set_editor_property("cull_by_distance", True)
s.set_editor_property("max_distance", float(max_distance))
s.set_editor_property("b_override_instance_count_settings", True)
s.set_editor_property("cull_max_instance_count", True)
s.set_editor_property("max_instances", int(max_instances))
arr.append(s)
effect_type.set_editor_property("system_scalability_settings", arr)
开放问题:
- 平台范围 — All Platforms 一刀切,还是只针对特定平台 / 特定 Scalability Level?决定
FNiagaraPlatformSet怎么填 - 重复运行语义 — 脚本若被二次执行,是 append 出第二条 entry,还是先清空再加?
附录 B — 参考资料
- Epic 官方:Niagara Effect Type
- Epic 官方:Niagara Scalability
- Epic 官方:Niagara Performance and Optimization