注册
web

我不允许还有人不知道前端实现时刻洪水模拟的方法!🔥

二维水动力 HydroDynamic2D


二维水动力介绍


二维水动力模型对象 HydroDynamic2D,基于真实数据驱动生成水动力模型(根据不同时刻下每个网格的流向、流速、高程、水位)


二维水动力模型考虑了水流在平面上的变化,适用于河道弯曲、水流方向多变的情况。这种模型能够更准确地反映水流在平面上的分布情况,适用于需要精确模拟水流动态的场景,如城市排水系统设计、洪水模拟。二维模型的优势在于能够提供更详细的水流信息,但计算复杂度较高,需要更多的计算资源和时间。


二维水动力效果2.gif

本篇文章主要介绍在DTS 数字孪生引擎中实现二维水动力效果。在DTS SDK中开放了 HydroDynamic2D对象 添加二维水动力,并可以通过多种数据源进行添加,如 Bin、Sdb、Shp、Tif 的方式。


本文章主要介绍shp加载的方式,这种方式相对其他方式会更简单通用。


shp数据源添加方式


所需数据源


二维水动力是用数据驱动生成渲染效果的接口,所以数据源及其重要。


要利用shp为数据源进行添加,使用的是addByShp()方法,其与数据源相关的参数有两个:shpFilePath shpDataFilePath


shpFilePath其实就是水动力模型中水面网格的范围与高程,shpDataFilePath则代表每个网格的水深以及流速、流向



  • shpFilePath: 添加二维水动力模型整体范围的shp文件路径,取值示例:"C:/shpFile/xxx.shp"。

    • 此shp文件包含水动力模型所有网格的范围
    • shp类型为Polygon
    • 坐标系必须与工程坐标系保持一致
    • 必须包含 ID和Elev 两个字段:ID是网格ID;Elev是网格的高程值,单位是米



image-20241216172254779.png

  • shpDataFilePath: 可选参数,仅在update()方法执行生效。更新二维水动力模型时包含水面网格的dat类型文件路径,取值示例:"C:/datFile/xxx.dat"。
  • dat文件是一种二进制文件,它提取了某一时刻包含的所有水面网格的信息,并把这些信息依次写入了二进制文件dat。
  • 一个水面网格信息包含如下一组四个值:id (int),h (double),u(double),v(double),必须完全符合顺序以及数据类型。
  • id对应shp属性表ID字段,h是网格对应的水深(单位是米),uv是流速和流向(单位米/秒,u朝东,v朝北)。
  • 更新效果需要准备多个时刻的.dat文件,如下图所示

    image-20241216172314255.png

添加方法


1、准备测试数据

这里给大家准备好了一些数据资源,包括了实现的数据源、代码以及dat数据转换的程序,大家可以自行下载测试



百度网盘数据资源连接:pan.baidu.com/s/1XS3UDkrB…




  • 【文件资源】@path : 放到cloud文件资源路径
  • 【示例代码】code : demo源代码,直接用demo工程场景运行即可
  • 【dat数据转换】jsonToDat : json转dat代码,分别含有node.js、java、python示例代码

准备好两份数据分别是shpFilePath填写的shp文件,以及shpDataFilePath填写的dat文件集。文件可以直接用本地路径读取,建议放置到Cloud文件资源路径下,用@path的方式引用


这里可以用孪创启航营给大家准备的数据进行测试,在提供的文件夹的【文件资源】@path\【孪创启航营】HydroDynamic2D


2、通过shp网格数据初始化水动力模型

通过add()初始化水动力模型,并使用focus()定位到网格位置,但没有具体内容,还需要调用update添加.dat数据驱动效果。


add()参数文章末尾有详解


//添加shp数据源
fdapi.hydrodynamic2d.clear()

