目录
一、效果展示
【UE5】-(三):Niagara 流(风)场特效
二、原理讲解
通过Niagara系统对Flowmap进行采样,生成具有流动效果的粒子动画。
FlowMap本质上是一张纹理,通过RGB通道存储顶点向量移动信息。其中R和G通道分别记录某个方向的向量及速度值,最终通过向量加法将这两个分量合成为单一向量,包含完整的运动方向和速度信息。

理解FlowMap原理后,我们需在Niagara中对FlowMap进行采样并施加相应速度,从而呈现粒子流动效果。
三、步骤实现
注意:以流场为例!!
1.图片获取
1.1 NC源下载
首先需要获取流场的数据。
NC源获取:
Global Ocean Physics Analysis and Forecast | Copernicus Marine Service
Met Office Hadley Centre observations datasets
上述网站中有一些公开的全球流场、温度等数据。
1.2 NC转换图片
将NC数据转换为仅含RG通道的UV贴图。
具体实现方法是将NC数据中的X方向、Y方向的最大最小值分别映射到R、G通道的像素值上:
class ImageGenerationException : public std::runtime_error {
public:
ImageGenerationException(const std::string& msg) : std::runtime_error(msg) {}
};
// 处理数据:替换无效值并移除 NaN
std::vector<double> processData(const std::vector<double>& data) {
std::vector<double> result;
for (double val : data) {
// 替换 null 和 -9999 为 0
if (std::isnan(val) || val == -9999.0) {
result.push_back(0.0);
} else {
result.push_back(val);
}
}
return result;
}
// 计算数据的有效范围 (min, max)
std::pair<double, double> calculateRange(const std::vector<double>& data) {
if (data.empty()) return {0.0, 0.0};
double min_val = std::numeric_limits<double>::max();
double max_val = std::numeric_limits<double>::lowest();
for (double val : data) {
if (!std::isnan(val)) {
if (val < min_val) min_val = val;
if (val > max_val) max_val = val;
}
}
// 处理全为无效值的情况
if (min_val == std::numeric_limits<double>::max()) return {0.0, 0.0};
return {min_val, max_val};
}
// 归一化值到 [0,255] 范围
uint8_t normalizeValue(double value, double min_val, double max_val) {
if (max_val - min_val < 1e-6) return 0; // 避免除零错误
double normalized = (value - min_val) / (max_val - min_val);
normalized = std::max(0.0, std::min(1.0, normalized));
return static_cast<uint8_t>(std::floor(normalized * 255.0));
}
void createPng(const std::string& outputFileName, const json& object) {
try {
auto& datas = object["data"];
if (!datas.is_array() || datas.empty()) {
throw ImageGenerationException("Invalid or empty data array");
}
// 处理主数据集
auto& data1 = datas[0];
auto& header1 = data1["header"];
auto value1 = data1["data"].get<std::vector<double>>();
auto list1 = processData(value1);
auto [min1, max1] = calculateRange(list1);
// 处理副数据集(如果存在)
std::vector<double> list2;
double min2 = 0.0, max2 = 0.0;
bool hasSecondary = false;
if (datas.size() > 1) {
auto& data2 = datas[1];
auto value2 = data2["data"].get<std::vector<double>>();
list2 = processData(value2);
std::tie(min2, max2) = calculateRange(list2);
hasSecondary = true;
}
// 获取图像尺寸
int width = header1["nx"].get<int>();
int height = header1["ny"].get<int>() - 1;
// 创建图像缓冲区 (RGBA格式) 核心关键!!!
std::vector<uint8_t> imageData(width * height * 4, 0);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int index = y * width + x;
int pixelIndex = (y * width + x) * 4;
// 主数据为0时设为蓝色
if (index < list1.size() && list1[index] == 0.0) {
imageData[pixelIndex] = 0; // R = 0
imageData[pixelIndex + 1] = 0; // G = 0
imageData[pixelIndex + 2] = 255; // B = 255
imageData[pixelIndex + 3] = 255; // A = 255
continue;
}
// 计算红色通道 (主数据)
uint8_t r = 0;
if (index < list1.size()) {
r = normalizeValue(std::abs(list1[index] - min1), 0, max1 - min1);
}
// 计算绿色通道
uint8_t g = 0;
if (hasSecondary && index < list2.size()) {
g = normalizeValue(list2[index], min2, max2);
} else if (index < list1.size()) {
g = normalizeValue(std::abs(list1[index] - min1), 0, max1 - min1);
}
// 设置像素值 (RGB)
imageData[pixelIndex] = r; // Red
imageData[pixelIndex + 1] = g; // Green
imageData[pixelIndex + 2] = 0; // Blue
imageData[pixelIndex + 3] = 255; // Alpha
}
}
// 保存为PNG
if (!stbi_write_png((outputFileName + ".png").c_str(), width, height, 4, imageData.data(), width * 4)) {
throw ImageGenerationException("Failed to write PNG file");
}
} catch (const std::exception& e) {
throw ImageGenerationException(std::string("Image generation failed: ") + e.what());
}
}
此外,将陆地上无数据区域处理为R和G通道值为0、B通道值为255的像素点。由于陆地区域不存在海流数据,而有数据的海洋区域仅RG通道包含有效值(B通道为空),因此在Niagara系统中可通过这一特征来识别并移除陆地上的粒子。
会得到这样一张图:

