ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Cesium三维淹没分析:热力图可视化水深分布实践

Cesium三维淹没分析:热力图可视化水深分布实践 简介本资源是一套基于Cesium实现的三维地理空间淹没分析可视化方案面向GIS开发工程师、Web三维可视化学习者及应急仿真系统开发者解决城市内涝模拟、地形水位动态推演与风险热力表达等实际问题。压缩包共464个文件9.35MB包含104个核心JavaScript逻辑文件含水位计算、地形高程采样、热力图叠加渲染、141张PNG/JPG效果截图与UI图标、93个Source Map调试支持文件、30个JSON格式地形与模拟参数配置以及配套CSS样式与SVG矢量资源结构清晰、模块解耦便于二次开发与功能复用。已有2634人学习下载提供开箱即用的完整前端工程涵盖交互式水位滑块控制、LOD地形加载优化、WebGL着色器增强渲染及热力图密度映射逻辑可直接集成至防汛指挥、数字孪生城市等业务系统。 前阵子有个水利信息化的项目找到我需求很直接在三维地球上模拟水库泄洪后周边区域会被水淹到哪里并且要看不同水位的淹没范围差异。当时团队里有人提议用现成的GIS桌面软件先跑一遍再导数据但甲方明确要求交互式调节水位、浏览器直接打开、还得在三维场景里直观显示受影响程度。于是我想到了Cesium这套方案最终用淹没分析配合热力图的形式把整个需求落地了。这篇文章就围绕这个实现过程来写。核心就三件事怎么把地形高程数据拿到手、怎么根据水位高度算出淹没区域边界、怎么用热力图把“淹多深”“影响多大”这种信息直观呈现出来。适合正在做三维GIS可视化、防汛减灾系统、或者需要在Web端快速验证淹没范围的开发者参考。1. 淹没分析的实现思路与方案选型1.1 为什么不能在Cesium里直接“灌水”最开始我犯过一个认知错误以为Cesium自带洪水模拟或者水文分析模块查了一圈文档发现根本没有现成的API。Cesium本质上是一个三维地球可视化引擎它的强项是渲染各种空间数据而不是做水文计算。真正要算出“某个水位下哪些地方会被淹”需要自己结合地形高程数据和阈值判断来实现。这其实让整个方案变得更灵活了。因为“淹没分析”本身在不同业务场景下定义并不一样洪水预警关心的是水面以下的地形范围城市规划关心的是积水深度分布环境影响评估关心的是不同时段水位上涨的动画过程。如果Cesium强行内置一套算法反而无法适配这么多场景。所以我的技术路径是用Cesium做可视化渲染用地形采样接口拿高程数据用Canvas动态生成热力纹理最后把三者拼装起来形成完整的淹没分析效果。1.2 两种主流实现路径的对比在决定用“热力图纹理水面多边形”这套方案前我反复比对了两种实现思路各有适用场景这里把关键差异列出来。方案实现方式优势劣势适用场景采样点热力散点在淹没范围内生成密集采样点每个点根据水深映射颜色用PointPrimitive批量渲染实现简单、热力效果直观、可统计单个点水深采样点多时性能下降、边缘锯齿明显小范围快速演示多边形水面纹理热力将整个淹没区域构造成一个水面多边形把水深数据绘制成Canvas贴图映射到多边形表面视觉效果好、整体感强、可动态更新水位需要自己处理纹理坐标映射、逻辑稍复杂正式项目交付我最终选择了第二种“水面多边形纹理热力”。原因很实际PointPrimitive方案在数据量少的时候效果不错但一旦把采样间距压缩到20米以内上万甚至十几万个点的渲染在性能一般的笔记本上就会明显掉帧。而纹理贴图方案无论数据量多大最终渲染的只是两个三角形组成的平面性能压力基本恒定。1.3 整体技术架构拆解整个淹没分析模块可以拆成四个独立的功能层每一层只负责一件事后期维护和替换都很方便。第一层是地形数据层。核心接口是Cesium.sampleTerrainMostDetailed传入经纬度坐标数组返回每个点的高程值。这一层决定了淹没判断的精度也直接影响热力图数据的准确性。第二层是淹没计算层。拿到一组带有高程值的采样点后用当前水位高度减去每个点的高程差值大于0说明被淹差值就是该点的水深。这层的关键是水位高度统一换算到海拔高度而不是相对高度。第三层是热力数据生成层。把水深数据归一化到一个0到1的区间映射到Canvas像素上生成一张彩色渐变的热力纹理图。第四层是渲染展示层。把热力纹理作为材质赋予水面多边形然后叠加到地形表面提供水位调节滑块、动画播放等交互能力。这四层各司其职哪一块出问题都能快速定位。实际开发过程中我在“热力数据生成层”遇到最多细节问题后面的章节会重点展开。2. 地形数据准备与淹没范围计算2.1 地形高程采样精度和性能怎么平衡做淹没分析最基础的数据是地形高程。Cesium默认加载的地形服务通常是全球低精度地形高程误差可能达到几十米这种精度做淹没分析基本没有参考价值。我在正式项目中用的是Cesium官方提供的CesiumTerrainProvider搭配STK World Terrain数据集部分区域能达到米级精度对流域尺度的淹没模拟够用了。采样点的生成逻辑是一个规则的经纬度网格。比如我的分析区域是一个矩形范围从西南角开始每隔一定距离生成一个采样点东北角结束。function generateSamplePoints(west, south, east, north, spacing) { const points []; for (let lon west; lon east; lon spacing) { for (let lat south; lat north; lat spacing) { points.push(Cesium.Cartographic.fromDegrees(lon, lat)); } } return points; }这里的spacing就是采样间距直接决定了数据量和计算精度。我做了几组对比测试间距500米采样约400个点计算瞬间完成但边界很粗糙间距100米采样约1万个点效果已经比较精细间距50米采样约4万个点视觉上基本看不出与更精细采样的区别。所以实际项目里我优先推荐100米间距作为起点再根据区域面积动态调整。如果分析区域只有几平方公里可以加密到20米如果是一个县城范围100米到200米就足够了。过密的采样点不仅让采样过程变慢还会让后续热力纹理的生成变卡。2.2 用sampleTerrainMostDetailed获取高程值Cesium提供的地形采样接口是异步的并且只有在地形加载完成后才能拿到准确的高程值。核心调用方式如下async function fetchTerrainHeights(positions) { const terrainProvider await Cesium.createWorldTerrainAsync({ requestVertexNormals: true, requestWaterMask: true }); const updatedPositions await Cesium.sampleTerrainMostDetailed( terrainProvider, positions ); return updatedPositions.map(p p.height); }这里有两点必须注意。第一sampleTerrainMostDetailed是异步方法必须用await等待完成不然下一步计算时拿到的height还是undefined。第二采样返回的height是海拔高度单位是米而Cesium场景中的高度也是海拔制两者可以直接比较不需要额外转换。踩过的一个坑是刚开始我图省事想用Cesium.sampleTerrain这个旧接口结果发现它只支持指定级别的地形数据精度不够还容易返回null值。后来统一换成sampleTerrainMostDetailed才稳定。这个接口会自动选择可用的最高精细度地形省去了自己管理地形级别的麻烦。2.3 淹没判断水位高度才是真正的核心算法淹没判断的数学逻辑非常简单就是一个减法。每个采样点的地形高度记为terrainHeight当前水位高度记为waterLevel淹没深度depth waterLevel - terrainHeight。如果depth 0说明这个点在水面以下属于被淹没区域如果depth 0说明这个点高于水面不受影响。function calculateInundation(samplePoints, waterLevel) { const inundatedPoints []; const depthMap []; for (let i 0; i samplePoints.length; i) { const point samplePoints[i]; const terrainHeight point.height; const depth waterLevel - terrainHeight; depthMap.push(depth); if (depth 0) { inundatedPoints.push({ longitude: point.longitude, latitude: point.latitude, depth: depth }); } } return { inundatedPoints, depthMap }; }但这里有一个陷阱waterLevel到底应该填什么值。它必须是相对于海平面的绝对海拔高度而不是相对某个基准面的上涨高度。比如水库正常蓄水位是85米那模拟溃坝水位涨到88米时waterLevel就应该填88而不是填3。我遇到过甲方直接在界面上说“水位上涨2米”然后拿相对高度去算结果整个区域都没有变化。这个问题的根源在于没有理解“海拔高程”这个概念。后来我在界面上专门加了一个提示明确标注“请输入海拔高度米”并且把当前区域的DEM最高点、最低点展示出来作为参考这个问题才彻底解决。2.4 淹没范围边界构建有了淹没点集合下一步要生成一个包围所有淹没点的多边形这个多边形就是水面渲染的范围。最粗暴的方式是取所有淹没点的外包矩形成多边形但这样做的问题很明显矩形会包含很多地形高于水位的区域这些区域虽然已经不在淹没点集合里却依然被覆盖在水面多边形内视觉上会出现水体悬浮在山坡上的奇怪效果。更精细的做法是用凸包算法生成一个凸多边形只包住真正被淹没的点。Cesium自带的Cesium.BoundingSphere不直接提供凸包计算但一个简单的GrahamScan算法实现并不复杂百来行代码搞定。function convexHull(points) { // 先按经度排序然后使用单调链算法求凸包 const sorted points.slice().sort((a, b) { return a.longitude - b.longitude || a.latitude - b.latitude; }); const hull []; // 下凸包 for (const point of sorted) { while (hull.length 2 crossProduct(hull[hull.length - 2], hull[hull.length - 1], point) 0) { hull.pop(); } hull.push(point); } // 上凸包 const lower hull.length 1; for (let i sorted.length - 2; i 0; i--) { while (hull.length lower crossProduct(hull[hull.length - 2], hull[hull.length - 1], sorted[i]) 0) { hull.pop(); } hull.push(sorted[i]); } hull.pop(); return hull; }不过凸包方案也有局限遇到河道这种细长弯曲的形状凸包会把很多河流拐弯处的空白区域包进来。真正生产级的做法是用Alpha Shape算法提取不规则边界或者结合水域矢量边界做裁剪。但考虑到大家拿到这篇博客主要是做原型验证凸包方案已经能覆盖大部分场景。如果项目要求高精度边界建议把河道中心线数据导入用缓冲区分析生成贴合地形的边界。我的个人建议是第一版先用矩形外包快速跑通流程第二步升级成凸包视觉质量立刻提升如果还不够再考虑Alpha Shape或业务边界裁剪。循序渐进不要一上来就陷入边界算法的细节里。3. 热力图可视化把水深数据变成颜色3.1 颜色映射方案水深怎么对应颜色热力图的核心是数据到颜色的映射。淹没分析里最常见的映射维度就是水深水深越大的地方颜色越突出比如深红色水深接近0的地方用浅蓝色。这符合人们对洪水的直觉认知中心区域最危险边缘区域是过渡带。我用的是分段线性插值定义一组颜色断点然后在水深范围内线性插值。断点可以按照业务需求灵活调整。const colorStops [ { value: 0.0, color: [0, 128, 255] }, // 浅蓝 { value: 0.3, color: [0, 200, 200] }, // 青色 { value: 0.6, color: [255, 200, 0] }, // 黄色 { value: 0.8, color: [255, 120, 0] }, // 橙色 { value: 1.0, color: [255, 0, 0] } // 深红 ]; function getColorByDepth(depth, maxDepth) { const normalized depth / maxDepth; for (let i 0; i colorStops.length - 1; i) { if (normalized colorStops[i].value normalized colorStops[i 1].value) { const localRatio (normalized - colorStops[i].value) / (colorStops[i 1].value - colorStops[i].value); const start colorStops[i].color; const end colorStops[i 1].color; return [ start[0] (end[0] - start[0]) * localRatio, start[1] (end[1] - start[1]) * localRatio, start[2] (end[2] - start[2]) * localRatio ]; } } return colorStops[colorStops.length - 1].color; }这里有个细节值得强调归一化的分母maxDepth不能简单取当前水位下的最大水深。因为随着水位上升最大水深是变化的如果每次都拿当前最大值做归一化颜色分布会被动态拉伸导致低水位时颜色区分度不足高水位时又过于饱和。我建议用一个固定的参考深度比如分析区域内历史最高水位对应的最大水深这样不同水位下的热力图才有可比性。3.2 用Canvas生成热力纹理确定了每个采样点的颜色接下来就是把这些颜色组织成一张可以在Cesium里使用的纹理图片。这里我用Canvas离屏绘制核心思路是创建一个Canvas画布把每个采样点按照经纬度映射到画布像素坐标以该点为中心画一个辐射渐变的圆形半径由该点的水深决定颜色由水深对应的RGB决定。function generateHeatmapTexture(samplePoints, bounds, canvasSize) { const canvas document.createElement(canvas); canvas.width canvasSize; canvas.height canvasSize; const ctx canvas.getContext(2d); const maxDepth getMaxDepth(samplePoints); samplePoints.forEach(point { const x (point.longitude - bounds.west) / (bounds.east - bounds.west) * canvasSize; const y (1 - (point.latitude - bounds.south) / (bounds.north - bounds.south)) * canvasSize; const [r, g, b] getColorByDepth(point.depth, maxDepth); const radius Math.max(2, point.depth / maxDepth * 15); const gradient ctx.createRadialGradient(x, y, 0, x, y, radius); gradient.addColorStop(0, rgba(${r}, ${g}, ${b}, 0.9)); gradient.addColorStop(1, rgba(${r}, ${g}, ${b}, 0)); ctx.fillStyle gradient; ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fill(); }); return canvas; }为什么这里不是简单地把每个点填一个颜色方块而是用辐射渐变原因是热力图要的是“连续过渡”的视觉感受渐变圆让相邻采样点之间的颜色自然融合模拟出热量扩散的效果。如果直接填色块出来的是一张马赛克图完全没有热力的感觉。Canvas尺寸的选择也很有讲究。我用512x512做原型完全够了但如果分析范围很大而且要把热力图放大看细节建议用1024甚至2048。尺寸越大纹理越清晰但生成耗时和内存占用也线性增长。根据我的测试1024x1024在普通电脑上生成一张热力图纹理耗时约50毫秒完全在可接受范围内。3.3 把热力纹理贴到水面多边形上热力纹理生成后就轮到Cesium上场了。我把整个淹没区域构造成一个PolygonGeometry然后通过自定义Material把Canvas纹理贴到多边形表面。这样渲染出来的效果就是水面上覆盖着热力图水深的地方颜色偏红水浅的地方偏蓝透明区域表示没有淹没。function createWaterEntity(positions, heatmapCanvas) { const texture new Cesium.Texture({ context: viewer.scene.context, source: heatmapCanvas }); return viewer.entities.add({ polygon: { hierarchy: new Cesium.PolygonHierarchy(positions), material: new Cesium.Material({ fabric: { type: Image, uniforms: { image: heatmapCanvas, transparent: true } } }), classificationType: Cesium.ClassificationType.TERRAIN, height: waterLevel, perPositionHeight: false } }); }这里有一个非常重要的参数classificationType。我之前写的版本忘了设置这个属性导致水面多边形虽然创建出来了但完全被地形遮挡或者边缘没贴住地形像一张悬浮在半空中的纸片。设置成Cesium.ClassificationType.TERRAIN后多边形会与地形完美贴合水体就像是填在地形凹陷处一样自然。还有个细节是height的设置。为了让水面贴合地形又不被地形完全覆盖我在实际项目中把水面高度设成了waterLevel 0.5也就是比目标水位高半米。这样从侧面看水体有明显的厚度感不会出现地形穿透水面的瑕疵。这半米的偏移不会影响淹没判断的准确性因为判断逻辑用的是纯采样点计算和水面渲染高度无关。3.4 动态水位变化时的热力图实时刷新业务场景通常不只是看一个固定水位而是要拖动滑块看不同水位下的淹没范围变化。这就意味着每次水位变化都要重新采样、重新计算、重新生成纹理、更新实体。为了保证交互流畅我用了“防抖异步重建”的策略。用户拖动滑块时不在每次拖动事件里都触发完整的重算流程而是等用户停下来300毫秒后再执行。let timer null; waterLevelSlider.addEventListener(input, function() { clearTimeout(timer); timer setTimeout(() { const waterLevel parseFloat(this.value); updateInundation(waterLevel); }, 300); });为什么要防抖因为“采样地形高度”这一步是异步网络请求即便这些采样点坐标不变、高度不应该变但每次都会重新请求非常耗时。实际上我这里做了一次优化第一次采样完成后就把所有采样点的高程值缓存起来后续水位变化时只做减法和纹理生成不再请求地形服务。这样从滑块拖动到画面更新延迟从几百毫秒降到了几十毫秒。let cachedHeights null; async function fetchTerrainHeights(positions) { if (cachedHeights) return cachedHeights; const terrainProvider await Cesium.createWorldTerrainAsync(); const updatedPositions await Cesium.sampleTerrainMostDetailed( terrainProvider, positions ); cachedHeights updatedPositions.map(p p.height); return cachedHeights; }这个缓存策略帮我解决了一个大问题水位动画播放的时候连续上百帧的更新都只消耗本地计算资源不会因重复请求地形服务导致卡顿。4. 交互设计与整个模块集成4.1 设计一个实用主义的水位调节面板交互面板的设计思路是少而精不堆功能。经过与甲方的多轮沟通最终确认了三个核心控件一个水位滑块、一个动画播放按钮、一个海拔高度参考信息展示区。滑块的最小值是分析区域DEM的最低点高程最大值是最高点高程加上一个富余量。滑块底部显示当前水位的绝对海拔高度以及该水位下的最大淹没水深、淹没面积估算值。这些信息虽然不是可视化必须的但甲方很喜欢因为他们写报告的时候需要这些数字。这段参考信息的计算逻辑其实很轻量最大淹没水深就是所有采样点水深的最大值淹没面积就是被淹没采样点的数量乘以每个采样点代表的实际面积。function updateStatistics(waterLevel, samplePoints) { let maxDepth 0; let submergedCount 0; samplePoints.forEach(point { const depth waterLevel - point.height; if (depth 0) { maxDepth Math.max(maxDepth, depth); submergedCount; } }); const spacing sampleSpacing; // 米 const areaPerPoint spacing * spacing; const submergedArea submergedCount * areaPerPoint; document.getElementById(maxDepth).textContent maxDepth.toFixed(1) 米; document.getElementById(submergedArea).textContent (submergedArea / 1000000).toFixed(2) 平方公里; }4.2 水位上涨动画流畅是关键动画播放是一个锦上添花的功能。甲方说只看静态图感觉不够直观希望看到“水慢慢涨上来”的动态过程。这个功能实现比较简单从较低水位开始每帧水位增加一个固定步长逐步重算淹没范围和热力纹理。let animationId null; let currentWaterLevel startLevel; function animateWaterLevel(targetLevel) { cancelAnimationFrame(animationId); function step() { currentWaterLevel 0.3; // 每帧上升0.3米约60fps下每秒上升18米 if (currentWaterLevel targetLevel) { currentWaterLevel targetLevel; } updateInundation(currentWaterLevel); updateWaterLevelSlider(currentWaterLevel); if (currentWaterLevel targetLevel) { animationId requestAnimationFrame(step); } } animationId requestAnimationFrame(step); }水位上涨速度的调整要因地制宜。我做的第一版每秒上升30米看起来像海啸完全不符合真实的洪水上涨速率。后来改成每秒5米并对大范围分析区域做了时间缩放才更接近演示预期。这里建议根据实际业务需要设置合理的速度倍率而不是追求视觉效果。4.3 热力图图例没有图例的热力图等于没做热力图图例是很多人容易忽略的部分但它恰恰是可用性的关键。没有图例的红色区域用户无法判断是淹了5米还是15米信息传递大打折扣。我在页面的右上角添加了一个垂直渐变条图例用CSS实现和热力图的颜色断点一一对应并标注了对应的水深范围。div classlegend div classlegend-title淹没深度米/div div classlegend-gradient/div div classlegend-labels span深/span span浅/span /div /div.legend-gradient { width: 20px; height: 150px; background: linear-gradient( to bottom, rgb(255, 0, 0), rgb(255, 120, 0), rgb(255, 200, 0), rgb(0, 200, 200), rgb(0, 128, 255) ); }图例的颜色顺序需要和热力图纹理生成时的颜色映射保持一致。这里特别容易出问题热力图是深红代表深水图例如果画反了整个分析结果就产生了误导。我每次更新颜色映射方案时都会同步检查图例的CSS渐变和Canvas纹理的颜色生成逻辑保证两者完全一致。5. 常见问题排查与实战经验总结5.1 水面多边形悬空或嵌入地形怎么办这是我在开发过程中遇到最多的问题几乎每换一个测试区域都会出现。水面悬空看起来像一块透明的玻璃板浮在空中水面嵌入地形则完全看不到水的效果。排查思路分三步。第一步检查classificationType是否设置成Cesium.ClassificationType.TERRAIN。第二步检查多边形高度是否设置正确如果是perPositionHeight: false模式height属性统一作用于整个多边形务必使用海拔高度。第三步检查地形服务是否正常加载如果地形服务没起来多边形只能贴在地球椭球面上看起来就是嵌入地面以下很深。还有一个不太起眼但影响很大的坑多边形顶点顺序。Cesium要求多边形的外环顶点按逆时针顺序排列。如果顺序反了多边形会被认定为内环渲染出来的效果是一片被掏空的区域。我写的凸包算法默认输出顺序就是逆时针但如果你从其他数据源导入多边形边界一定要检查顶点顺序。5.2 热力图颜色断层严重过渡不自然如果热力图纹理上出现明显的块状色斑而不是平滑的渐变通常有三个原因Canvas画布尺寸太小导致像素被放大后出现马赛克采样点间距太大相邻点的颜色没有交叠区域渐变圆半径太小无法覆盖到相邻点之间的空白区域。我的调参经验是渐变圆的半径设为采样间距的0.6到0.8倍这样相邻点的渐变圆会有充足的重叠区域颜色过渡自然顺滑。如果半径太小每个点都是一个孤立的小圆点半径太大远处细节被模糊掉相当于做了一次不必要的低通滤波。实测下来采样间距100米、Canvas画布1024像素、渐变圆半径8到12像素这组参数在大多数场景下都能得到满意的热力效果。你可以把它当起点再根据实际区域微调。5.3 性能问题数据量一大就卡顿当分析区域扩展到整个县城级别时采样点数量会突破5万甚至10万个这时有两个性能瓶颈会先后出现。第一个瓶颈在Canvas纹理生成阶段每次重算都需要遍历所有采样点并绘制圆形渐变这个操作是纯CPU计算非常耗时。我实测10万个点在Canvas上绘制需要约1.2秒远远达不到交互流畅的要求。解决办法是双管齐下。一方面降低Canvas分辨率到512绘制时间可以降到300毫秒左右另一方面对采样点做抽稀在生成纹理时不使用全部采样点而是先过滤掉水深小于0.1米的点因为这些边缘区域对热力图视觉贡献很小却能节省大量绘制时间。第二个瓶颈在Cesium实体更新阶段。每次水位变化都销毁旧实体、创建新实体会触发场景树更新数据量大的时候会明显卡顿。优化方式是复用实体只更新它的材质纹理和位置高度。// 复用实体只更新属性 waterEntity.polygon.material new Cesium.Material({ fabric: { type: Image, uniforms: { image: newCanvas } } });这个方式避免了实体的销毁重建界面更新速度快了很多。我在项目中实测使用复用实体方案后同样10万个采样点水位滑块的响应时间从2秒以上降到了200毫秒以内体验提升了不止一个档次。5.4 水位调节时热力图和淹没范围不同步这个问题的表现是淹没边界已经变化了但热力图的颜色分布还是旧水位的。原因在于更新顺序出错了。我在第一版代码里先更新了水面多边形的位置和纹理然后再重新采样计算淹没点导致一个时间窗口内界面显示的是新数据而纹理还是上次计算的结果。正确的更新顺序应该是先根据水位重新计算所有采样点的水深生成新的热力纹理最后一次性更新水面实体。避免在“计算-生成纹理-更新实体”这个链条中插入其他异步操作。如果水位动画播放的时候还要同时更新其他UI元素可以用一个标志位锁住画面更新等所有数据准备完成后再统一渲染。5.5 坐标系的坑经纬度和高程别搞混最后提一个隐蔽的坑sampleTerrainMostDetailed返回的高度是椭球高也就是WGS84坐标系下的高度。如果你手上的水位数据来自水文站的观测数据它通常是基于国家高程基准的Normal Height正常高两者之间存在一个高程异常值在不同地区可能相差几十米。怎么处理两种方式。第一种是粗略处理认为分析区域较小高程异常值近似恒定直接用一个常数修正把观测水位加上高程异常值得到椭球高。第二种是精确处理用EGM2008或当地似大地水准面模型为每个采样点单独计算高程异常值。具体在代码里这个修正在采样完成后做// 假设研究区域高程异常值为 -28.5 米 const geoidOffset -28.5; async function fetchTerrainHeights(positions) { const updatedPositions await Cesium.sampleTerrainMostDetailed( terrainProvider, positions ); return updatedPositions.map(p p.height geoidOffset); }这个细节如果处理不好整个分析结果可能偏差十几米直接导致淹没范围判断失误。我在正式项目里都会向甲方索要当地的似大地水准面精化模型数据如果没有至少也要确认一个区域平均的高程异常值作为修正。整个淹没分析带热力图的功能从上手到稳定运行我陆续花了差不多两周时间。地形采样和淹没计算本身并不难真正费工夫的是热力图纹理的生成优化和交互体验的打磨。如果你正准备做类似功能我建议先按最小闭环跑通选一个小范围区域、生成采样点、采样地形、计算淹没点、创建水面实体确认这五步没问题后再往上加热力图和动画。这样每一步出问题都能快速定位不至于一次引入太多变量导致排查困难。本文还有配套的精品资源点击获取
返回列表