let hydrodynamic2d_add = {
id: 'hdm_shp', // HydroDynamic2D对象ID
collision: false, //开启碰撞sd
displayMode: 0, // 显示样式,取值范围:[0,1],0水体样式(默认值),1热力样式
waterMode: 0, // 水面显示模式,枚举类型 0 水动画模式 ,1水仿真模式,2 水流向模式
arrowColor: [1, 1, 1, 0], // 箭头颜色和透明度
speedFactor: 0.1, // 速度因子
rippleDensity: 1, // 水波纹辐射强度
rippleTiling: 3, // 水波纹辐射平铺系数
shpFilePath: '@path:【孪创启航营】HydroDynamic2D/shp/grid.shp' // 添加二维水动力模型整体范围的shp文件路径
}
await fdapi.hydrodynamic2d.add(hydrodynamic2d_add)

await fdapi.hydrodynamic2d.focus('hdm_shp', 200)

3、根据.dat更新水动力模型

写一个定时器,根据不同时刻,调用hydrodynamic2d.update()更新shpDataFilePath路径,达到水动力更新的效果。



  • 参数updateTime 是更新动画的插值时间,单位为秒,一般与更新定时器的时间一致即可。

let index = 0
let hydrodynamicModel_for_update = {
id: 'hdm_shp', // HydroDynamic2D对象ID
updateTime: 1, // 更新动画的插值时间
shpDataFilePath: ''// 更新二维水动力模型时包含水面网格的dat类型文件路径
}

// 使用dat数据填充shp网格
let updateTimer = setInterval(async () => {
hydrodynamicModel_for_update.shpDataFilePath = '@path:【孪创启航营】HydroDynamic2D/dat/hydrodynamic_' + index + '.dat'

if (index > 9) {
clearInterval(updateTimer)
} else {
await __g.hydrodynamic2d.update(hydrodynamicModel_for_update) // 水动力更新
index = index + 1
}
}, 1000)

通过以上就可以达成二维水动力的创建以及更新了。


4、实现二维水动力热力效果

二维水动力支持热力效果,可以根据.dat文件中的水深字段进行配色


二维水动力热力效果.gif

仅需要把add()中的displayMode参数设置为1热力样式,再通过valueRangecolors进行热力样式的调整



  • valueRange (array) ,二维水动力模型颜色插值对应的数值区间
  • colors (object) 二维水动力模型自定义调色板对象,包含颜色渐变控制、无效像素颜色和调色板区间数组

    • gradient (boolean) 是否渐变
    • invalidColor (Color) 无效像素点的默认颜色,默认白色
    • colorStops (array) 调色板对象数组,每一个对象包含热力值和对应颜色值,结构示例:[{"value":0, "color":[0,0,1,1]}],每一个调色板对象支持以下属性:

      • color (Color) 值对应的调色板颜色
      • value (number) 值



    const addHeat = async () => {
    fdapi.hydrodynamic2d.clear()

    let hydrodynamic2d_add = {
    id: 'hdm_shp_heat', // HydroDynamic2D对象ID
    offset: [0, 0, 0], // 二维水动力模型的整体偏移,默认值:[0, 0, 0]
    displayMode: 1, // 显示样式,取值范围:[0,1],0水体样式(默认值),1热力样式
    waterMode: 2, // 水面显示模式,枚举类型 0 水动画模式 ,1水仿真模式,2 水流向模式
    arrowColor: [1, 1, 1, 0.5], // 箭头颜色和透明度
    collision: false, //开启碰撞sd
    arrowTiling: 3, // 箭头平铺系数
    speedFactor: 0.1, // 速度因子
    rippleDensity: 1, // 水波纹辐射强度
    rippleTiling: 2, // 水波纹辐射平铺系数
    shpFilePath: '@path:【孪创启航营】HydroDynamic2D/shp/grid_heat.shp',

    valueRange: [1, 1.3], // 二维水动力模型颜色插值对应的数值区间
    alphaMode: 1, //使用colors色带透明度
    colors: {
    gradient: true,
    invalidColor: [0, 0, 0, 1],
    colorStops: [
    {
    value: 0,
    color: [0, 0, 1, 0.2]
    },
    {
    value: 0.25,
    color: [0, 1, 1, 0.2]
    },
    {
    value: 0.5,
    color: [0, 1, 0, 0.2]
    },
    {
    value: 0.75,
    color: [1, 1, 0, 0.2]
    },
    {
    value: 1,
    color: [1, 0, 0, 0.2]
    }
    ]
    }
    }
    await fdapi.hydrodynamic2d.add(hydrodynamic2d_add)

    await fdapi.hydrodynamic2d.focus('hdm_shp_heat', 200)

    let index = 0
    let hydrodynamicModel_for_update = {
    id: 'hdm_shp_heat',
    updateTime: 1,
    shpDataFilePath: ''
    }

    //使用dat数据填充shp网格
    let updateTimer = setInterval(async () => {
    hydrodynamicModel_for_update.shpDataFilePath = '@path:【孪创启航营】HydroDynamic2D/dat/hydrodynamic_' + index + '.dat'

    if (index > 9) {
    clearInterval(updateTimer)
    } else {
    await __g.hydrodynamic2d.update(hydrodynamicModel_for_update)
    index = index + 1
    }
    }, 1000)
    }



