基于ThreeJS+DEM数据的三维水体建模与污染源扩散可视化实现
基于ThreeJS+DEM数据的三维水体建模与污染源扩散可视化实现
1. 前言与技术背景
最终效果图


配套仓库
- 推荐配套开源仓库:https://github.com/clpz299/gis-gallery.git ⭐,该仓库包含完整代码、示例数据与在线演示,克隆后即可直接运行。
在典型湖泊(水库)场景中,三维可视化往往同时需要满足三类目标:
- 地形真实:需要从 DEM(GeoTIFF)还原真实地形起伏,并保证高程最值可信。
- 影像真实:需要获取可用的卫星/航片影像作为地形贴图,并保证与 DEM 网格严格对齐(避免“看起来像贴上去但对不上”的错位)。
- 水体可信与好看:水面不仅是一个平面;需要考虑岸线过渡、深浅变化、流动视觉、反射高光与法线扰动等。
- 污染扩散可解释:污染源(点源/面源)在流场作用下的对流-扩散过程,需要可视化呈现,且参数可调、结果可解释。
本项目以 “三岔湖典型湖泊 Demo” 为例,构建了一条从 DEM/STAC 数据获取 → 预处理 → 端侧 Three.js 三维渲染 → GPU 实时污染对流扩散仿真 的完整链路。
1.1 影像获取:为什么要做成“后端合成 + 前端裁剪对齐”
地形的视觉真实感主要来自影像贴图,因此影像获取需要满足工程上的两个约束:
- 可缓存:影像瓦片请求数量大、受服务端限流影响明显,且直接在前端请求容易遇到 CORS 与不稳定。工程上更合理的是由后端一次性合并瓦片为单张影像,落盘缓存并复用。
- 可对齐:瓦片合并结果通常对应“整瓦片范围”,实际覆盖 bbox 往往比请求 bbox 更大。如果直接把整张合成影像贴到 DEM 平面,会产生错位(岸线、道路等特征对不齐)。
因此本项目采用“分工明确”的方案:
- 后端负责拉瓦片并合成大图(同时写出
imagery_meta.json记录 tileRange、matrixSet、bboxRequested/bboxActual 等对齐所需信息) - 前端负责按瓦片矩阵全局像素坐标做裁剪,从合成大图中抠出严格对应
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 总体流程图
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 分钟内出效果)
- 打开 Demo 页面,点击“刷新元信息”
- 点 “DEM 高度图(Step1)”
- 点 “水域掩码(Step2)”
- 点 “生成 flowmap(Step3)”(可选再做 “手工增强(Step4)”)
- 点 “启动 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:
关键建议:
targetCrs与resolutionMeters决定 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 | localGeoTiffPath、bbox |
/api/dem/sanchalake/step2 |
POST | 生成水域 mask | bbox |
/api/dem/sanchalake/step3 |
POST | 生成 flowmap(D8 / D∞) | flowModel、bbox |
/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 数据流(后端合成 + 前端对齐裁剪)
- 前端触发影像准备:
POST /api/dem/sanchalake/imagery/prepare(支持指定zoom/format/url),逻辑见SanchalakeImageryService.java - 后端使用
TileUtils:- 自动识别
TileMatrixSet(WebMercator / EPSG:4326 等) - 按 bbox 合并瓦片为单张影像并落地(PNG/JPG/TIF)
- 同步写入
imagery_meta.json,字段包含bboxRequested/bboxActual/tileRange/matrixSet/zoom/pixelWidth/pixelHeight
- 自动识别
- 前端加载
imagery与imagery-meta:- 若 meta 获取失败,退化为直接加载整张合成图(可能略有错位,但保证有图可看)
- 若 meta 存在,则按 bboxRequested 在瓦片矩阵中计算全局像素范围,再在 canvas 中裁剪出严格对齐 DEM 的贴图
前端实现集中在:
- 资源 URL:
imageryAssetUrl / imageryMetaUrlSanchalakeWaterDemo.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时启用(降低合成图体积与加载压力),逻辑见imageryZoomEnabledSanchalakeWaterDemo.vue - 裁剪窗口必须是有限且正尺寸,否则 fallback 为整图加载
2.8.3 前端贴图策略:可控开关 + 生命周期管理
- 贴图开关:UI 通过
simulationState.render.showImagery控制,统一在applyImageryOverlay()中应用到terrainMaterial.mapSanchalakeWaterDemo.vue - 色彩空间与各向异性:
- 影像贴图设置为
SRGBColorSpace,并尽可能开启 anisotropy 提升倾斜视角清晰度(见loadTexture(kind='color'))
- 影像贴图设置为
- 缓存键:
scenarioId|zoom|format,避免重复加载/重复贴图(见currentImageryKey与loadedImageryKey) - 资源释放:重建地形或切换贴图时主动
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/key转https://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中完成:
- 拉取
dem_meta.json,读取width/height/cellSize/min/max/crs/minX/minY/...。 - 读取
heightmap.png像素(RG 16bit),再反解为真实高程:
const v16 = r * 256 + g
const t = v16 / 65535.0
const z = min + t * (max - min)
- 用
PlaneGeometry(sizeX, sizeY, w - 1, h - 1)创建规则网格,将每个顶点的 y(或 z)写为高程,并computeVertexNormals()。 - 渲染材质使用
MeshStandardMaterial,并配合 DirectionalLight / HemisphereLight 提升地形立体感。
3.5 工程要点:heightmap 的编码契约(RG16 → 实数高程)
项目为了兼顾 “加载速度” 与 “高程可还原性”,选择将高程编码到 PNG 的 RG 两通道(16-bit):
- 后端:根据
elevationMin/elevationMax把dem.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:岸线描边纹理,用于高亮和增强轮廓。
核心步骤:
- 从 mask 像素抽取边界点集合;
- 构造边界环(loop),适当抽稀;
- 用二次贝塞尔在 canvas 上绘制平滑曲线,并合成;
- 从合成 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 表示水域,采样使用NearestFilter,flipY=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)上进行,因此必须完成映射:
lon/lat投影到demMeta.crs(关键:不能假设统一是 WebMercator)- 用
localX = projX - originX、localY = projY - originY - 用
u = localX / worldW、v = localY / worldH
项目中实现了 EPSG:3857 与 EPSG: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 / res、dy = worldH / res计算 dt 上限; - 若 dt 过大则 clamp 并输出提示(
ui.pollution.lastStability)。
5.5 渲染集成:Turbo 色谱 + log/pow 非线性映射 + 透明度策略
污染结果直接叠加在水面 shader 中(片元阶段):
mapPollution()支持log与pow两种映射,提升低浓度可见性。- 色谱使用
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:缺水域 maskSTEP3_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())。
- 水面高度场传播迭代次数按 FPS 降级(
- 纹理/几何生命周期管理
- 任何重建都严格 dispose 旧的
geometry/material/texture/renderTarget,避免显存泄漏。 - 污染仿真与水面仿真都是 ping-pong,重置时必须统一清理。
- 任何重建都严格 dispose 旧的
- 分辨率权衡
- 水面动态高度场使用
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)。
- 原因:不同 Texture 类型、RenderTarget、CanvasTexture 的
7.3 性能验收指标(落地可量化)
工程验收建议先给出 “最小可用指标”,避免只讨论观感:
- 首屏时间:Step1 完成后从加载 heightmap 到地形首帧可交互 ≤ 3s(本机/局域网)
- 稳定帧率:水面 + 反射 + 污染叠加开启,FPS ≥ 30(常见独显/苹果 M 系列)
- 显存/内存:切换参数与重建水面/污染后显存不持续增长(纹理/RT 必须可回收)
7.4 排障清单(按优先级)
/api/dem/sanchalake/meta的actions[]是否符合预期(决定流程是否正确)dem_meta.json的crs/min/max/width/height/cellSize是否合理(决定渲染与投影)mask.pngalpha 是否正确(决定水面是否渲染、污染是否裁剪)flowmap*.png是否存在且数值不全为中性(决定对流是否发生)- 前端 debug log 是否输出 source 的
uvError(快速判断投影是否选对)
8. 项目总结与展望
本项目在工程上打通了 “真实地形 + 真实水体观感 + 实时污染扩散可视化” 的闭环,核心优势体现在:
- 链路清晰且可复用:后端负责数据契约与预处理,前端负责渲染与实时仿真,二者通过
dem_meta + png + mask解耦。 - 强交互与强解释:污染源参数可控、仿真稳定性可观测(dt clamp/误差输出),效果不再是“黑箱动画”。
- 质量与性能兼顾:采用自适应 LOD、反射降级、分辨率分档等策略,在普通设备上也能保持可用帧率。
下一阶段可重点推进:
- WebGPU 计算管线:将污染仿真从 WebGL RenderTarget 迁移到 WebGPU compute,以获得更高分辨率与更低成本的迭代次数。
- 更严谨的水动力数据:引入外部水动力模型结果(时间维度、分层流速),并支持与污染物模型参数联动。
- 工程化压测与回归:建立场景基准数据与自动化回归(帧率/显存/一致性),让“效果”与“性能”可持续迭代。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)