☰
UE5风场模拟:基于Niagara Grid粒子探针驱动场景粒子
2026/10/14 10:53:46 网站建设 项目流程

最近在搞一个野外场景的天气系统,树叶不落、草不摆、纸屑在空中打转,传统的那套“定向风源”组件根本管不住粒子。于是搭了一个叫UE5 Wind - 01 Niagara Grid的东西:用一批排列整齐的 Niagara 粒子作为“风场探针”,把风速风向变成一个可以采样、可视化、复用的网格场,再让叶子、纸屑、灰尘跟着它动。这篇文章我就把第一期的完整方案拆开讲,从Grid粒子阵列的生成、风场HLSL的计算,到调试和驱动其他粒子,一次性说清楚。

这个方案适合正在做环境动态的技术美术、特效开发,也适合想搞清楚“Niagara粒子网格到底怎么服务于风场”的人。不需要你有多深的图形学基础,但最好能看懂最基本的 HLSL 结构。

1. 项目定位与技术选型:用粒子网格做风场,图什么

1.1 这个系列的第一课要落地什么

UE5 Wind - 01 Niagara Grid里的“01”说明这是一个系列的第一期。第一期核心不是直接交付一个成品风场资产,而是把“风场”这件事从原理到基础搭建走通。风在游戏里不是物理对象,它是一种“场”——空间每个位置都对应一个风向和风速,物体在这个位置就受到对应方向和大小的力。

这里有一个很容易绕进去的误区:很多人以为用引擎自带的方向风源组件就能驱动粒子,实际试过就知道,那个组件主要影响的是植被、布料和一些显式读取风向的物体,对 Niagara 粒子的控制力非常有限。你打开 Force 模块拖几个径向力场,确实能让粒子乱动,但那不是风,是随机抖动。

我最后选择了 Niagara Grid 作为风场的骨架,原因很直接:粒子本身就是数据载体。把粒子排列成规则的网格,每个粒子作为一个小探针,持续向外输出自己所在位置的“风向 + 风速”。其他粒子、植被、布料,都可以通过查询周围探针,拿到自己需要的那一份风力数据。

这个架构最大的优势在于“可寻址”。普通力场是一锅汤,你没法单独看某个位置的风;Grid 则是一本账,位置、大小、方向都清清楚楚。第一期我们先把这本账记明白,后续再考虑怎么把它导出给远处的大批量植被或毛发系统。

1.2 三种常见风场方案的对比

我实际比较过三种方案,各有各的坑,列个表方便你判断:

方案优点缺点适合场景
引擎方向风源组件配置简单,对植被等系统友好对 Niagara 粒子几乎无感,只能给全局方向非粒子类场景细节
叠加径向/漩涡力场见效快,可做出明显局部效果多力场叠加后方向杂乱,无法精确控制空间分布单点事件型特效
Niagara Grid 探针空间连续、可调试、可导出、可参数化需要搭建数据通道,入门门槛高大范围持续风场

方案对比下来,Grid 是最适合“野外场景”这种大范围、持续、有方向性风场的选择。它也确实更费工夫,但这篇文章会把每一步都拆开。你只需要一个支持 GPU 粒子的 D3D12 或 Vulkan 环境,后面所有步骤都可以照做。

2. 搭建Grid探针系统:从空白Niagara到整齐的粒子阵列

2.1 创建Emitter与网格粒子生成

打开 Niagara 编辑器,新建一个空的 Niagara 系统,加一个 GPU Emitter,我给它起名WindGridProbes。这个 Emitter 的任务很简单:生成一批不移动、不渲染(平时)的粒子,每个粒子固定在世界空间某个格点上。

粒子数量用Spawn Burst Instantaneous控制,数量设置为GridX * GridZ。我在这个例子里用16 * 16 = 256个粒子,先声明两个参数:

  • GridOrigin:网格左下角原点
  • GridSpacing:格子间距

初始化粒子位置的核心是用 Custom HLSL 根据粒子索引算行列:

UDN::Spawner::SetPosition( GridOrigin + float3( (float)(Index % GridX) * GridSpacing, 0.0f, (float)(Index / GridX) * GridSpacing ) );

这里Index就是粒子的执行索引。你要在 Custom HLSL 模块的输入参数里把它绑定到Execution Index。取模拿到 x 方向排第几列,整除拿到 z 方向排第几行。只要GridX * GridZ与 Spawn 数量一致,就能生成规则网格。

注意:我用的写法是示意性的。不同 UE5 小版本里 Niagara 的属性命名会有点差异,核心逻辑不变——拿到粒子的“序号”,算“行列”,再映射到“世界坐标”。如果你不想用 Custom HLSL,也可以拆成“Add Float / Multiply Float / Add Vector”节点一步步搭,但那样节点会比较多,调试时也不够直观。