demo运行


缺乏数据源的小伙伴可以尝试运行我们准备好的demo示例,感受一下水动力的效果与参数调用。



  1. **下载资源:**下载百度网盘数据资源
  2. 替换资源:把【文件资源】@path的文件放到cloud文件资源路径下
  3. **启动cloud:**cloud启动demo工程
  4. 替换sdk:【示例代码】code\lib\aircity中的ac.min.jsac.min.js,替换为cloud右上角"sdk"路径的对应文件
  5. **运行:**双击运行示例代码】code\二维水动力.html 代码里的 shpFilePathshpDataFilePath路径得和第2步中一致

二维水动力效果1.gif

.dat 数据转换?


在数据源中,网格对应的水深、流速、流向数据,大家获取到可能不是标准的dat数据,有可能是json、csv甚至是excel数据。所以这里教大家如何把常见的数据转为dat二进制文件!


大象进冰箱需要三步,咱们转数据也需要三步



  1. 解析数据:读取文件,把不同数据源中的id,h,u,v(网格id、水深、流速流向u、流速流向v)提取出来。
  2. 转为二进制数据:把id,h,u,v转化为二进制的格式。
  3. 文件创建并写入:把二进制的格式数据保存为.dat文件

其中解析数据每份数据可能各不相同,都需要单独编写。这里我以一个json数据格式为例子,教大家如何转换为.dat,例如我们有一个data.json文件数据示例如下:


[
{
"index": 0,
"time": "08:30:00",
"data": [
{
"id": 0,
"h": 2,
"u": 0,
"v": 0
},
{
"id": 1,
"h": 2,
"u": 0,
"v": 0
}
]
},
{
"index": 1,
"time": "09:00:00",
"data": [
{
"id": 0,
"h": 2,
"u": 0,
"v": 0
},
{
"id": 1,
"h": 2.001,
"u": 0.1,
"v": 0.1
}
]
}
]


我们可以使用不同的编程手段来处理,如node.js、python、java,这里直接把转换的代码贴给大家~


注意:这三种编程手段都需要单独的安装对应的环境,如果没有环境可以选择一种自行百度安装



node官网:Node.js — 在任何地方运行 JavaScript


python官网:python.org


java官网:Java | Oracle



node.js


  1. 解析数据:使用require('./data.json')同步地引入并解析JSON数据文件,将其内容存储在jsonData变量中。
  2. 转为二进制数据:pamarToBuffer函数将idhuv转换为小端字节序的二进制Buffer。
  3. 文件创建并写入:遍历JSON数据,对每个时间点,使用path.join构建.dat文件路径,fs.createWriteStream创建写入流,datStream.write写入二进制Buffer,最后datStream.end关闭写入流。

// 引入必要的模块
const fs = require('fs') // 用于文件的读写操作
const path = require('path') // 用于处理文件路径
const jsonData = require('./data.json') // 引入 JSON 数据文件

// 确保 ./dat 目录存在
const datDir = path.join(__dirname, 'dat')
if (!fs.existsSync(datDir)) {
fs.mkdirSync(datDir)
}

