基于ThreeJS+DEM数据的三维水体建模与污染源扩散可视化实现

1. 前言与技术背景

最终效果图

在这里插入图片描述
在这里插入图片描述

配套仓库

在典型湖泊(水库)场景中,三维可视化往往同时需要满足三类目标:

  • 地形真实:需要从 DEM(GeoTIFF)还原真实地形起伏,并保证高程最值可信。
  • 影像真实:需要获取可用的卫星/航片影像作为地形贴图,并保证与 DEM 网格严格对齐(避免“看起来像贴上去但对不上”的错位)。
  • 水体可信与好看:水面不仅是一个平面;需要考虑岸线过渡、深浅变化、流动视觉、反射高光与法线扰动等。
  • 污染扩散可解释:污染源(点源/面源)在流场作用下的对流-扩散过程,需要可视化呈现,且参数可调、结果可解释。

本项目以 “三岔湖典型湖泊 Demo” 为例,构建了一条从 DEM/STAC 数据获取 → 预处理 → 端侧 Three.js 三维渲染 → GPU 实时污染对流扩散仿真 的完整链路。

1.1 影像获取:为什么要做成“后端合成 + 前端裁剪对齐”

地形的视觉真实感主要来自影像贴图,因此影像获取需要满足工程上的两个约束:

  • 可缓存:影像瓦片请求数量大、受服务端限流影响明显,且直接在前端请求容易遇到 CORS 与不稳定。工程上更合理的是由后端一次性合并瓦片为单张影像,落盘缓存并复用。
  • 可对齐:瓦片合并结果通常对应“整瓦片范围”,实际覆盖 bbox 往往比请求 bbox 更大。如果直接把整张合成影像贴到 DEM 平面,会产生错位(岸线、道路等特征对不齐)。

因此本项目采用“分工明确”的方案:

  1. 后端负责拉瓦片并合成大图(同时写出 imagery_meta.json 记录 tileRange、matrixSet、bboxRequested/bboxActual 等对齐所需信息)
  2. 前端负责按瓦片矩阵全局像素坐标做裁剪,从合成大图中抠出严格对应 bboxRequested 的贴图,确保与 DEM 几何一致。

1.2 影像来源与可替换性

默认影像源由后端配置 dem.sanchalake.imageryTileUrl 指定(默认是 ArcGIS World_Imagery),并支持前端在 UI 中临时覆盖(“瓦片 URL(可选)”输入框)。无论替换为哪类 XYZ/WMTS 服务,只要满足瓦片模板 {z}/{y}/{x},后端均可按相同逻辑进行合并与缓存。

1.3 贴图效果:为什么“有影像就像真的”

在工程演示与验收中,影像贴图通常是最直观的“真实感增强”手段:

  • 地形信息更可辨:道路、水系、耕地/林地边界等纹理特征能直接帮助用户建立空间认知。
  • 水陆边界更可信:水域 mask 决定“哪里有水”,影像贴图决定“哪里像水/岸线”,两者叠加能显著提升观感与可信度。
  • 调参更高效:水位、岸线 feather、深浅色谱、污染浓度叠加等参数是否合理,往往需要在真实纹理背景下才看得出来。

项目内已集成 “影像(z16)” 一键准备与贴图开关(z≥16 才启用),你可以在 Demo 面板中直接体验地形贴图效果(见“数据准备”与“水体渲染参数”区域):

  • 影像准备入口:/api/dem/sanchalake/imagery/prepare(前端按钮 “影像(z16)”)
  • 贴图加载与严格对齐:loadAlignedImageryTexture()

2. 整体系统设计与实现思路

2.1 系统分层与职责划分

  • 后端(Spring Boot + GeoTools/JTS)
    • Step1:从 STAC 获取 DEM GeoTIFF,按目标网格重采样,输出二进制与可视化产物。
    • Step2:加载 OSM 湖泊面(relation multipolygon),栅格化为水域 mask(PNG)。
    • Step3/Step4(不在本文展开):生成/增强流场 flowmap,供水体波浪与污染对流使用。
  • 前端(Vue3 + Three.js)
    • 以 DEM heightmap 构建三维地形网格(PlaneGeometry 顶点抬升)。
    • 以 mask/flowmap/height-field 构建水面 ShaderMaterial,并做岸线平滑与高亮。
    • 以 WebGL RenderTarget ping-pong 在 GPU 上做污染对流-扩散仿真,结果直接注入水面 shader。

