我不允许还有人不知道前端实现时刻洪水模拟的方法!🔥
二维水动力 HydroDynamic2D
二维水动力介绍
二维水动力模型对象 HydroDynamic2D,基于真实数据驱动生成水动力模型(根据不同时刻下每个网格的流向、流速、高程、水位)
二维水动力模型考虑了水流在平面上的变化,适用于河道弯曲、水流方向多变的情况。这种模型能够更准确地反映水流在平面上的分布情况,适用于需要精确模拟水流动态的场景,如城市排水系统设计、洪水模拟。二维模型的优势在于能够提供更详细的水流信息,但计算复杂度较高,需要更多的计算资源和时间。
本篇文章主要介绍在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是网格的高程值,单位是米
- 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
文件,如下图所示
添加方法
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
文件中的水深字段进行配色
仅需要把add()
中的displayMode
参数设置为1热力样式,再通过valueRange
、colors
进行热力样式的调整
- 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示例,感受一下水动力的效果与参数调用。
- **下载资源:**下载百度网盘数据资源
- 替换资源:把
【文件资源】@path
的文件放到cloud文件资源路径下 - **启动cloud:**cloud启动demo工程
- 替换sdk:
【示例代码】code\lib\aircity
中的ac.min.js
与ac.min.js
,替换为cloud右上角"sdk"路径的对应文件 - **运行:**双击运行
示例代码】code\二维水动力.html
代码里的shpFilePath
、shpDataFilePath
路径得和第2步中一致
.dat 数据转换?
在数据源中,网格对应的水深、流速、流向数据,大家获取到可能不是标准的dat
数据,有可能是json、csv甚至是excel数据。所以这里教大家如何把常见的数据转为dat
二进制文件!
大象进冰箱需要三步,咱们转数据也需要三步
- 解析数据:读取文件,把不同数据源中的
id,h,u,v
(网格id、水深、流速流向u、流速流向v)提取出来。 - 转为二进制数据:把
id,h,u,v
转化为二进制的格式。 - 文件创建并写入:把二进制的格式数据保存为
.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
- 解析数据:使用
require('./data.json')
同步地引入并解析JSON数据文件,将其内容存储在jsonData
变量中。 - 转为二进制数据:
pamarToBuffer
函数将id
、h
、u
、v
转换为小端字节序的二进制Buffer。 - 文件创建并写入:遍历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
- 解析数据:使用
json.load(f)
方法从打开的JSON文件对象f
中读取并解析数据,将JSON格式的数据转换为Python的字典或列表结构,存储在变量json_data
中。 - 转为二进制数据:使用
struct.pack('=iddd', id, h, u, v)
方法将这些数据按照指定的格式(=
表示本地字节顺序,i
表示整数,d
表示双精度浮点数)打包成二进制数据。 - 文件创建并写入:使用
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文件。
- 解析数据:使用
ObjectMapper
从data.json
文件中读取JSON数据,并解析为TimePoint
对象的列表。 - 转为二进制数据:
convertToBytes
方法将TimePointData
对象的id
、h
、u
、v
字段转换为小端字节序的字节数组。 - 文件创建并写入:遍历
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时,样式就只需要控制waterMode
、waterColor
设置水体样式- waterMode (number) 水面显示模型,枚举类型 0 水动画模式 ,1水仿真模式,2 水流向模式
- waterColor (Color) 水体颜色和透明度,注意:仅在displayMode=0时生效
- 当
displayMode
为1时,样式就需要通过valueRange
、colors
控制热力样式- 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) 箭头平铺系数 值越小则箭头越小越密集,反之则更大更疏松
水面效果参数
- foamWidth (number) 泡沫宽度取值范围:[0~10000],默认值:1米
- foamIntensity (number) 泡沫强度 取值范围:[0~1],默认值:0.5
- speedFactor (number) 速度因子
- flowThreshold (array) 水浪效果漫延的范围 即把水动力模型[minSpeed,maxSpeed],最小最大流速的范围映射到[0~~1],取值示例:[0.1,0.4],取值范围[0-1]
- rippleDensity (number)水波纹辐射强度
- rippleTiling (number) 水波纹辐射平铺系数
以上就是本篇文章的所有内容,相信大家看完这篇文章后可以轻松的通过DTS实现二维水动力效果。
在DTS中还有各式各样的水分析相关接口,如FloodFill 水淹分析、Fluid 流体仿真对象、HydroDynamic1D 一维水动力、WaterFlowField 水流场,大家可以根据自身需求选择,这里给大家推荐一篇《开闸放水》的教程,后续也会陆续推出更多教程~
来源:juejin.cn/post/7452181029994971147