// 遍历 JSON 数据 time_i 是当前时间点的索引
for (let time_i = 0; time_i < jsonData.length; time_i++) {
// 创建 .dat 文件路径
const datFilePath = path.join(datDir, `hydrodynamic_${time_i}.dat`)
// 创建写入流
const datStream = fs.createWriteStream(datFilePath)

// 获取并遍历时间点的数据
const timeData = jsonData[time_i].data
for (let grid_i = 0; grid_i < timeData.length; grid_i++) {
// 数据转换和写入
const { id, h, u, v } = timeData[grid_i]
const buffer = pamarToBuffer(id, h, u, v)
datStream.write(buffer)
}

datStream.end()
}

function pamarToBuffer(id, h, u, v) {
// 创建一个 Buffer 来存储二进制数据
const buffer = Buffer.alloc(4 + 8 + 8 + 8) // 分配足够的空间:4 字节用于 id,3 个 8 字节用于 double 值
// 向 Buffer 中写入数据
buffer.writeInt32LE(id, 0) // 从索引 0 开始写入 id(32 位整数)
buffer.writeDoubleLE(h, 4) // 从索引 4 开始写入 h(64 位浮点数)
buffer.writeDoubleLE(u, 12) // 从索引 12 开始写入 u(64 位浮点数)
buffer.writeDoubleLE(v, 20) // 从索引 20 开始写入 v(64 位浮点数)

return buffer
}


python


  1. 解析数据:使用json.load(f)方法从打开的JSON文件对象f中读取并解析数据,将JSON格式的数据转换为Python的字典或列表结构,存储在变量json_data中。
  2. 转为二进制数据:使用struct.pack('=iddd', id, h, u, v)方法将这些数据按照指定的格式(=表示本地字节顺序,i表示整数,d表示双精度浮点数)打包成二进制数据。
  3. 文件创建并写入:使用open函数以二进制写入模式打开(或创建)文件,最后通过write方法将转换好的二进制数据写入到该文件中。

import json
import os
import struct

# 读取JSON文件
json_file_path = './data.json'
with open(json_file_path, 'r') as f:
json_data = json.load(f)

# 定义输出目录
output_dir = os.path.join(os.getcwd(), 'dat')
if not os.path.exists(output_dir):
os.makedirs(output_dir)

# 遍历JSON数据
for time_i, time_node in enumerate(json_data):
# 创建.dat文件路径
dat_file_path = os.path.join(output_dir, f"hydrodynamic_{time_i}.dat")

# 打开文件以二进制写入模式
with open(dat_file_path, 'wb') as dat_file:
# 获取并遍历时间点的数据
time_data = time_node['data']
for grid_i, data_element in enumerate(time_data):

# 数据转换和写入
id = int(data_element['id'])
h = float(data_element['h'])
u = float(data_element['u'])
v = float(data_element['v'])

# 使用struct模块将数据转换为二进制格式
binary_data = struct.pack('=iddd', id, h, u, v)
# 写入二进制数据到文件
dat_file.write(binary_data)

print("Data processing complete.")

Java

java需要安装对应的 jackson json解析依赖才能使用,这里给大家提供了一个最简洁的版本,只需要有了对应的java环境运行目录下的start.bat文件即可生成dat文件。



  1. 解析数据:使用ObjectMapperdata.json文件中读取JSON数据,并解析为TimePoint对象的列表。
  2. 转为二进制数据:convertToBytes方法将TimePointData对象的idhuv字段转换为小端字节序的字节数组。
  3. 文件创建并写入:遍历TimePoint列表,为每个时间点创建.dat文件,并使用FileOutputStream将转换后的字节数组写入文件。

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

class TimePointData {
int id;
double h;
double u;
double v;

public TimePointData(int id, double h, double u, double v) {
this.id = id;
this.h = h;
this.u = u;
this.v = v;
}

}

class TimePoint {
List<TimePointData> data;

public TimePoint(List<TimePointData> data) {
this.data = data;
}

}