2.2 数据产物与文件契约

项目采用 “预处理产物文件 + 元数据 JSON” 的契约方式,使处理具备幂等性与可追踪性:

  • dem.f32:float32 little-endian,高程采样结果(按 grid 的 row-major)。
  • heightmap.png:将 dem.f32 按 min/max 归一化编码到 PNG(用于前端快速加载)。
  • dem_meta.json:包含 grid 与 min/max 等关键元数据,前端用于还原真实高程。
  • mask.png:水域掩膜,alpha=255 表示水域,alpha=0 表示非水域。

其中 Step1/Step2 的后端幂等逻辑集中在 [SanchalakePreprocessService]

  • Step1 缓存命中条件:dem_meta.json + dem.f32 + heightmap.png 均存在且元数据可信(elevationMin!=0 || elevationMax!=1 等判定)。
  • Step2 缓存命中条件:mask.png 存在。

2.3 总体流程图

STEP1_HEIGHTMAP

STEP2_WATER_MASK

前端刷新元信息 /api/dem/sanchalake/meta

actions: 是否需要 Step1/2/3/4

后端 Step1: STAC 下载 DEM GeoTIFF

GeoTools 读取 + CRS 变换 + 按 GridSpec 重采样

输出 dem.f32 / heightmap.png / dem_meta.json

后端 Step2: 读取 OSM relation multipolygon

EPSG:4326 -> target CRS 变换

按 GridSpec 栅格化输出 mask.png

前端加载 dem_meta + heightmap 生成地形

前端加载 mask/flowmap 构建水面

前端启动污染对流扩散 GPU ping-pong

污染浓度纹理注入水面 shader

2.4 快速开始(本地跑通)

克隆仓库

git clone https://github.com/clpz299/gis-gallery.git

后端启动(Spring Boot)

cd /gis-gallery
mvn -q -DskipTests spring-boot:run

前端启动(Vite + Vue3)

cd /gis-gallery/frontend
npm i
npm run dev

页面操作顺序(保证 5 分钟内出效果)

  1. 打开 Demo 页面,点击“刷新元信息”
  2. 点 “DEM 高度图(Step1)”
  3. 点 “水域掩码(Step2)”
  4. 点 “生成 flowmap(Step3)”(可选再做 “手工增强(Step4)”)
  5. 点 “启动 GPU 污染模拟”

2.5 后端配置项(dem.sanchalake)

后端参数由 SanchalakeProperties.java定义,推荐在 application.yml 覆盖(下例是工程常用配置,而非强制):

dem:
  sanchalake:
    minLon: 104.1833
    minLat: 30.0667
    maxLon: 104.8833
    maxLat: 30.6500
    resolutionMeters: 30.0
    targetCrs: EPSG:32648
    workDir: data/dem/sanchalake
    osmPath: src/main/resources/data/osm/sanchalake.osm
    osmRelationId: 13432
    demStacBaseUrl: https://earth-search.aws.element84.com/v1
    demStacCollection: cop-dem-glo-30
    imageryTileUrl: https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}
    imageryZoom: 16
    imageryFormat: png
    autoPrepareOnStartup: false
    autoPrepareFailFast: false
    localGeoTiffPath:

关键建议:

  • targetCrsresolutionMeters 决定 grid 的尺度与投影,会直接影响前端的地形尺寸、污染 UV 映射与数值稳定性。
  • workDir 作为场景产物的根目录,建议放到可持久化的位置(便于缓存复用与排障)。

2.6 接口清单(后端编排与资产输出)

后端入口集中在 SanchalakeDemoController.java,遵循统一响应包装:

{
  "code": 200,
  "message": "ok",
  "actions": ["STEP1_HEIGHTMAP", "STEP2_WATER_MASK"],
  "data": {}
}