生成完可以先用 Sprite Renderer 把这 256 个粒子显示出来,你会看到一个整整齐齐的 16 x 16 网格。到这里阵列生成就完成了。

2.2 探针粒子的初始化与调试渲染

网格粒子生成后,有两件必须做的事。第一,把这些粒子变成真正的“探针”,也就是固定位置、不参与物理运算。在 Niagara 里只需要把 Update 阶段的 Gravity 模块移除,并把 Position 更新锁死。实际操作里,我会把 Update 阶段的模块全部清掉,只保留一个 Custom HLSL,用来逐帧刷新探针上的风场数据。

第二,考虑渲染方式。探针本身是不需要出现在最终画面里的,但它又是调试风场的最佳武器。我的习惯是:平时把 Sprite Renderer 直接删掉,系统只跑数据;调风场时再临时加一个渲染器,让探针以很小的 Sprite 显示出来,颜色绑定风强度。这么做的好处是:你能肉眼看到风在哪里强、哪里弱、哪里方向突变,比盯着数字面板高效得多。

调试渲染时还有一个更进阶的做法——让每个探针显示一个小箭头。Niagara 里最省事的办法是给 Sprite 设置 Facing,让它朝向ProbeWind的方向,再在 Renderer 里把 Sprite 的尺寸按风速拉长。这样 Grid 就像一张风场示意图,风向、强度一目了然。

2.3 探针不参与物理运动的注意事项

写到这里必须强调一个常见的坑:探针粒子一旦绑定了重力、阻力、碰撞,整个网格就会散架。它们应当是纯粹的数据节点,不应该有任何动力学行为。

为了让这一点更可靠,我给探针添加了一个自定义 Bool 属性ProbeActive,默认设为 true。后续做场景生长变化时,可以根据这张探针的激活状态决定是否输出风力,而不用删粒子。这样既保持阵列结构完整,又能局部开关风力。

另一个经验是:探针网格越规则越好。如果GridSpacing在两个方向不一样,风场在空间上会被拉伸变形,树叶飘动时会透出一种诡异的“方格感”。想降低性能开销可以减少网格数量,但不要用非均匀间距,得不偿失。

3. 风场计算核心:把“风”写成一段可控的HLSL

3.1 风场必须满足的两个基本要求

在写风场函数之前,先明确自然风的两个基本要求,否则调出来的东西会被眼睛一眼识破。

第一,方向变化要平滑。真实的风不会一帧之内从正东急转到正西,它有一个渐变的过渡。这意味着风力函数不能是白噪声,必须是带频率限制的连续场。纯随机数做出来的风会让粒子疯狂抖动,看起来像得了癫痫。

第二,空间上不能有“源”和“漏”。这是流体力学里的无散(divergence-free)概念,通俗讲就是:风穿过任何一个空间区域,流进来的量要等于流出去的量。如果违反了这一点,场景里会出现莫名其妙的“风源”或“风汇”,树叶会被吸到一个点或者从某个点狂喷出去。旋度噪声(Curl Noise)天然满足这个性质,这也是它成为流体粒子首选噪声的原因。

旋转度噪声的核心公式是:

curl(P) = ( dNz/dy - dNy/dz, dNx/dz - dNz/dx, dNy/dx - dNx/dy )

也就是求一个三维噪声场的旋度。具体实现里用差分近似偏导:

dNz/dy ≈ (Nz(P.y + h) - Nz(P.y - h)) / (2h)

h 一般取比较小的值,比如 0.1 或 0.05。h 太小噪点会变细,h 太大会丢失细节,需要根据你的 GridSpacing 调。

3.2 一个能立刻跑起来的风场函数

完整实现 Perlin 噪声加旋度计算,代码量不小。我建议第一步先用一个“叠加正弦风场”跑通整个链路。这个函数不算物理正确的无散场,但视觉上已经足够骗过大部分眼睛:

float t = GlobalTime * 0.4f; float3 p = ProbePosition * 0.08f; float windX = sin(p.x + t) * 0.6f + sin(p.z * 0.7f - t * 0.6f) * 0.4f; float windZ = cos(p.z + t * 0.8f) * 0.6f + cos(p.x * 0.6f + t * 0.5f) * 0.4f; float weakness = saturate(1.0f - ProbePosition.y / FalloffHeight); ProbeWind = float3(windX, sin(t + p.y) * 0.1f, windZ) * BaseWindSpeed * weakness; ProbeStrength = length(ProbeWind);

这段代码放在探针粒子的 Update Custom HLSL 里。它做的事情很简单:用两个不同频率和方向的正弦波叠加,构造出既有整体方向变化、又有局部细节的风向场。weakness让风随高度衰减,模拟地面摩擦。

