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

1,751 阅读17分钟

二维水动力 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) 可选,Group分组
  • 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美术,前端轻松解决水利开闸放水难题!!!