工程落地时最重要的是 actions 驱动的“分步骤状态机”:前端先调用 /meta 拿到当前 scenario 的产物状态,再决定按钮可用性与下一步执行。

Step 编排接口

接口 方法 作用 关键入参
/api/dem/sanchalake/meta GET/POST 获取 scenario + assets + actions POST 可带 bbox(double[4])
/api/dem/sanchalake/step1 POST 生成 heightmap + dem_meta + dem_f32 localGeoTiffPathbbox
/api/dem/sanchalake/step2 POST 生成水域 mask bbox
/api/dem/sanchalake/step3 POST 生成 flowmap(D8 / D∞) flowModelbbox
/api/dem/sanchalake/step4 POST 入/出水口手工增强 flowmap inlets/outlets
/api/dem/sanchalake/imagery/prepare POST 下载并拼接影像纹理 imageryZoom/imageryFormat/imageryUrl

资产输出接口(前端直接拉取)

资产 URL 类型
DEM 元数据 /assets/dem-meta?scenarioId=... JSON
高度图 /assets/heightmap?scenarioId=... PNG
水域掩膜 /assets/mask?scenarioId=... PNG
D8 flowmap /assets/flowmap?scenarioId=... PNG
D∞ flowmap /assets/flowmap-dinf?scenarioId=... PNG
增强 flowmap /assets/flowmap-enhanced?scenarioId=... PNG
flow dir(二进制) /assets/flow-dir?scenarioId=... octet-stream
影像纹理 /assets/imagery?scenarioId=...&zoom=...&format=... png/jpg/tif
影像元信息 /assets/imagery-meta?scenarioId=...&zoom=... JSON

2.7 工作目录与产物结构(排障必备)

所有处理结果落在 dem.sanchalake.workDir/scenarios/{scenarioId} 下(scenarioId 由 bbox 归一化生成)。建议工程排障时优先打开 meta 返回的 paths.workDir

典型目录结构(示意):

data/dem/sanchalake/
  scenarios/
    bbox_104p..._30p..._104p..._30p.../
      raw/
        dem_cop-dem-glo-30.tif
      processed/
        dem.f32
        heightmap.png
        dem_meta.json
        mask.png
        flow_rg.png
        flow_rg_dinf.png
        flow_rg_enhanced.png
        flow_dir.u8
        imagery_z16.png
        imagery_meta_z16.json
      sim/
        {simId}/
          sim_meta.json
          frame_0000.png
          frame_0001.png
          ...

2.8 影像贴图地形:设计思路与对齐策略

地形的“真实感”很大程度来自影像(卫星/航片)而不是纯色材质。工程落地时,影像贴图要解决两个问题:

  • 来源稳定:直接按 XYZ/WMTS 拉瓦片,在后端合成一张可缓存的大图,避免前端并发瓦片请求导致的 CORS、限流与不确定性。
  • 几何对齐:瓦片合成图的边界往往是“整瓦片范围”,与真实请求 bbox 存在 bboxActual ≠ bboxRequested,如果直接把整张合成图贴到 DEM 平面,会出现明显错位。

本项目的做法是:后端负责“下载与合成(可缓存)”,前端负责“按瓦片矩阵全局像素坐标精确裁剪(强对齐)”。

2.8.1 数据流(后端合成 + 前端对齐裁剪)
  1. 前端触发影像准备:POST /api/dem/sanchalake/imagery/prepare(支持指定 zoom/format/url),逻辑见SanchalakeImageryService.java
  2. 后端使用 TileUtils
    • 自动识别 TileMatrixSet(WebMercator / EPSG:4326 等)
    • 按 bbox 合并瓦片为单张影像并落地(PNG/JPG/TIF)
    • 同步写入 imagery_meta.json,字段包含 bboxRequested/bboxActual/tileRange/matrixSet/zoom/pixelWidth/pixelHeight
  3. 前端加载 imageryimagery-meta
    • 若 meta 获取失败,退化为直接加载整张合成图(可能略有错位,但保证有图可看)
    • 若 meta 存在,则按 bboxRequested 在瓦片矩阵中计算全局像素范围,再在 canvas 中裁剪出严格对齐 DEM 的贴图

