【UE5】 - Niagara : 通过采样FlowMap实现流(风)场效果

UE5中NC源下载及转换图片方法
该文章已生成可运行项目,

目录

一、效果展示

二、原理讲解

三、步骤实现

1.图片获取

1.1 NC源下载

1.2 NC转换图片

2.Niagara中实现效果

2.1 粒子基础流动

2.2 初步效果优化

2.2.1 多余粒子删除

2.2.2 粒子材质优化

2.2.3 优化粒子数量与刷新状态

2.2.4 限制粒子的力

2.3 对细节进一步优化

2.3.1 根据速度设置颜色渐变

2.3.2 根据速度设置粒子大小

3.蓝图控制

3.1 在Niagara中添加对应的用户控制

3.2 在蓝图中添加构造函数

总结


一、效果展示

【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

Climate Data Store

Met Office Hadley Centre observations datasets

NCAR RDA Dataset d316001

上述网站中有一些公开的全球流场、温度等数据。

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 DefinitionBounding 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新手入门菜鸟一枚,欢迎各位大佬批评指正!!

该文章已生成可运行项目
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值