2.Niagara中实现效果
2.1 粒子基础流动
先验证原理讲解中的流向图在Niagara中流动的正确性。
通过采样这张图来验证粒子的流向是否正确:

首先创建一个新的粒子发射器:

在发射器更新中添加 Spawn Particles in Grid 粒子生成方式。该方式将指定网格的行数和列数,系统会根据行列数生成对应数量的粒子,并在定义的区域内均匀分布:

这里缺少依赖项,点击修复问题。

先将 Spawn Particles in Grid 参数设置为40*40进行测试:

修改 Grid Location 中的 Dimensions Definition 为Bounding Box Size 控制边界框大小:

为确保 Sample Texture 功能正常使用,需将发射器切换为GPU模拟模式。同时需调整以下参数:

1.在粒子更新阶段添加 Sample Texture 功能
2.使用一张测试纹理进行采样验证
3.将纹理UV坐标分解为两个方向的向量分量

接下来为粒子添加 Acceleration Force 加速度力让它流动起来:

添加 网格体渲染器,并设置一个箭头网格体,将朝向模式调整为"速度",同时调整粒子大小以观察流动方向是否正确。

当前Niagara粒子流向与示例flowmap对比图:


此时发现,Niagara粒子流向与flowmap的流向Y轴方向相反。
原来UE与Unity在坐标系设计上存在差异:
Unity采用OpenGL标准的坐标系系统,原点(0,0)位于左下角,这与传统数学坐标系保持一致。
而UE则基于DirectX体系,原点(0,0)设定在左上角,这种设计模式与Windows位图存储方式一致。正是这种坐标系差异导致了G通道数据反转的现象。
只需要在 Acceleration Force 给Y轴的速度乘上-1重新反转Y轴即可:

此时Niagara流向已经完全正确:

2.2 初步效果优化
在第一步中,已成功实现了最基本的粒子流动功能。
2.2.1 多余粒子删除
接下来需要剔除陆地区域的多余粒子。如前文所述,陆地应被处理为纯蓝色,即R通道和G通道值为0,B通道值为255。
使用 Kill Particles 剔除蓝色背景中的粒子:


2.2.2 粒子材质优化
原来的网格体渲染器效果不太美观,改用sprite渲染器方式并优化材质,使粒子流动效果更加美观。
1.更改粒子材质:


2.将对齐模式改为速度对齐:

3.统一颜色控制:

2.2.3 优化粒子数量与刷新状态
从图中可见,当前粒子密度过低,导致视觉效果不明显;此外,其刷新频率(即粒子出现和消失的间隔时间)也出现异常:

这里我采用的niagara缩放如下:

1.更改粒子密度:

2.设置粒子刷新时间:

3.设置粒子生命周期和大小:

粒子的运动轨迹出现了明显偏移,因此需要施加约束力来限制其运动。

2.2.4 限制粒子的力


限制生效后,边界附近的粒子效果已基本控制在合理范围内:
2.3 对细节进一步优化
2.3.1 根据速度设置颜色渐变
1.在color的设置中选择 Select Linear Color from Array:

2.新暂存一个动态输入,命名为 ColorByVelo:

3.自定义模块内容:

4.添加对应的MAX、MIN和颜色数组:

可以看到,其中速度较快的地方呈红色:

2.3.2 根据速度设置粒子大小
1.首先在粒子更新中添加 Scale Sprite Size 节点:

2.优化粒子出现的运动曲线,使其呈现从无到有、由小到大的渐进式显现效果:

3.在 Initial Sprite Size 中调整粒子初始尺寸,通过速度动态改变其大小:


3.蓝图控制
接下来可能需要在蓝图中对一些粒子参数进行动态控制:
3.1 在Niagara中添加对应的用户控制
在右侧用户公开中添加动态参数:

创建如下参数:

1.粒子数量:

2.粒子大小:

3.粒子速度:

4.图片采样:

3.2 在蓝图中添加构造函数
1.将Niagara粒子系统添加至蓝图:

2.在蓝图构造函数中添加如下节点:

也可以添加自定义事件以便调用:

将变量更改为可编辑实例:

总结
粒子参数可能仍需进一步优化,且粒子数量对性能影响显著。当前 Kill Particles 功能采用先生成后删除的流程,这种两步操作可能导致性能问题。建议探索直接控制粒子生成算法的方法,避免不必要的生成-删除过程。
本文只提供思路,Niagara新手入门菜鸟一枚,欢迎各位大佬批评指正!!
UE5中NC源下载及转换图片方法


2441

被折叠的 条评论
为什么被折叠?