有了这版数据,探针粒子开渲染后,你就能在编辑器里看到风场随时间的流动。这一步的关键是怎么让“风”有“呼吸感”,而不是循环平移。诀窍是给几个波设置不同的时间和空间频率,让它们之间产生相位差。上面的公式里p.x + t、p.z * 0.7 - t * 0.6这种写法就是干这个的。

3.3 进阶方向:旋度噪声与内外参数化

当你确认整个链路没问题,想追求更自然的流体感,就把正弦风场替换成旋度噪声。你不需要在 Custom HLSL 里硬写一套 Perlin,直接拖一个 Noise Data Interface 进 Niagara,把它的输出作为噪声采样源。在 Custom HLSL 里按 3.1 的差分公式,对噪声场求旋度,得到的就是一个无散的矢量场。

这里要多说一句:Noise Data Interface 的版本和参数会直接影响风感。我用下来比较顺手的组合是:基频 0.05 - 0.1,FBM 层数 2 - 3 层,时间输入乘一个 0.2 - 0.5 的系数。基频太高会导致风场碎成渣,太低则风像一块慢慢挪动的果冻。没有绝对正确,只有适不适合你的场景尺寸。

同时,强烈建议把风速、基频、湍流强度、空间衰减高度这四组参数全部暴露为 Emitter 参数。因为在实际调效果时,你一定会反复来回拖这些参数。把它们硬编码在 HLSL 里是最蠢的做法,每改一次都要进节点编辑器,非常浪费时间。

3.4 让探针显示风的方向与强度

探针显示这里有一个我踩过的坑。直接用ProbeWind的 x 分量映射成红色,z 分量映射成蓝色,确实能看出方向,但颜色很不直观。后来我改成用ProbeStrength映射亮度,再叠加一个小箭头的朝向,调试效率一下子高了很多。具体做法是给 Sprite Renderer 加一个颜色模块,让它的 Value 绑定ProbeStrength,然后在高光处调一个偏暖的颜色,低风速处调成偏冷的暗色,比如“红 - 黄 - 青”渐变。这样一眼就能看出风场的强弱分布。

如果追求更高精度,可以把探针的 Sprite 用方向向量来控制旋转,形成小箭头。做法是使用 Niagara 的Sprite Facing模块,把 Facing 模式改成自定义向量,然后绑定normalize(ProbeWind)。再配合 1.5 倍速播放的预览,就能看到整个风场如潮水般流动。

4. 从风场到实物:驱动树叶、纸屑与尘埃

4.1 展示粒子的初始化与受力更新

探针系统建好以后,新建第二套系统,我起名DebrisDriven,专门用来放树叶、纸屑这类被风吹动的物体。这套系统里粒子数量可以多一些,比如 2000 - 5000 个,初始位置随机撒在 Grid 覆盖的范围内,初始速度为零。

在 Particle Update 里加 Custom HLSL,把探针里那套风场公式原样复制一份,只是把输入从ProbePosition换成当前展示粒子的Position:

float t = GlobalTime * 0.4f; float3 p = ParticlePosition * 0.08f; // 与探针完全同源的风场计算 float windX = sin(p.x + t) * 0.6f + sin(p.z * 0.7f - t * 0.6f) * 0.4f; float windZ = cos(p.z + t * 0.8f) * 0.6f + cos(p.x * 0.6f + t * 0.5f) * 0.4f; float weakness = saturate(1.0f - ParticlePosition.y / FalloffHeight); float3 wind = float3(windX, sin(t + p.y) * 0.1f, windZ) * BaseWindSpeed * weakness; Velocity += wind * DeltaTime * 0.5f; Velocity *= exp(-3.0f * DeltaTime);

加阻尼那行exp(-3.0 * DeltaTime)很重要。没有阻尼,速度会无限累积,叶子越飞越快;阻尼太大,树叶又会被“粘”在半空。0.5 和 3.0 这两个系数是我调过的起点值,你完全可以从这里开始上下试。

为了让叶片看起来更像被风吹,而不是被一只无形的手拖着走,我还会在 Update 阶段把粒子的朝向对齐到速度方向。最省事的是用 Calculate Rotation 模块,基于速度向量算旋转四元数,再用 Lerp Rotation 做平滑过渡。这样叶子在转向时会有“挣扎感”,而不是瞬间横过来。

4.2 公共Module Script:让多套系统共用同一份风场

复制风场公式到第二套系统,首先是能用的,但一旦涉及调优,你就会体会到“复制粘贴”的恶果:每次改参数要改两处,漏改一处,探针显示和粒子表现就对不上。

正确的做法是把这个风场函数提取成一个 Niagara Module Script。在 Content Browser 里右键创建 Niagara Module Script,把上面那段 HLSL 放进去,定义输入参数为Position、Time、BaseWindSpeed、FalloffHeight,输出为WindVector3。然后在探针 Emitter 和展示 Emitter 里都加上这个 Module。以后调风场只改一处,两边同步更新。

