UE5.8 Niagara Fluids 流体模拟:油桶爆炸与火旋风的实战思路
引子:老陈的火把测试
关卡美术老陈接到了一个破坏关卡的任务:玩家用燃烧瓶击中油桶后,桶口先冒黑烟,接着炸出一团橙红色火球,最后在地上留下几缕翻滚的火焰。策划说,这不是一段贴图动画,火焰要能绕过木箱、被侧风吹斜、在墙角形成涡流。
老陈打开老一套做法:用 Sprite 粒子拼烟雾,用 Ribbon 拖尾做火苗,再用序列帧动画补爆炸。调了两天后,效果远看还行,但只要相机靠近,烟雾像一层层平面的贴图,火焰也从不和场景几何互动。策划看着墙角本该被卷起的火舌毫无反应,摇了摇头。
同组的技术美术小李提到 UE5.8 里有 Niagara Fluids,说是可以直接在引擎里解算流体场,再让粒子跟着场跑。老陈半信半疑:流体解算不是 Houdini 的事吗?放到实时游戏里能跑多快?小李说先拿油桶做个最小原型,看看 Grid 2D 能不能把烟雾托起来,不行再回粒子拼贴。
这篇文章记录老陈那个原型踩过的坑,以及后来做火旋风时把 3D Grid 和 Niagara 粒子串起来的过程。
一、Niagara Fluids 到底在解决什么问题
Niagara 传统粒子系统擅长处理大量离散的点:火星、碎片、光斑。每个粒子有自己的位置、速度、颜色,彼此之间没有物理联系。做一团烟雾时,只能用 Curl Noise 或 Vector Field 伪造湍流,粒子之间不会互相推开,也不会因为环境遮挡而绕路。
流体模拟的核心是把空间切成网格,在每个格子上存储速度、密度、温度、压力等连续量,然后按 Navier-Stokes 方程推进。Niagara Fluids 把这套机制搬进了 Niagara 的 GPU Simulation 管线,让特效师不用离开引擎就能看到烟雾翻滚、火焰升腾、墨水扩散。
architecture-beta
group ue[UE5.8 Niagara Fluids]
service input[输入层 场景几何/外力/粒子源] in ue
service grid[Grid 数据层 密度/速度/压力/温度] in ue
service solver[GPU Solver 扩散/对流/投影] in ue
service particle[Niagara 粒子采样与反馈] in ue
service render[材质与光照渲染] in ue
input:B --> T:grid
grid:B --> T:solver
solver:B --> T:particle
particle:B --> T:renderNiagara Fluids 的定位不是替代 Houdini 离线解算,而是处理游戏里需要实时交互的中等规模流体。适用场景大致有三类。
第一类是环境氛围。篝火烟雾、烟囱废气、蒸汽管道泄漏。这些效果距离玩家较远,不需要极高精度,但要能随风向变化,并且能和角色移动产生扰动。
第二类是技能特效。火墙、毒雾、冰霜吐息、龙卷风。玩家释放后,流体场要能跟随技能中心移动,同时和场景边界碰撞,避免穿墙。
第三类是破坏反馈。爆炸烟尘、燃烧残留、油渍扩散。这类效果生命周期短,但对动态感和空间贴合度要求高,静态贴图很难糊弄。
老陈的油桶和火旋风分别属于第二类和第三类,正好覆盖了 Niagara Fluids 最常见的用法。
二、2D Grid 与 3D Grid 的选择
Niagara Fluids 提供两种网格模拟:Grid2D Collection 和 Grid3D Collection。名字已经说明了一切:一个只在 XY 或 XZ 平面上解算,一个在三维空间里解算。
Grid2D 的开销低、易于调试,适合表现平板状或柱状流体。地面上的火焰、水面上的蒸汽、贴墙蔓延的黑烟,都可以先用 Grid2D 做原型。Grid2D 默认把世界空间的一个矩形区域划分成 128x128 或 256x256 的格子,每个格子存速度、密度、温度、燃料等量。因为少了一个维度,显存和计算量都更容易控制。
Grid3D 适合需要立体结构的流体。火旋风、蘑菇云、爆炸气浪、地面向空中翻卷的烟柱,都需要三维速度场才能表现出正确的涡旋和遮挡。代价是显存按立方增长:一个 128x128x128 的 Grid3D,数据量是 128x128 Grid2D 的 128 倍。所以在游戏里用 3D 流体,第一个要想清楚的就是分辨率能压到多低。
flowchart TD
A[需求分析] --> B{流体是否有明显厚度变化}
B -->|平面/柱状为主| C[Grid2D 原型]
B -->|立体涡旋/穿墙遮挡| D[Grid3D 原型]
C --> E{精度是否够}
D --> F{性能是否可接受}
E -->|不够| D
F -->|不够| C
E -->|够| G[Grid2D 量产]
F -->|够| H[Grid3D 量产]老陈做油桶爆炸时,先开了 Grid2D。他以为火焰从桶口向上冒,X 和 Y 两个方向的涡流就够了。结果发现火焰撞到天花板后应该向四周扩散,二维速度场只能把密度推到格子的左右边界,无法表现出热浪向前扑向玩家的压迫感。于是他把核心区域换成了 64x64x64 的 Grid3D,精度降得很低,但体积感立刻出来了。
火旋风则完全相反。一开始小李建议直接上 3D,因为旋风是立体的。老陈试了一个 96x96x96 的 Grid3D,帧率掉了 8 毫秒。后来他改用 Grid2D 的 XZ 平面做底部旋转场,再用垂直粒子发射模拟高度,视觉上反而更像一个旋风柱,开销却只有原来的十五分之一。
所以 Grid 维度的选择没有标准答案。2D 和 3D 可以组合:用 Grid3D 处理近处核心区域,用 Grid2D 处理远处大面积烟雾,再用普通粒子补细节火星。
三、流体求解基础:密度、速度、压力、扩散
要调 Niagara Fluids,不能只拖参数。老陈花了两个晚上把几本 GPU Gems 和 Jos Stam 的 Stable Fluids 论文翻了翻,理解了四个核心量:密度、速度、压力、扩散。
密度场 描述的是烟雾或燃料在空间中的分布。渲染时把密度当成不透明度或发光强度来读。密度不会凭空产生,发射器每帧往源格子里注入一定量的密度。密度也会因为速度场对流而移动,因为扩散而变模糊。
速度场 描述流体运动的方向和快慢。每个格子存储一个三维向量。速度场决定密度怎么飘、粒子怎么被带动。速度场本身也受外力、压力、粘性影响。
压力场 用来保证流体不可压缩。没有压力修正,速度场会很快发散,出现奇怪的源和汇。投影步会把速度场拆成无旋部分和有旋部分,去掉散度,让流体看起来真实。
扩散项 让速度场在相邻格子之间平滑,模拟粘性。烟和空气的粘性不同,扩散系数也不同。扩散太高,流体像蜂蜜;扩散太低,数值容易震荡。
不可压缩 Navier-Stokes 方程写成这样:
密度场的对流扩散方程:
其中 是外力,比如浮力让热烟上升; 是密度源,比如油桶每帧注入燃料; 是密度扩散系数。
Niagara Fluids 的求解器把上面的连续方程离散到 Grid 上,每帧分几步跑。老陈把这几步对应到 Niagara 模块里,调参时心里才有底。
Advection:把当前格子的密度和速度沿速度方向往回追溯,插值上一帧的值。Niagara 里叫 Advect 或 Transport。这一步决定流体跟着自己的速度走。Cfl 条件要满足,否则数值会发散。简单说,一个时间步长内流体不能跑过超过一个格子。如果速度太快,要么降低时间步长,要么提高 Grid 分辨率。
Diffusion:在相邻格子之间做高斯模糊或 Jacobi 递推,让密度和速度平滑。Smoke 和 Fire 模板里通常有一个 Diffusion 参数直接控制。
Force / Buoyancy:给速度场加外力。火焰的浮力向上,爆炸的冲击波向外,侧风给速度场加一个水平分量。老陈在油桶里加了一个垂直浮力 ,其中 是温度, 是高度,避免火焰无限上升。
Divergence Free Projection:计算压力并修正速度,让速度场无散度。这一步计算量最大,Niagara 默认用 Jacobi 循环求解 Poisson 方程。循环次数越多,流体越不可压缩,但开销也越大。
sequenceDiagram
participant S as Solver 起始
participant A as Advection
participant D as Diffusion
participant F as Force/Buoyancy
participant P as Pressure Projection
participant N as 下一帧
S->>A: 输入密度场与速度场
A->>D: 对流后的场
D->>F: 扩散后的场
F->>P: 加入外力
P->>N: 输出无散度速度场老陈一开始只调 Density Scale 和 Velocity Scale,结果烟雾要么糊成一团,要么碎成噪点。后来他明白,真正决定流体气质的是 Diffusion 和 Projection Iterations。Diffusion 低、投影次数多,烟雾边缘锐利,能看到小涡旋;Diffusion 高、投影次数少,烟雾柔和,更像远处的大气。
四、把流体场和 Niagara 粒子串起来
流体解算本身不产生可见像素。Niagara Fluids 的真正用法是用 Grid 驱动粒子,再用粒子去渲染。老陈一开始以为开了 Niagara Fluids 就会自动冒烟,结果发现还要手动建一个发射器去读 Grid。
最常见的耦合方式有两种。
第一种是粒子被动跟随速度场。发射器里加一个 Sample Grid 模块,每个粒子根据自己的世界位置去 Grid 里采样速度,然后把速度加到粒子的 Velocity 属性上。粒子像灰尘一样被风卷着走。油桶的火星和碎屑适合这种方式。
第二种是粒子主动影响流体场。粒子把自己当成密度源或热源写入 Grid。爆炸瞬间生成的火花、燃油滴,都可以是 Grid 的输入。Niagara Fluids 模板里的 Emitter 通常会把 Particles.Position 和 Particles.Velocity 写入 Grid,实现双向耦合。
graph LR
A[粒子位置/速度] -->|写入| B[Grid 密度/速度场]
B -->|求解| C[更新后的场]
C -->|采样| D[粒子新速度]
D -->|更新| A老陈的油桶特效用了三个 Niagara Emitter。
第一个叫 FuelSpray,是一个普通粒子发射器,Spawn Burst Instantaneous 在爆炸瞬间生成 200 个燃油滴粒子。每个粒子有初速度,生命周期 0.5 秒。Particle Update 里加了一个 Grid3D Set Fluid Source 模块,把粒子的位置、速度和燃料属性写进 Grid3D 的燃料通道。
第二个叫 FluidSolver,是 Niagara Fluids 模板自带的发射器类型。它不生成可见粒子,只负责每帧跑 Advection、Diffusion、Buoyancy、Projection。它读取 FuelSpray 写进去的燃料,解算密度和速度场。
第三个叫 SmokeParticle,是可见的烟雾粒子发射器。Spawn Rate 根据 Grid3D 里的密度来驱动:密度高的区域多生粒子,密度低的区域少生。Particle Update 里用 Sample Grid3D Velocity 读取速度场,再叠加上粒子自身的浮力和 Drag。材质读取粒子的密度属性控制不透明度和颜色。
graph TD
A[FuelSpray 粒子] -->|Fuel/Veclocity| B[Grid3D]
B --> C[FluidSolver 解算]
C -->|Density/Velocity| D[SmokeParticle 采样]
D --> E[材质渲染]粒子采样 Grid 时要注意坐标转换。Grid 有自己的 World Bounds,粒子世界位置要先转换到 Grid 的 UV 空间。Niagara 的 Grid3D Sample 模块会自动做这个转换,但 World Bounds 必须和 Grid 创建时一致,否则采样会错位。老陈有一次把 Grid 的中心点移了 10 厘米,结果烟雾从油桶旁边两米处冒出来,查了半天才发现是 Bounds 没同步。
另一个容易忽略的是 Grid 的边界条件。边界可以设为 Open、Wrap 或 Closed。Open 让流体自由流出,适合远处烟雾;Closed 会把流体反弹回来,适合盒子里的火焰。老陈的油桶放在墙角,Grid 边界设成 Closed,火焰撞到墙后会向上卷,符合直觉。如果设成 Open,火焰会从墙里穿出去,看起来很假。
五、材质渲染与光照
流体粒子最终要靠材质画出来。Niagara Fluids 没有自带的渲染器,它输出的是 Grid 数据和被场驱动的粒子。老陈在材质侧做了三件事:密度转不透明度、温度转颜色、自发光配合 Lumen。
烟雾材质最简单。用 Niagara Vertex Color 或 Particle.Color 控制基础色,用 Particle.Density 控制 Alpha。密度越大,烟雾越浓。为了让烟雾有体积感,材质里加了 Depth Fade 和 Camera Fade,避免粒子边缘过硬,也避免离相机太近时穿帮。
火焰材质复杂一些。火焰不是简单的不透明渐变,而是高温气体发出的光。老陈用了 SubUV 动画贴图做火苗细节,再用 Particle.Temperature 驱动颜色:低温偏红,高温偏黄白。温度场由流体解算器维护,燃油粒子写入燃料通道时同时写入温度。
材质里关键的一段节点逻辑可以简化成:
BaseColor = lerp(DarkRed, HotYellow, saturate(Temperature / 1200))
Emissive = BaseColor * Density * EmissiveScale
Opacity = Density * OpacityScale * DepthFade光照方面,Niagara Fluids 的火焰会产生自发光,但自发光本身不会照亮场景。老陈给火焰区域加了一个 Point Light 或 Rect Light,用 Niagara 的 Light Renderer 生成。UE5.8 里 MegaLights 和 Lumen Lite 让这种动态光源的开销更可控。火旋风中心的光源数量一多,老陈用 MegaLights 的 Shadow Budget 限制同时投射阴影的灯光数量,避免帧率崩盘。
Lumen 对半透明烟雾的间接光照反馈有限,但火焰作为强光源照亮周围场景的效果很明显。老陈在场景里放了几面墙,火旋风旋转时,墙上的高光也跟着移动。这种光影联动是传统 Sprite 粒子很难做到的。
渲染顺序也要注意。流体烟雾通常是半透明,放在 Translucent 渲染队列。如果场景里还有水体、玻璃、其他粒子,排序可能出问题。老陈把火焰和烟雾分成两个 Niagara System,火焰用 Additive 混合、烟雾用 Alpha 混合,分别控制 Sort Priority,减少 Z-Fighting 和 Overdraw。
六、性能限制与平台差异
流体解算是 GPU 计算密集型效果。老陈在 PC 上跑得顺的原型,放到主机模式一测,帧率掉了十几毫秒。他把性能问题拆成三块:Grid 分辨率、循环步数、粒子数量。
Grid 分辨率直接影响显存和带宽。一个 128x128x128 的 Grid3D,每个通道是 FP16,一个属性就要 4 MB。密度、速度 XYZ、温度、压力、燃料,加起来轻松超过 20 MB。如果还要双缓冲,再翻一倍。老陈最后把 Grid3D 压到 80x80x80,视觉上只损失了远处烟雾细节,帧率却回到可接受范围。
循环步数决定投影步的精度。默认 Jacobi 递推 20 到 40 次。次数太少,速度场发散,烟雾会莫名其妙地收缩或膨胀;次数太多,Compute Shader 时间变长。老陈在主机上把循环步数降到 12,配合 Diffusion 调高,视觉上仍然是稳定的烟雾,只是涡旋细节少一点。
粒子数量取决于 Spawn Rate 和 Density Threshold。流体场驱动的粒子可以很多,但 Overdraw 会吃掉 GPU 填充率。老陈用 Niagara 的 Scalability 在不同平台设置不同 Spawn Rate。PC 上烟雾粒子峰值 3000,主机 1200,移动端干脆关掉流体模拟,用预烘焙的 Flipbook 代替。
平台差异不只是性能,还有功能支持。Niagara Fluids 依赖 Compute Shader 和 UAV 随机写入,对 GPU 架构有要求。Switch 和手机芯片的支持较弱,某些旧设备上 Grid3D 解算器会直接失效。老陈的项目目标是 PS5、Xbox Series X 和 PC,所以问题不大,但他还是做了 fallback:如果检测到不支持 Compute Shader,就切换成普通粒子版本。
性能估算上,流体解算每帧的成本可以近似写成:
其中 是 Grid 体素总数, 是压力投影循环次数,、、 分别是投影、对流、扩散的单体素开销。优化的核心就是同时压 和 ,而不是只调一个。
老陈还用了两个取巧办法。第一个是把 Grid 更新频率降到 30 FPS,中间帧用上一帧结果插值。玩家不太会注意到烟雾慢半帧,但 GPU 时间省了一半。第二个是用 LOD:距离相机 10 米内用 Grid3D,10 到 30 米用 Grid2D,更远直接 billboard。Niagara System 的 LOD Distance Check 可以自动切换 Emitter,配合 Niagara Fluids 很好用。
七、UE5.8 中的流体改进
UE5.8 对 Niagara Fluids 的改动不算革命,但几个细节确实让老陈的工作轻松了一些。
首先是模板库扩充。Content Examples 里新增了 Pyro、Liquid、Smoke、Dust 四个 Niagara Fluids 模板,每个都带完整的 Grid 设置、材质和粒子耦合示例。老陈的油桶原型就是基于 Smoke Template 改的,省掉了从零搭 Solver 的时间。
其次是 Grid 调试可视化。5.8 的 Niagara Debugger 里可以直接把 Grid3D 的某个通道画成体素云,或者用 Slice 平面查看密度分布。老陈以前调试烟雾错位,只能靠猜,现在能直接看到 Grid 里哪里密度异常。
第三是 Grid 和 SDF 的结合更顺畅。5.8 里 Niagara 可以直接读取场景的 Signed Distance Field,做更精确的边界碰撞。老陈的火旋风撞到圆柱形石柱时,以前只能用 Box Bounds 近似,现在 SDF 能做出弧形绕流,涡旋更自然。
第四是 Material Layer 和 Substrate 的兼容。Niagara Fluids 输出的粒子属性可以直接驱动 Substrate 的散射和发光层。老陈在火焰材质里用了 Substrate 的 Unlit 层做高温核心,再用 Base 层做受热空气的折射扭曲,效果比传统材质更立体。
第五是异步解算。5.8 允许把部分 Niagara Fluids 的 Compute Shader 放到异步 Compute Queue 上跑,和 G-Buffer 生成并行。老陈在 PS5 上测试时,这一项把流体模拟的可见延迟降低了 2 到 3 毫秒。
architecture-beta
group ue58[UE5.8 Niagara Fluids 改进]
service template[模板库 Pyro/Smoke/Liquid/Dust] in ue58
service debug[Grid 调试可视化] in ue58
service sdf[SDF 边界碰撞] in ue58
service substrate[Substrate 材质兼容] in ue58
service async[异步 Compute Queue] in ue58
template:B --> T:debug
debug:B --> T:sdf
sdf:B --> T:substrate
substrate:B --> T:async最后是文档和示例视频的完善。5.7 时 Niagara Fluids 的参数说明很零散,5.8 官方文档把每个 Grid 参数、Solver 设置、模块输入都补全了。老陈做火旋风时卡在一个 Pressure Iteration 的默认值上,翻文档发现已经标注了主机推荐值。
写在最后
老陈的油桶特效最后通过了策划评审。相机靠近时,火焰会绕过木箱;燃烧瓶从侧面扔来,烟雾被风压斜向一边;爆炸后的余烟贴着地面向墙角扩散。这些细节用传统粒子也能硬做,但工作量要大好几倍,而且每改一次场景布局就要重新手调。
Niagara Fluids 的真正价值不是让流体看起来多真实,而是让特效师能在引擎里快速调整。改动 Grid Bounds、换一个 Buoyancy 曲线、调整 Diffusion,效果立刻更新。这种反馈速度是离线解算无法提供的。
火旋风上线前,老陈又做了一次性能审计。Grid 分辨率、循环步数、粒子数量、光源数量,每一项都对应一条平台差异配置。他把 PC、主机、移动端三套 Scalability 参数存成 Niagara System 的 Scalability Settings,打包时自动切换。
UE5.8 的 Niagara Fluids 仍然需要开发者理解 Navier-Stokes 的基本概念,不是拖个模板就能出效果的工具。但只要把密度、速度、压力、扩散这几个量搞清楚,再把 Grid 和粒子之间的数据流理顺,做出现象级的环境特效和技能特效并不遥远。老陈现在最常说的一句话是:先做 Grid2D 原型验证,再决定要不要上 3D,别一上来就把分辨率拉满。