前端实现集中在:

  • 资源 URL:imageryAssetUrl / imageryMetaUrl SanchalakeWaterDemo.vue]
2.8.2 对齐关键:用“瓦片矩阵全局像素坐标”裁剪

后端合成影像时不可避免会扩到整瓦片范围,因此必须把“请求 bbox”映射到 tileRange 对应的大图坐标系中再裁剪。

核心思路(前端):

  • bboxRequested 的四个角转换为当前 matrixSet + zoom 下的 全局像素坐标 (pxMin, pxMax)
  • 再减去合成大图的像素原点 origin = tileRange.minX/minY * 256,得到裁剪窗口 sx/sy/sw/sh
  • 使用 canvas 把大图裁剪成严格的 bboxRequested 范围,并生成 CanvasTexture

对应函数:

  • lonLatToGlobalPixel():支持 WebMercator 与 EPSG:4326 等矩阵集 SanchalakeWaterDemo.vue

工程要点:

  • 贴图只有在 zoom >= 16 时启用(降低合成图体积与加载压力),逻辑见 imageryZoomEnabled SanchalakeWaterDemo.vue
  • 裁剪窗口必须是有限且正尺寸,否则 fallback 为整图加载
2.8.3 前端贴图策略:可控开关 + 生命周期管理
  • 贴图开关:UI 通过 simulationState.render.showImagery 控制,统一在 applyImageryOverlay() 中应用到 terrainMaterial.map SanchalakeWaterDemo.vue
  • 色彩空间与各向异性:
    • 影像贴图设置为 SRGBColorSpace,并尽可能开启 anisotropy 提升倾斜视角清晰度(见 loadTexture(kind='color')
  • 缓存键:scenarioId|zoom|format,避免重复加载/重复贴图(见 currentImageryKeyloadedImageryKey
  • 资源释放:重建地形或切换贴图时主动 dispose() 旧纹理,避免显存持续增长(见 buildTerrain() 对 imageryTexture 的处理)
2.8.4 影像贴图最常见问题与排查
  • 贴图错位:优先检查 imagery_meta.json 是否存在且 bboxRequested/tileRange/matrixSet/zoom 正确;若 meta 获取失败或内容不全,会 fallback 为整图贴图导致错位。
  • 贴图模糊:检查 zoom 是否足够(建议 ≥16),以及纹理是否开启 mipmap/anisotropy(倾斜视角尤其明显)。
  • 贴图上下颠倒:检查 flipY 设置是否一致(本项目约定:大部分 Texture flipY=true,RT flipY=false),并确保 PlaneGeometry 的 UV 与贴图一致。

3. DEM 地形数据处理与三维地形搭建

3.0 Step1 调用方式(curl 级可复现)

工程落地建议先用 curl 跑通(便于判断是后端链路问题还是前端渲染问题):

curl -X POST 'http://localhost:8080/api/dem/sanchalake/step1' \
  -H 'Content-Type: application/json' \
  -d '{
    "bbox":[104.1833,30.0667,104.8833,30.6500],
    "localGeoTiffPath": null
  }'

成功返回中会给出 heightmapPng/demF32/metaJson 的绝对路径(便于定位文件落地情况),并同步返回新一轮 actions[]

3.1 Step1:DEM 获取(STAC → GeoTIFF)

Step1 的核心是:按给定 bbox(WGS84)在 STAC 中检索 DEM,选择最新(或最合适)条目,并下载其资产为 GeoTIFF。

关键点:

  • 请求包含 collection + bbox + limit
  • 选择策略:按 item properties 的 datetime 取最新。
  • 资产 href 归一化:支持 s3://bucket/keyhttps://bucket.s3.amazonaws.com/key

3.2 Step1:按目标网格重采样(GeoTIFF → GridSpec)

为了前后端一致的 “渲染坐标系/像元布局”,后端会按 GridSpec(crs、extent、cellSize、width、height)做一次采样:

  • 用 GeoTools 读取 GeoTIFF 成 GridCoverage2D,并拿到 srcCrs
  • 构造 dstCrs = grid.crs,计算 toSrc = CRS.findMathTransform(dstCrs, srcCrs, true)
  • 遍历目标网格每个像元中心点 (px, py)
    • dst -> src 变换到 GeoTIFF 的坐标系
    • worldToGrid 得到 gc.x/gc.y
    • 越界或非有限值写 NaN

3.3 Step1:产物编码(F32 + 高度图 PNG + 元数据)

后端输出三类产物:

  • dem.f32:便于后续数值计算(如水文/流场)使用。
  • heightmap.png:便于前端用 TextureLoader 快速加载(避免前端解析 GeoTIFF 的复杂性)。
  • dem_meta.json:保证前端可将 PNG 的 0…1 还原成真实高程,并能做投影与 UV 映射。

3.4 前端:用高度图构建三维地形网格

前端在 SanchalakeWaterDemo中完成:

  1. 拉取 dem_meta.json,读取 width/height/cellSize/min/max/crs/minX/minY/...
  2. 读取 heightmap.png 像素(RG 16bit),再反解为真实高程:
const v16 = r * 256 + g
const t = v16 / 65535.0
const z = min + t * (max - min)
  1. PlaneGeometry(sizeX, sizeY, w - 1, h - 1) 创建规则网格,将每个顶点的 y(或 z)写为高程,并 computeVertexNormals()
  2. 渲染材质使用 MeshStandardMaterial,并配合 DirectionalLight / HemisphereLight 提升地形立体感。

3.5 工程要点:heightmap 的编码契约(RG16 → 实数高程)

项目为了兼顾 “加载速度” 与 “高程可还原性”,选择将高程编码到 PNG 的 RG 两通道(16-bit):

  • 后端:根据 elevationMin/elevationMaxdem.f32 归一化为 0…1,并写入 RG(示意)。
  • 前端:用 r*256+g 还原 0…65535,再线性映射回真实高程范围。

工程注意事项:

  • elevationMin/elevationMax 必须是可信的真实最值,否则整个渲染与污染映射都会偏。
  • NaN 与 NoData 的处理要一致:后端写 NaN,前端在构建地形时最好已有兜底策略(当前实现以编码结果为准)。

4. 基于 ThreeJS 的真实水体效果渲染

水体渲染的工程目标不是 “水面像玻璃一样平”,而是:

  • 几何层:岸线与地形自然贴合(避免穿插/硬边),并在视距变化时保持性能可控。
  • 着色层:深度色谱(浅→深)、Fresnel 反射、高光、法线扰动、流动条纹/泡沫。
  • 数据层:mask/flowmap/height-field 与污染浓度纹理可以统一注入同一水面 shader。

4.1 水域 mask:从 OSM multipolygon 到 PNG

后端 Step2 中完成:

  • 从本地 OSM PBF/OSM 文件读取 relationId 对应的 multipolygon 几何。
  • EPSG:4326 -> targetCrs 做一次几何变换(JTS.transform)。
  • 对目标网格每个像元中心点做 prep.contains(point) 判定,写入 alpha 值:
    • inside → 255
    • outside → 0

这类实现虽然是 O(w*h) 的纯 CPU 栅格化,但胜在简单稳定,且能缓存。

4.2 水面静态 height-field:让水域“有形状”

仅有 mask 会导致水面内部缺乏层次。项目中额外构建了 uHeightFieldTex(水域权重/伪深度场),核心思路是:

  • 在水域内部,结合地形高程的相对低洼程度构造一个 0…1 的“盆地权重”;
  • 再做轻量平滑,使岸线过渡更自然;
  • 在顶点阶段让 terrainH + field * amplitude 参与水面高度计算,使岸线附近水面更贴地形、更自然。

4.3 岸线平滑与描边:Bezier + SDF mask

为了避免像元级 mask 的锯齿,项目中做了两类纹理:

  • uSmoothMaskTex:基于 Signed Distance 的 feather,控制岸线平滑过渡(决定 shorelineBlend)。
  • uShorelineTex:岸线描边纹理,用于高亮和增强轮廓。

核心步骤:

  1. 从 mask 像素抽取边界点集合;
  2. 构造边界环(loop),适当抽稀;
  3. 用二次贝塞尔在 canvas 上绘制平滑曲线,并合成;
  4. 从合成 mask 构造 signed-distance 并 smoothstep 得到 feather alpha。

4.4 水面动态:WebGL ping-pong 高度场

为增加动态水面起伏,项目在 GPU 上运行了一个简单的高度场传播(非严格物理),用两个 WebGLRenderTarget ping-pong:

  • 输入:uBaseFieldTex(静态水域权重)+ uFlowTex(流向)+ 上一帧 uCurrTex
  • 输出:下一帧的动态波面 uDynamicHeightTex
  • 每帧可做多次传播迭代(simPasses),并根据 FPS 自适应降级。

4.5 反射与高光:场景颜色抓取 + Fresnel

水面反射采用 “隐藏水面 → 渲染场景到 RT → 在水面 shader 中采样” 的方式:

  • 双缓冲:uSceneColorTex + uPrevSceneColorTex 做简单的时间混合,减轻闪烁。
  • 自适应:根据 FPS 自动调整反射 RT 分辨率与更新间隔。

4.6 工程落地参数表(水面 shader / 仿真)

水面相关的核心参数集中在 SanchalakeWaterDemo.vue顶部常量与 waterUniforms

参数 默认值 说明
TERRAIN_EXAGGERATION 3.2 地形垂向夸张系数
WATER_ALPHA 0.6 水面透明度基准
WATER_DEPTH_MAX 48.0 深度归一化上限(决定深浅配色)
WATER_HEIGHT_FIELD_RESOLUTION 512 静态 height-field 分辨率
WATER_SIM_RESOLUTION 256 动态高度场分辨率(RT)
WATER_SIM_PASSES 2 每帧传播迭代次数(会自适应降级)
uFlowSpeed 来自 UI 水流尺度(m/s),同时影响对流与水面流动效果
uWaveAmp/uWaveLen/uWaveSpeed 来自 UI 波浪幅度/波长/波速

纹理约定(工程最容易出错的部分):

  • maskTex:PNG alpha 表示水域,采样使用 NearestFilterflipY=true
  • flowTex:PNG RG 编码 flow 向量,取值区间映射到 [-1,1]flipY=true
  • uDynamicHeightTex(RT 输出):flipY=false(RT 默认约定),shader 侧按当前坐标系处理。

5. 污染源动态扩散可视化核心实现

污染扩散的核心是经典的 对流-扩散方程(Advection-Diffusion)

∂C/∂t + u·∇C = D∇²C + S - λC
  • C:浓度
  • u:流速(由 flowmap 给出方向,再由 flowSpeed 给出尺度)
  • D:扩散系数
  • S:源项(污染源注入)
  • λ:衰减(decay)

本项目以 GPU 纹理计算实现实时模拟,并将结果作为 uPollutionTex 注入水面 shader 叠加显示。

5.1 污染源纹理初始化:多源高斯注入

前端支持多点源输入,每个点源生成一张高斯纹理,最后做叠加合成:

  • 每个 source:createGaussianTextureForSource(res, worldW, worldH, source, sigmaMeters, peak)
  • 合成:createAdditiveCompositeTexture(res, sources, clampMax)

5.2 坐标映射:lon/lat → 投影坐标 → 局部坐标 → UV

污染源输入是经纬度,但仿真是在水面 UV(0…1)上进行,因此必须完成映射:

  1. lon/lat 投影到 demMeta.crs(关键:不能假设统一是 WebMercator)
  2. localX = projX - originXlocalY = projY - originY
  3. u = localX / worldWv = localY / worldH

项目中实现了 EPSG:3857EPSG:326xx(UTM) 的投影,按 demMeta.crs 自动选择。

这是一个典型踩坑点:如果 DEM 是 UTM,但仍用 WebMercator 投影,localX/localY 量级不匹配会导致 UV 越界 clamp,进而污染纹理采样为 0,画面看起来 “污染无效”。

5.3 GPU 对流(Advection):反向追踪 + 双线性采样

对流使用反向追踪(semi-Lagrangian)思想:

  • 当前像元 vUv 对应的上一时刻采样位置为 prevUv = vUv - duv
  • duv = (flow * flowSpeed * dt) / worldSize
  • 用双线性采样从上一帧浓度纹理取值,提升稳定性并减轻网格效应。

5.4 GPU 扩散(Diffusion):5 点拉普拉斯差分

扩散采用 5 点格式:

C' = C + αx(CE + CW - 2C) + αy(CN + CS - 2C)
αx = D·dt/dx²
αy = D·dt/dy²

为了稳定性,项目做了 CFL 约束与 alpha clamp:

  • 依据 dx = worldW / resdy = worldH / res 计算 dt 上限;
  • 若 dt 过大则 clamp 并输出提示(ui.pollution.lastStability)。

5.5 渲染集成:Turbo 色谱 + log/pow 非线性映射 + 透明度策略

污染结果直接叠加在水面 shader 中(片元阶段):

  • mapPollution() 支持 logpow 两种映射,提升低浓度可见性。
  • 色谱使用 turbo()(感知均匀、适合浓度表达)。
  • alpha 随浓度增加而增强,并且受水域 mask 与岸线 feather 约束,避免溢出到陆地。

5.6 工程落地参数表(污染对流-扩散)

污染仿真参数全部前端可调(便于对比与验收),推荐先按保守参数跑通再逐步增大:

参数 UI 字段 推荐起步 说明
分辨率 pollution.resolution 1024 2048 对显存/带宽要求更高
扩散系数 pollution.diffusion 0.10 m²/s;越大扩散越快,越容易要求 dt 更小
衰减率 pollution.decay 0.00 1/s;可模拟自然衰减/降解
源项 sigma pollution.sigmaMeters 650 m;控制初始污染团尺度
峰值浓度 pollution.peak 1.0 初始强度(相对值),会受 clamp 限制
浓度截断 pollution.clampMax 3.0 防止数值爆炸;也影响可视化映射
流速 hydro.flowSpeedMS 0.35 m/s;过大时需要更小 dt 才稳定
时间步长 hydro.dtSeconds 1 s;作为 dt 的尺度因子

稳定性策略(工程必须具备):

  • dt clamp:根据 dx/dy/D 做上限约束(CFL),否则扩散会出现发散或纹理“花屏”。
  • mask 裁剪:陆地区域直接置零,避免污染越界扩散到陆地。

6. 场景交互与功能拓展

6.1 “分步骤”交互与幂等式数据准备

前端面板将预处理拆成 Step1/2/3/4,配合后端 actions[] 控制按钮可用性:

  • 优点:链路可观测、可回溯;失败时不会污染后续步骤;结果可缓存复用。
  • UI 侧统一使用全局排他 Loading(withLoading()),避免多操作并发导致资源状态不一致。

6.1.1 actions 的工程含义(前后端协同约定)

  • STEP1_HEIGHTMAP:缺 DEM 产物(dem_meta/dem_f32/heightmap)
  • STEP2_WATER_MASK:缺水域 mask
  • STEP3_FLOW_D8 / STEP3_FLOW_DINF:缺 flowmap(允许选择模型)
  • STEP4_FLOW_ENHANCE:允许进行入/出水口增强
  • IMAGERY_DOWNLOAD:缺影像拼接产物

前端不直接判断文件是否存在,而是信任 actions,这样能避免 “前端逻辑与后端真实状态不一致”。

6.2 视角交互:OrbitControls + 水面 LOD 自适应

水面网格分段(segX/segY)会显著影响性能。项目中做了 “按相机距离自适应 profile”:

  • near/mid/far 分档,动态选择网格分段数;
  • 在 controls 结束交互时尝试重建水面网格,减少频繁重建带来的卡顿;

6.3 可扩展方向

  • 更多源项:面源/线源(沿岸排污口)、时间变化排放率(工况曲线)。
  • 更真实水动力:将流场从 “静态 flowmap + 速度尺度” 扩展为时变速度场,或接入外部水动力模型结果。
  • 多图层耦合:叠加风场、降雨径流、温盐场,形成多物理量协同展示。

7. 项目性能优化与踩坑总结

7.1 性能优化要点(工程可落地)

  • 渲染自适应降级
    • 水面高度场传播迭代次数按 FPS 降级(getAdaptiveWaterSimulationPasses())。
    • 反射 RT 分辨率与更新频率按 FPS 动态调整(getAdaptiveWaterReflectionScale/Interval())。
  • 纹理/几何生命周期管理
    • 任何重建都严格 dispose 旧的 geometry/material/texture/renderTarget,避免显存泄漏。
    • 污染仿真与水面仿真都是 ping-pong,重置时必须统一清理。
  • 分辨率权衡
    • 水面动态高度场使用 256^2(WATER_SIM_RESOLUTION),确保实时性。
    • 污染模拟支持 1024/2048 两档,通过 UI 切换以满足精度需求。

7.2 典型踩坑总结(可直接复用的经验)

  • DEM 全黑 / 高程范围异常

    • 现象:elevationMin=0 & elevationMax=1,高度图看似正常但实际高程全丢失。
    • 解决:后端采样必须读取原始像元值并计算真实 min/max,同时做坏缓存失效(Step1 meta suspicious 判定)。
  • 污染扩散无响应(最常见)

    • 现象:点击启动后无变化,浓度纹理全 0。
    • 根因:DEM 的 CRS 是 EPSG:326xx(UTM)却按 EPSG:3857 投影,导致 UV 越界。
    • 解决:按 demMeta.crs 自动选择投影(UTM/WebMercator),并输出 source 的 uvError 便于诊断。
  • 水面黢黑

    • 原因:ShaderMaterial 不会自动吃场景灯光。
    • 解决:在水面 shader 内显式引入 uSunDir/uSunIntensity/uAmbientIntensity 并实现光照项。
  • 纹理上下颠倒 / UV 不一致

    • 原因:不同 Texture 类型、RenderTarget、CanvasTexture 的 flipY 默认行为不同。
    • 解决:对所有纹理加载与创建明确设置 flipY(项目约定:大多数贴图 flipY=true,RT 输出通常 flipY=false)。

7.3 性能验收指标(落地可量化)

工程验收建议先给出 “最小可用指标”,避免只讨论观感:

  • 首屏时间:Step1 完成后从加载 heightmap 到地形首帧可交互 ≤ 3s(本机/局域网)
  • 稳定帧率:水面 + 反射 + 污染叠加开启,FPS ≥ 30(常见独显/苹果 M 系列)
  • 显存/内存:切换参数与重建水面/污染后显存不持续增长(纹理/RT 必须可回收)

7.4 排障清单(按优先级)

  1. /api/dem/sanchalake/metaactions[] 是否符合预期(决定流程是否正确)
  2. dem_meta.jsoncrs/min/max/width/height/cellSize 是否合理(决定渲染与投影)
  3. mask.png alpha 是否正确(决定水面是否渲染、污染是否裁剪)
  4. flowmap*.png 是否存在且数值不全为中性(决定对流是否发生)
  5. 前端 debug log 是否输出 source 的 uvError(快速判断投影是否选对)

8. 项目总结与展望

本项目在工程上打通了 “真实地形 + 真实水体观感 + 实时污染扩散可视化” 的闭环,核心优势体现在:

  • 链路清晰且可复用:后端负责数据契约与预处理,前端负责渲染与实时仿真,二者通过 dem_meta + png + mask 解耦。
  • 强交互与强解释:污染源参数可控、仿真稳定性可观测(dt clamp/误差输出),效果不再是“黑箱动画”。
  • 质量与性能兼顾:采用自适应 LOD、反射降级、分辨率分档等策略,在普通设备上也能保持可用帧率。

下一阶段可重点推进:

  • WebGPU 计算管线:将污染仿真从 WebGL RenderTarget 迁移到 WebGPU compute,以获得更高分辨率与更低成本的迭代次数。
  • 更严谨的水动力数据:引入外部水动力模型结果(时间维度、分层流速),并支持与污染物模型参数联动。
  • 工程化压测与回归:建立场景基准数据与自动化回归(帧率/显存/一致性),让“效果”与“性能”可持续迭代。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