这一步也是我建议所有 Niagara 项目坚持的基线:不要写“一次性模块”,所有可能复用的逻辑全部独立成 Module Script。Niagara 的节点图虽然直观,但项目一大就会变成蜘蛛网,Module Script 是唯一有效的治理方式。

4.3 进阶数据通道:Grid如何扛起更大的场景

到这里你应该会产生一个疑问:探针似乎只是起了“可视化”的作用,真正给展示粒子提供风力的是“同一份公式”,而不是“Grid 数据”。这在第一期项目里是完全可以接受的,但如果场景极大、需要驱动的粒子系统有十多个,每个都跑一遍完整的噪声计算,性能就不可控了。

所以从工程化的角度看,Grid 真正的价值是作为一个“风场数据源”,把连续的风场函数离散化存储。大致有三条路可以走:

  • 把探针的风矢量渲染到 Render Target 2D/3D,展示粒子通过采样贴图拿到风。
  • 使用 Neighbor Grid 3D Data Interface,让展示粒子在同一个 Emitter 内查询 Grid 粒子上的风速属性。
  • 把风场烘焙成 Volume Texture,再做一次离线优化,运行时几乎零计算。

这三条路都会在系列的后续文章里展开。现阶段先把探针和展示粒子之间的“同步电机”跑通,让风感可视化、可调参,之后再做数据通道的工程化,顺序是最舒服的。

5. 常见问题排查与调优实录

5.1 探针阵列常见错误

网格乱飞或全挤到原点。最常见原因是 Custom HLSL 里Index变量没绑定对,它拿到的是一个常量或者随机数。检查 Custom HLSL 输入变量是否绑定了Execution Index,并且类型是 int。另一个常见原因是 Spawn 数量与GridX * GridZ不匹配,会导致取模后位置重叠或空白。

粒子在原地高频抖动。说明探针的位置更新模块没删干净。如果你在 Initialize 和 Update 里各写了一份设置位置的代码,两段会互相打架。探针只允许在 Initialize 阶段设置位置,Update 阶段只更新风力属性。

网格像一块布一样被风吹动。你大概率不小心把探针粒子的位置也写入到了更新公式里。注意区分:ProbePosition在更新阶段必须始终是固定的格子点坐标,风速计算用的只是它的值,不能反过来给 Position 赋值。

5.2 展示粒子飘动异常的排查

粒子朝随机方向乱撞。先确认展示粒子的风力公式是否和探针完全一致。很多情况下是 GlobalTime 没同步,或ParticlePosition和Velocity的坐标系写混了。一个高效排查办法:把探针的调试渲染打开,和展示粒子同屏播放,看粒子是否基本沿着箭头方向移动。如果方向对不上,优先检查坐标空间。

树叶速度一直在涨,飘到天边不回来。这是阻尼系数太小或风速调太猛的典型表现。我一般会先加一个最大速度限制:

Velocity = clamp(length(Velocity), 0.0f, 3.0f) * normalize(Velocity + float3(0.001, 0, 0));

注意 normalize 时加一个微小值防止零向量除零。

叶片翻面像抽搐。朝向对齐速度方向时,如果速度接近零,旋转四元数会跳变。解决办法是加一个最小速度阈值,低于阈值时保持上一帧的朝向,或者把 Lerp Rotation 的插值速度调慢一些。

5.3 性能账单与优化顺序

这套方案的性能开销分两块。探针侧:256 个 GPU 粒子非常轻,主要成本在 Custom HLSL 里的噪声计算。如果你用三到四层 FBM,可以把更新频率降低,比如每两帧更新一次风场属性。展示侧:几千个粒子真正贵的是 Sprite 的渲染数量,以及粒子数量和半透明排序。

优化的先后顺序,我自己的经验是:先压展示粒子数量,把透明度排序调对;再降低探针更新频率;最后才考虑把噪声拆到更少层。不要上来就砍噪声层数,那个对风感影响最大,一旦砍过头,整个场景会像静止了一样。

另外提醒一句,如果你在场景编辑器里看探针网格觉得顺畅,但在实际游戏视角里却有闪烁,多半是半透明排序问题,和风场本身无关。把展示粒子改成 Depth Fade + 固定排序优先级,问题基本能消失。

最后分享一个我自己的调试习惯:调风场时永远把探针的调试渲染保持在可切换状态,颜色映射到风速强度。调完一版就切出去跑一下真实视角,看看树叶整体有没有“叙事性”——是朝着一个方向渐次飘动,还是一团乱麻。风场不是数学题,最终起决定作用的是眼睛。这套 Niagara Grid 方案给了我一个把“看不见的风”变成“看得见的场”的抓手,希望对你也有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询