如何在真实地图上构建灾害模拟:使用 Mapbox GL JS 和 Three.js 实现动画画布图层与倒塌的 3D 建筑

如何在真实地图上构建灾害模拟:使用 Mapbox GL JS 和 Three.js 实现动画画布图层与倒塌的 3D 建筑

💡 原文英文,约11800词,阅读约需43分钟。
📝

内容提要

本文介绍在网页地图上实现灾害模拟动画的方法,核心原则是地面效果须用地图坐标而非屏幕坐标绘制。以海啸和地震为例,讲解将画布注册为地图数据源、加载真实高程、用广度优先搜索模拟洪水、叠加视频纹理,以及用 three.js 在地图 GL 上下文中渲染倒塌建筑、碎片与尘土,并抖动容器而非相机。

🔎

延伸解读

地面效果必须用地图坐标,而非屏幕坐标

文章反复强调的核心原则是:洪水、烟雾等贴地效果必须绘制在地图坐标系中,而不是覆盖在 DOM 上的画布。把画布注册为地图的 canvas source 后,地图会自动处理倾斜、旋转和地形起伏,效果会随地图一起变换。若用屏幕坐标,一旦倾斜或旋转地图,效果就会错位,且没有透视和遮挡关系。

洪水模拟依赖真实高程与连通性搜索

文章用广度优先搜索模拟洪水,从海岸线向内陆扩展,只有水面高于地面时水才能到达。高程数据来自 Terrarium 或 Mapbox Terrain-RGB 瓦片,解码后存入与地图瓦片对齐的网格。这样洪水会沿河谷上溯、被堤坝阻挡,而不是简单地淹没所有低洼区域。

建筑倒塌需要两种 3D 技术配合

地图原生的 fill-extrusion 适合大量静态建筑,但无法移动。对于需要倒塌的建筑,文章用 three.js 自定义图层在地图 WebGL 上下文中渲染真实物体,并共享深度缓冲。倒塌时,先缩小挤出高度,再替换为 three.js 物体,最后变成瓦砾,从而兼顾性能与视觉效果。

实现中的关键陷阱与性能考量

文章指出几个易错点:画布需设置 crossOrigin 避免污染;canvas source 必须开启 animate 才能逐帧更新;setFeatureState 需要要素 ID,而 Mapbox Streets 建筑没有,需复制到自有 GeoJSON 源并 promoteId。性能上,洪水填充只需运行一次,逐帧循环应避免重复创建 ImageData,并只遍历受淹单元。

❓

Q&A

为什么在网页地图上做灾害模拟动画时,不能直接把 canvas 覆盖在地图 DOM 上?

因为覆盖的 canvas 附着在相机上,而不是地图表面。地图拖动、缩放、倾斜或旋转时,canvas 上的内容不会随地图变换,导致效果有边缘、浮在地面上,且无法与地图对齐。正确做法是将 canvas 作为地图的数据源(如 Mapbox GL JS 的 canvas source),让地图将其像卫星影像一样铺在地图表面上。

如何让洪水模拟尊重真实地形,而不是简单地淹没所有低洼区域?

使用广度优先搜索(BFS)从海岸线开始向内陆扩展,并加入规则:只有当水位高于地面高程时,水才能到达该单元格。水位随距离线性衰减(例如每 12 公里损失全部波高),这样水会沿着河谷上溯,并在堤坝或高地前停止。

如何将视频纹理只叠加在洪水区域,而不覆盖整个地图?

利用 canvas 的合成模式:先用 source-atop 将视频绘制到洪水画布上,这样视频只出现在已有水像素的地方;再用 destination-in 将缓冲区裁剪到陆地掩膜(landMask),避免纹理覆盖开阔海域。最后将处理后的缓冲区以 source-atop 绘制到洪水画布上。

在 Mapbox GL JS 中,如何让建筑物看起来更真实,而不是简单的灰色方块?

使用 fill-extrusion-pattern 属性,将运行时通过 map.addImage 添加的立面纹理(如窗户图案)平铺到建筑墙面上。根据建筑高度选择不同纹理(如玻璃、浅色、砖墙),并添加一个薄薄的屋顶挤出层来赋予屋顶独立纹理。

如何在不重建图层的情况下动画化建筑物的倒塌?

利用 fill-extrusion 图层的高度表达式乘以一个 feature-state 值 k(默认 1)。每帧通过 map.setFeatureState 更新 k 值(从 1 逐渐减小到废墟高度比例),挤出高度随之缩小。需要将建筑复制到自定义 GeoJSON 源并设置 promoteId,因为 Mapbox Streets 的建筑没有稳定 ID。

如何决定哪些建筑物会在地震中倒塌?

根据震级和距离计算修正麦卡利烈度(MMI),然后使用脆弱性曲线将烈度映射为倒塌概率。再乘以建筑质量因子和高度因子(中层建筑最脆弱,高层和低层相对较好)。最后为每栋建筑生成随机数,决定其状态:倒塌、损坏或完好。

🏷️

标签

➡️

继续阅读