public class JsonToDatConverter {

public static void main(String[] args) {
String jsonFilePath = "data.json";
String datDir = "dat";

// 读取JSON文件
ObjectMapper objectMapper = new ObjectMapper();
try {
JsonNode rootNode = objectMapper.readTree(Files.newInputStream(Paths.get(jsonFilePath), StandardOpenOption.READ));
List<TimePoint> timePoints = parseJsonToTimePoints(rootNode);

Path dirPath = Paths.get(datDir);
if (!Files.exists(dirPath)) {
Files.createDirectory(dirPath);
}

for (int time_i = 0; time_i < timePoints.size(); time_i++) {
TimePoint timePoint = timePoints.get(time_i);
String datFilePath = Paths.get(datDir, "hydrodynamic_" + time_i + ".dat").toString();

try (FileOutputStream fos = new FileOutputStream(datFilePath)) {
for (TimePointData data : timePoint.data) {
byte[] bytes = convertToBytes(data.id, data.h, data.u, data.v);
fos.write(bytes);
}
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}

private static List<TimePoint> parseJsonToTimePoints(JsonNode rootNode) {
if (rootNode == null || !rootNode.isArray()) {
throw new IllegalArgumentException("Invalid JSON structure: 'timePoints' field is missing or not an array");
}

List<TimePoint> timePoints = new ArrayList<>();
for (JsonNode timePointNode : rootNode) {
JsonNode dataNode = timePointNode.get("data");
if (dataNode == null || !dataNode.isArray()) {
throw new IllegalArgumentException(
"Invalid JSON structure: 'data' field is missing or not an array within a 'timePoints' object");
}

List<TimePointData> dataList = new ArrayList<>();
for (JsonNode dataItemNode : dataNode) {
int id = dataItemNode.get("id").asInt();
double h = dataItemNode.get("h").asDouble();
double u = dataItemNode.get("u").asDouble();
double v = dataItemNode.get("v").asDouble();
dataList.add(new TimePointData(id, h, u, v));
}

timePoints.add(new TimePoint(dataList));
}

return timePoints;
}

private static byte[] convertToBytes(int id, double h, double u, double v) {
ByteBuffer buffer = ByteBuffer.allocate(4 + 8 + 8 + 8).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(id);
buffer.putDouble(h);
buffer.putDouble(u);
buffer.putDouble(v);
return buffer.array();
}
}

二维水动力添加参数详解


通用参数

通用参数比较简单理解,这里就简单列举出来



  • id (string) HydroDynamic2D对象ID
  • groupId (string) 可选,Gr0up分组
  • userData (string) 可选,用户自定义数据
  • offset (array) 二维水动力模型的整体偏移,默认值:[0, 0, 0]
  • collision (boolean) 是否开启碰撞,注意:开启后会影响加载效率

数据参数

数据参数前面介绍所需数据源已有详细介绍



  • shpFilePath(string)添加二维水动力模型整体范围的shp文件路径,取值示例:"C:/shpFile/xxx.shp"。

    • 此shp文件包含水动力模型所有网格的范围
    • shp类型为Polygon
    • 坐标系必须与工程坐标系保持一致
    • 必须包含 ID和Elev 两个字段:ID是网格ID;Elev是网格的高程值,单位是米


  • shpDataFilePath (string)可选参数,仅在update()方法执行生效。更新二维水动力模型时包含水面网格的dat类型文件路径,取值示例:"C:/datFile/xxx.dat"。

    • 注意:dat文件是一种二进制文件,它提取了某一时刻包含的所有水面网格的信息,并把这些信息依次写入了二进制文件dat,一个水面网格信息包含如下一组四个值:id,h,u,v。id对应shp属性表ID字段(int类型),h是网格对应的水深(double类型,单位是米),uv是流速和流向(double类型,单位米/秒,u朝东,v朝北)。



显示样式参数


  • displayMode (number) 显示样式,取值范围:[0,1],0水体样式(默认值),1热力样式

    • displayMode为0时,样式就只需要控制waterModewaterColor设置水体样式

      • waterMode (number) 水面显示模型,枚举类型 0 水动画模式 ,1水仿真模式,2 水流向模式
      • waterColor (Color) 水体颜色和透明度,注意:仅在displayMode=0时生效


    • displayMode为1时,样式就需要通过valueRangecolors控制热力样式

      • valueRange (array) ,二维水动力模型颜色插值对应的数值区间
      • colors (object) 二维水动力模型自定义调色板对象,包含颜色渐变控制、无效像素颜色和调色板区间数组

        • gradient (boolean) 是否渐变
        • invalidColor (Color) 无效像素点的默认颜色,默认白色
        • colorStops (array) 调色板对象数组,每一个对象包含热力值和对应颜色值,结构示例:[{"value":0, "color":[0,0,1,1]}],每一个调色板对象支持以下属性:

          • color (Color) 值对应的调色板颜色
          • value (number) 值




      • colors代码示例
        // colors示例
        {
        gradient: true,// 是否渐变
        invalidColor: [0, 0, 0, 1],// 无效像素点的默认颜色
        colorStops: [
        {
        value: 0,
        color: [0, 0, 1, 1]
        },
        {
        value: 0.25,
        color: [0, 1, 1, 1]
        },
        {
        value: 0.5,
        color: [0, 1, 0, 1]
        },
        {
        value: 0.75,
        color: [1, 1, 0, 1]
        },
        {
        value: 1,
        color: [1, 0, 0, 0]
        }
        ]
        }






  • alphaComposite (boolean) 是否使用混合透明度 取值:true / false 默认:true
  • alphaMode (number) 透明模式,取值:[0,1],0 : 使用colors调色板的不透明度值 1 : 使用系统默认值

箭头相关参数

箭头方向根据每个格网的uv流向决定



  • arrowDisplayMode (number) 箭头显示模式 取值范围:[0,1],0默认样式(受arrowColor参数影响),1热力样式(受arrowColors调色板参数影响)

    • arrowDisplayMode 为0,则设置arrowAlphaMode = 0,并通过arrowColor调整箭头的颜色和透明度

      • arrowColor (Color) 箭头颜色和透明度


    • arrowDisplayMode 为1,则设置arrowAlphaMode = 1,并通过arrowColors调整箭头的颜色和透明度

      • arrowColors (object)箭头颜色调色板 仅在arrowDisplayMode=1时生效,河道箭头热力样式下的调色板配色对象,包含颜色渐变控制、无效像素颜色和调色板区间数组

        • 格式同上方的显示样式参数colors






  • arrowAlphaMode (number) 箭头透明度模式,仅在arrowDisplayMode=0时生效,取值:[0,1],0使用arrowColor的透明度,1使用调色板的透明度
  • arrowTiling (number) 箭头平铺系数 值越小则箭头越小越密集,反之则更大更疏松

箭头.png
水面效果参数


  • foamWidth (number) 泡沫宽度取值范围:[0~10000],默认值:1米
  • foamIntensity (number) 泡沫强度 取值范围:[0~1],默认值:0.5
  • speedFactor (number) 速度因子

速度因子.gif

  • flowThreshold (array) 水浪效果漫延的范围 即把水动力模型[minSpeed,maxSpeed],最小最大流速的范围映射到[0~~1],取值示例:[0.1,0.4],取值范围[0-1]

水浪效果漫延的范围.png

  • rippleDensity (number)水波纹辐射强度

水波纹辐射强度.gif

  • rippleTiling (number) 水波纹辐射平铺系数

水波纹辐射平铺系数.gif


以上就是本篇文章的所有内容,相信大家看完这篇文章后可以轻松的通过DTS实现二维水动力效果。


在DTS中还有各式各样的水分析相关接口,如FloodFill 水淹分析、Fluid 流体仿真对象、HydroDynamic1D 一维水动力、WaterFlowField 水流场,大家可以根据自身需求选择,这里给大家推荐一篇《开闸放水》的教程,后续也会陆续推出更多教程~


不再需要UE美术,前端轻松解决水利开闸放水难题!!!


作者:女前端浅入数字孪生
来源:juejin.cn/post/7452181029994971147

0 个评论

要回复文章请先登录注册