ARTICLE DETAIL

资讯详情

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

Cesium结合Heatmap.js实现动态洪水模拟:无需GLSL的轻量方案

Cesium结合Heatmap.js实现动态洪水模拟:无需GLSL的轻量方案 做可视化这么些年我最大的感受是业务方很少要求你“写一个震惊四座的3D特效”更多时候只是希望“把这个灾害过程给我讲清楚”。最近接了一个洪水淹没模拟的演示项目地形、河流、历史水位数据都齐了可项目组聊到技术方案时一听到要写GLSL着色器几个人就沉默了。GLSL本身并不算难但业务方只给了三天时间还要每天跟着水文模型结果调参数根本没有余力去折腾渲染管线。我最后选择绕开着色器用Cesium 1.120搭配Heatmap.js 2.0.5做了一套动态洪水模拟方案效果能应付大屏演示核心代码也很少。这篇文章就把完整实现过程和源码整理出来给同样没有图形学背景、又被洪水模拟需求追着跑的朋友一个参考。1. 项目整体设计与方案选型1.1 需求拆解洪水模拟到底在模拟什么很多项目刚开始提需求时就一句话“做个洪水淹没的动态效果要有水漫过去的感觉。”但如果直接按这句话去开发后面一定会返工。我习惯先把需求拆开通常一个可交付的洪水模拟系统至少包含下面几件事空间基准洪水发生在哪里需要叠加到真实经纬度坐标上不能只是屏幕上一个动画。时间演进洪水不是静止的要有“水位持续上涨、范围逐步扩大”的动态过程。强度表达不同区域水深、流速、受灾程度不一样在视觉上要能一眼看出来。可交互至少能暂停、回放或者点击某个位置看到水深估算值。可迭代水文模型结果一变前端能马上更新而不是重新出图。这套需求如果用纯Cesium加上自定义着色器来做效果上限当然很高水面反射、波纹、动态法线都能做得非常漂亮。但对一个非图形学背景的团队来说开发成本很高光是“把洪水淹没范围裁剪到地形上”这一步就需要处理深度测试、高度图采样、UV映射等一堆问题。所以我决定换一条路用Heatmap.js 2.0.5生成热力图再以动态纹理的方式贴到Cesium 1.120的矩形实体上用颜色表示水深用纹理扩散表示洪水演进。整条链路里完全不用自己写GLSLCesium内部已经帮我们把WebGL那层处理掉了。1.2 为什么选Heatmap.js而不是硬啃着色器Heatmap.js本质是一个轻量级的Canvas热力渲染库版本2.0.5在体积和稳定性之间算是比较平衡的版本。它的核心思路是把离散的数据点比如经纬度坐标上的水深值通过径向渐变、透明度叠加形成一张连续的热力分布图。而洪水水深恰恰就是一个空间连续的场每个位置都有一个值只是来源是水文模型或者传感器插值不是物理仿真计算。Heatmap.js方案相比GLSL有几条很实在的优势开发速度快只需要操作JavaScript对象和Canvas不需要学习OpenGL ES的渲染管线。数据结构简单输入就是一堆{x, y, value}从业务接口拿到数据后直接喂进去。调试直观热力图长什么样打开浏览器就能截图看不需要对着着色器报错日志猜问题。更新成本低数据一变调用setData重新绘制Canvas再把新的Canvas交给Cesium几行代码就完成一次动态更新。从项目风险角度看业务方的核心诉求是“看清楚淹没范围和水深变化”热力图完全可以满足。物理精度上的不足通过配色和动态过程去弥补反而更适合做应急指挥大屏这种场景。1.3 与专业洪水模拟的区别以及如何向业务方解释这里要泼一盆冷水Heatmap.js方案不是水力模型它只是把已有数据“画”出来。如果项目要求精确计算洪水流速、考虑地形糙率、模拟溃坝过程那还是要用专业的水文水动力软件比如各类数值模型生成结果再用前端做可视化。用热力图做出来的模拟本质上是一个“视觉化推演”适合用在态势展示、灾情感知、培训演练这些场景。跟业务方沟通时我一般会明确说清楚“我们做的是把上游计算好的淹没数据动态渲染出来不是自己算洪水怎么流。”这样可以避免很多不切实际的期望。在这个项目里我们每天会收到一批新的淹没范围和水深数据Heatmap.js方案正好就是“数据来了→画出来→展示到大屏上”的轻量流程。2. 环境准备与基础实现2.1 依赖引入与Cesium 1.120初始化先说版本组合Cesium 1.120、Heatmap.js 2.0.5。Cesium 1.120是一个比较稳定的版本对ImageMaterialProperty、RectangleGeometry这些旧有API都保留得很好同时又修复了不少WebGL上下文问题。Heatmap.js 2.0.5则是在npm和CDN上都能直接拿到的常见版本。建议直接通过CDN引入省去打包配置的麻烦link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/cesium1.120.0/Build/Cesium/Widgets/widgets.css script srchttps://cdn.jsdelivr.net/npm/cesium1.120.0/Build/Cesium/Cesium.js/script script srchttps://cdn.jsdelivr.net/npm/heatmap.js2.0.5/build/heatmap.min.js/script初始化Viewer时有几个参数值得注意。Cesium默认的UI控件在这个场景下基本都用不到建议直接关掉避免大屏上冒出乱七八糟的按钮。同时要记得设置一个合适的baseLayer否则在某些环境下默认影像会请求失败场景里只剩一个光秃秃的地球。const viewer new Cesium.Viewer(cesiumContainer, { animation: false, baseLayerPicker: false, fullscreenButton: false, geocoder: false, homeButton: false, infoBox: false, sceneModePicker: false, selectionIndicator: false, timeline: false, navigationHelpButton: false, // 生产环境建议换为你自己的影像服务地址 baseLayer: new Cesium.ImageryLayer( new Cesium.UrlTemplateImageryProvider({ url: https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png, subdomains: [a, b, c] }) ) }); viewer.scene.globe.enableLighting true; viewer.scene.globe.depthTestAgainstGround false; viewer.camera.setView({ destination: Cesium.Rectangle.fromDegrees(118.0, 31.0, 120.0, 33.0) });这里有个很多人踩过的坑baseLayer参数在Cesium 1.120里已经推荐使用ImageryLayer对象而不是直接塞一个UrlTemplateImageryProvider。如果你去看Cesium中文文档会发现老版本创建方式是imageryProvider: xxx新版本更建议用baseLayer搭配new Cesium.ImageryLayer(...)。版本不同API形式会有细微变化这也是为什么我建议大家学会看官方案例和迁移说明。2.2 用矩形圈定洪水评估范围在Cesium里表示一块固定区域最方便的不是画复杂的多边形而是直接用矩形。这里我用到的正是“Cesium绘制矩形”这个基础操作。之所以用矩形是因为洪水评估区域通常是经纬度范围的一个包围盒数据源直接给出东、南、西、北边界矩形天然适配。const west 118.2; const south 31.2; const east 119.8; const north 32.8; const floodRectangle viewer.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees(west, south, east, north), material: new Cesium.ColorMaterialProperty(Cesium.Color.TRANSPARENT), classificationType: Cesium.ClassificationType.BOTH } });在后续动态更新中这个矩形实体相当于一块“画布”我们只要不断替换它的material就能实现洪水纹理的动态变化。注意一点矩形实体默认是可以被地形遮挡的但为了演示方便上面代码里没有开严格的深度检测这样从高空俯视时热力图始终清晰可见。2.3 Heatmap.js的基础用法Heatmap.js的使用模式非常固定创建实例、配置数据、重绘三步走const heatmap h337.create({ container: document.getElementById(heatCanvasWrapper), radius: 30, maxOpacity: 0.8, minOpacity: 0.1, blur: 0.85 });数据格式是const data { max: 5, min: 0, data: [ { x: 100, y: 200, value: 3.2 }, { x: 150, y: 180, value: 4.5 } ] }; heatmap.setData(data);这里有个关键点Heatmap.js 2.0.5会把每个点的value按max做归一化然后映射到颜色梯度。所以如果你把max设成10但业务数据最大只有3热力图就始终是淡蓝色看起来像没信号。我习惯把max设置成业务水深的最大值并留出10%余量这样颜色区分度更高。GLSL方案里颜色映射是在GPU上通过片段着色器做插值写个自定义函数就行Heatmap.js方案里颜色映射通过gradient对象配置比如let gradient { 0.0: rgba(0, 0, 255, 0), 0.2: rgba(0, 100, 255, 0.6), 0.4: rgba(0, 200, 255, 0.7), 0.6: rgba(255, 255, 0, 0.8), 0.8: rgba(255, 100, 0, 0.85), 1.0: rgba(255, 0, 0, 0.9) }; heatmap.configure({ gradient: gradient });这个渐变就是从“浅蓝→深蓝→黄→橙→红”的过程视觉上和实际水深从浅到深完全对应业务方一看就懂。3. 动态洪水模拟的核心实现3.1 数据模型经纬度如何落到热力图Canvas上这一步是整个方案最容易出错的环节。Heatmap.js自己只认{x, y}像素坐标不认经纬度必须自己做一次线性映射。热力图Canvas的坐标系统是左上角为原点x向右增大y向下增大。因此需要把经纬度范围转换到Canvas像素范围。假设矩形评估区域的边界是west、south、east、northCanvas尺寸是width、height那么任意一个经纬度点(lon, lat)转换公式x (lon - west) / (east - west) * width; y (north - lat) / (north - south) * height;注意第二个公式里是north - lat因为纬度越高越靠北投影到Canvas上应该越靠上而Canvas的y轴向下为正所以要用北边界减纬度。这个细节反了整个热力图会上下颠倒看起来就像洪水跑到天上去了。反过来如果要实现“鼠标点击查看水深”也要写一个逆向函数lon west (mouseX / width) * (east - west); lat north - (mouseY / height) * (north - south);然后根据坐标位置从当前数据插值出水深。这个交互放在大屏演示里很加分操作者一点某个位置就能看到对应水深。3.2 动态洪水数据生成与Heatmap更新实际项目中动态数据来自后端接口每一帧或每隔几秒推送一批新的淹没点。为了演示我写了一个模拟函数洪水中心点沿着一个方向漂移离中心越近水深越大随时间的推进逐渐向外扩散。function generateFloodData(time) { const points []; const centerLon 119.0 0.3 * Math.sin(time * 0.02); const centerLat 32.0 0.2 * Math.cos(time * 0.015); const radiusKm 10 time * 0.13; for (let i 0; i 400; i) { const angle Math.random() * Math.PI * 2; const dist Math.random() * radiusKm; const lon centerLon (dist / 111.0) * Math.cos(angle); const lat centerLat (dist / (111.0 * Math.cos(Cesium.Math.toRadians(centerLat)))) * Math.sin(angle); const value Math.max(0, 5 * (1 - dist / (radiusKm 10))); const x (lon - west) / (east - west) * canvasWidth; const y (north - lat) / (north - south) * canvasHeight; points.push({ x: x, y: y, value: value }); } return { max: 5, data: points }; }这里使用了简单的经纬度距离换算纬度方向1度大约111公里经度方向要乘以纬度的余弦值。虽然是粗略估算但对示例已经够了。真实项目如果用等经纬度切片也是同样的线性关系。每一次模拟数据生成后调Heatmap.js重绘const data generateFloodData(currentTimeInSeconds); heatmap.setData(data);setData内部会清空上一批点再绘制新一批点并同步更新Heatmap.js生成的那个Canvas。到这里动态画面已经出来了只是还没和Cesium绑定。3.3 将热力图Canvas作为Cesium矩形材质的关键手法在Cesium中让一个矩形显示图片纹理常见做法是给entity.rectangle.material设置ImageMaterialProperty并把image指向Heatmap.js生成的Canvas。这个方案可以绕开自己写GLSLCesium内部会把这个Canvas上传成GPU纹理然后由内置的材质系统渲染到矩形上。每个Heatmap.js实例都有一个getCanvas()方法拿到真实的Canvas元素const heatCanvas heatmap.getCanvas();首次绑定时floodRectangle.rectangle.material new Cesium.ImageMaterialProperty({ image: heatCanvas, transparent: true });如果只是静态展示到这里已经能看到结果了。但动态洪水模拟要求材质不断变化。我最开始的做法是每次都新建一个ImageMaterialProperty重新赋给实体floodRectangle.rectangle.material new Cesium.ImageMaterialProperty({ image: heatmap.getCanvas(), transparent: true });这个写法在1.120版本实测可行原理是每次赋值都触发了Cesium的材质更新机制。不过频繁替换会带来一定的对象创建开销对示例规模来说可接受。如果数据量大、帧率要求高可以考虑复用同一个ImageMaterialProperty但需要确保内部纹理在Canvas重绘后能被Cesium重新上传。这块在不同版本里有兼容性问题稳妥起见我还是推荐直接替换material代码简单理解成本低。另外transparent: true必须设置。如果不设置Cesium会认为这个材质完全不透明热力图里本来应该透明的低水位区域会变成黑块或白块覆盖住底图观感极其糟糕。3.4 用Cesium时钟驱动动画并同步UICesium自带的时钟系统非常适合这种动态模拟。我不建议自己开一个setInterval去刷新因为Cesium场景有自己的渲染循环用时钟驱动能保证视觉与时序同步。初始化时设置时钟范围const start Cesium.JulianDate.fromDate(new Date(2024-06-01T00:00:00)); const stop Cesium.JulianDate.fromDate(new Date(2024-06-01T06:00:00)); viewer.clock.startTime start; viewer.clock.stopTime stop; viewer.clock.currentTime start; viewer.clock.multiplier 60; viewer.clock.shouldAnimate true;然后在viewer.clock.onTick事件里更新热力图viewer.clock.onTick.addEventListener(function (clock) { const elapsedSeconds Cesium.JulianDate.secondsDifference(clock.currentTime, start); const floodData generateFloodData(elapsedSeconds); heatmap.setData(floodData); floodRectangle.rectangle.material new Cesium.ImageMaterialProperty({ image: heatmap.getCanvas(), transparent: true }); if (viewer.scene.requestRenderMode) { viewer.scene.requestRender(); } });如果开启了requestRenderMode高性能模式每次数据更新后都要手动调用viewer.scene.requestRender()否则画面不会刷新。如果关闭了该模式Cesium会持续渲染但CPU占用会高一些。大屏项目我一般会开启requestRenderMode只在数据变化或相机移动时重绘能明显降低机器发热和风扇噪音。如果你想加一个进度条显示模拟时间可以直接在页面里加一个input[typerange]监听onTick同步值再允许用户拖动滑块来跳转模拟时刻这样演示时就能随意回放方便给领导讲“水位是几点涨起来的”。4. 完整源码参考下面给出一份可以直接复制运行的完整HTML。代码基于Cesium 1.120与Heatmap.js 2.0.5内置了模拟数据开箱即用。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleCesium 1.120 Heatmap.js 2.0.5 动态洪水模拟/title link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/cesium1.120.0/Build/Cesium/Widgets/widgets.css style html, body, #cesiumContainer { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Microsoft YaHei, sans-serif; } #info { position: fixed; top: 12px; left: 12px; z-index: 999; background: rgba(0, 0, 0, 0.7); color: #fff; padding: 10px 16px; border-radius: 6px; font-size: 14px; pointer-events: none; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } #controls { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 999; background: rgba(0, 0, 0, 0.75); color: #fff; padding: 12px 18px; border-radius: 8px; display: flex; align-items: center; gap: 10px; } #controls button { border: none; background: #1a6fb5; color: #fff; padding: 6px 14px; border-radius: 4px; cursor: pointer; } #controls input[typerange] { width: 260px; } /style /head body div idcesiumContainer/div div idinfo当前模拟时刻--:--/div div idcontrols button idbtnPlay暂停/button input typerange idtimeRange min0 max360 value0 step1 span idtimeLabel0 min/span /div script srchttps://cdn.jsdelivr.net/npm/cesium1.120.0/Build/Cesium/Cesium.js/script script srchttps://cdn.jsdelivr.net/npm/heatmap.js2.0.5/build/heatmap.min.js/script script // 你可以在这里改成自己的Cesium Ion Token或者注释掉下面的默认逻辑 // Cesium.Ion.defaultAccessToken 换成你的token; const viewer new Cesium.Viewer(cesiumContainer, { animation: false, baseLayerPicker: false, fullscreenButton: false, geocoder: false, homeButton: false, infoBox: false, sceneModePicker: false, selectionIndicator: false, timeline: false, navigationHelpButton: false, baseLayer: new Cesium.ImageryLayer( new Cesium.UrlTemplateImageryProvider({ url: https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png, subdomains: [a, b, c] }) ) }); viewer.scene.globe.enableLighting true; // 开启手动刷新模式减少无谓的渲染开销 viewer.scene.requestRenderMode true; // 洪水评估范围 const west 118.2; const south 31.2; const east 119.8; const north 32.8; viewer.camera.setView({ destination: Cesium.Rectangle.fromDegrees(west - 0.2, south - 0.2, east 0.2, north 0.2) }); // 创建承载热力图纹理的矩形 const floodRectangle viewer.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees(west, south, east, north), material: new Cesium.ColorMaterialProperty(Cesium.Color.TRANSPARENT), classificationType: Cesium.ClassificationType.BOTH } }); // Heatmap.js配置 const heatCanvasWrapper document.createElement(div); heatCanvasWrapper.style.width 800px; heatCanvasWrapper.style.height 600px; heatCanvasWrapper.style.position absolute; heatCanvasWrapper.style.left -9999px; heatCanvasWrapper.style.top 0; document.body.appendChild(heatCanvasWrapper); const heatmap h337.create({ container: heatCanvasWrapper, radius: 30, maxOpacity: 0.8, minOpacity: 0.05, blur: 0.85 }); heatmap.setData({ max: 5, data: [] }); const canvasWidth 800; const canvasHeight 600; function lonLatToPixel(lon, lat) { const x (lon - west) / (east - west) * canvasWidth; const y (north - lat) / (north - south) * canvasHeight; return { x, y }; } function generateFloodData(elapsedSeconds) { const points []; const centerLon 119.0 0.3 * Math.sin(elapsedSeconds * 0.015); const centerLat 32.0 0.2 * Math.cos(elapsedSeconds * 0.012); const radiusKm 8 elapsedSeconds * 0.12; const maxVal 5; for (let i 0; i 500; i) { const angle Math.random() * Math.PI * 2; const dist Math.random() * radiusKm; const lon centerLon (dist / 111.0) * Math.cos(angle); const lat centerLat (dist / (111.0 * Math.cos(Cesium.Math.toRadians(centerLat)))) * Math.sin(angle); const value Math.max(0, maxVal * (1 - dist / (radiusKm 8))); const pixel lonLatToPixel(lon, lat); points.push({ x: pixel.x, y: pixel.y, value: value }); } return { max: maxVal, data: points }; } // 设置Cesium时钟 const start Cesium.JulianDate.fromDate(new Date(2024-06-01T00:00:00)); const stop Cesium.JulianDate.fromDate(new Date(2024-06-01T06:00:00)); viewer.clock.startTime start; viewer.clock.stopTime stop; viewer.clock.currentTime start; viewer.clock.multiplier 120; viewer.clock.shouldAnimate true; const btnPlay document.getElementById(btnPlay); const timeRange document.getElementById(timeRange); const timeLabel document.getElementById(timeLabel); const info document.getElementById(info); btnPlay.addEventListener(click, () { viewer.clock.shouldAnimate !viewer.clock.shouldAnimate; btnPlay.textContent viewer.clock.shouldAnimate ? 暂停 : 开始; }); timeRange.addEventListener(input, (e) { const minutes Number(e.target.value); const julian Cesium.JulianDate.addMinutes(start, minutes, new Cesium.JulianDate()); viewer.clock.currentTime julian; viewer.clock.shouldAnimate false; btnPlay.textContent 开始; }); // 核心动态更新逻辑 viewer.clock.onTick.addEventListener((clock) { const elapsedSeconds Cesium.JulianDate.secondsDifference(clock.currentTime, start); const elapsedMinutes Math.floor(elapsedSeconds / 60); const floodData generateFloodData(elapsedSeconds); heatmap.setData(floodData); floodRectangle.rectangle.material new Cesium.ImageMaterialProperty({ image: heatmap.getCanvas(), transparent: true }); const hours Math.floor(elapsedMinutes / 60); const mins elapsedMinutes % 60; const timeStr String(hours).padStart(2, 0) : String(mins).padStart(2, 0); info.textContent 当前模拟时刻 timeStr; timeRange.value Math.min(elapsedMinutes, 360); timeLabel.textContent elapsedMinutes min; viewer.scene.requestRender(); }); // 保持canvas尺寸与热力图一致 heatmap.getCanvas().width canvasWidth; heatmap.getCanvas().height canvasHeight; /script /body /html这段源码的核心逻辑就三层数据生成、Heatmap重绘、Cesium材质替换。如果你接入了真实水文数据只要把generateFloodData换成接口返回的经纬度水深数组即可。5. 常见问题与排查技巧实录5.1 热力图不显示或只显示一个黑块这个问题我遇到过不只一次原因多半出在透明度配置上。第一ImageMaterialProperty没有设置transparent: trueCesium会按不透明材质去渲染黑色背景就露出来了。第二Heatmap.js的gradient中起始颜色透明度必须是0比如rgba(0, 0, 255, 0)否则整个矩形会被一层半透明底色覆盖。第三检查Canvas尺寸如果宽高是0画出来自然什么都没有。还有一个隐蔽问题Heatmap.js的容器如果尺寸不是整数或使用了display: noneCanvas可能渲染异常。我为了避免影响页面布局把heatmap容器放到了屏幕外但必须给它明确的宽高不能用visibility: hidden配合零尺寸。5.2 热力图与底图位置对不上坐标映射出错的比例很高。主要检查两处y轴方向确认是(north - lat)而不是(lat - north)。矩形宽高比如果Canvas宽高比和经纬度矩形范围的比例差别太大贴图会被拉伸视觉上热力图范围能和矩形框吻合但内部点位相对位置会扭曲。比如矩形经纬度宽度是2度纬度高度是1度Canvas宽高比却是4比3那么经向和纬向的缩放比例就不一致。解决办法是让Canvas宽高比尽量接近矩形经纬度的实际比例或者在生成像素坐标时把经度方向的缩放乘以cos(lat)修正但那个方案会复杂一些演示项目通常不需要。5.3 动态更新时画面闪烁或卡顿闪烁通常是因为频繁给实体赋新的material而Cesium内部的材质更新会触发一次渲染的重新提交。如果机器性能一般建议降低热力图Canvas尺寸比如从800x600降到600x450数据点数量从500降到300。Heatmap.js的绘制性能与点数和Canvas像素面积强相关肉眼看起来颜色差异并不会太大但帧率能提升不少。卡顿还有一个容易被忽略的原因开了requestRenderMode但忘记在数据更新后调用viewer.scene.requestRender()导致画面停在上一帧看起来像卡死。严格来说这时功能还在运行但视觉上不动。我建议在动态模拟场景里帧循环更新逻辑末尾无条件调用一次requestRender()保证每次材质替换都能被及时绘制。5.4 在3D Tiles场景里热力图被模型盖住或裁剪实际业务中洪水模拟经常要叠加到城市建筑、地形模型上这时候矩形实体和3D Tiles的遮挡关系需要处理。如果热力图应该“悬浮”在模型表面之上可以把矩形的classificationType调整为只在模型表面以上显示或者提高矩形的height。如果希望洪水在地形上顺着地势走那就需要采样地形高度把每个热力点对应的高度值补充进数据模型再把矩形变成不规则的Polygon并沿着地形起伏。这块已经超出Heatmap.js的能力范围建议结合“高程数据WebGL Cesium”相关的Terrain采样方案来做。我在一个城市内涝演示项目里是把热力图作为“积水等级分布”贴在建筑物地面模型上用Viewer.scene.pick去获取模型节点再根据热力图点位的二维坐标反查高程最终生成一张带有高度起伏的多边形水面。这种方案不需要手工写GLSL但需要仔细处理坐标转换属于进阶玩法。5.5 Cesium滚动地球时崩溃或卡死这个标题虽然有点吓人但真遇到过。原因多数是开了持续渲染同时频繁替换Canvas材质导致GPU纹理频繁重新分配最终触发浏览器崩溃。解决办法很简单开启requestRenderMode并且尽量复用Canvas。另外如果你在页面销毁时没有销毁Cesium Viewer和Heatmap实例内存泄漏也会越积越严重。正确销毁方式viewer.destroy(); heatmapInstance.setData({ max: 1, data: [] });在单页应用里不要反复创建Cesium.Viewer最好全局只保留一个实例。5.6 Cesium 1.120里的API变化提醒1.120版本虽然不算大版本但一些用法已经调整。比如viewer.imageryLayers相关操作越来越不推荐手动管理ImageryLayer而ImageryProvider也推荐使用新的数据源API。在写业务代码时尽量看当前版本的Cesium中文文档不要拿两三年前的代码直接套否则很容易碰到“某个属性不见了”的坑。如果遇到ImageMaterialProperty的image传canvas不生效先检查Cesium版本再检查canvas是否已经绘制完成。最后再分享一个小技巧在暴雨洪水这类场景中我很建议给Cesium加上一个简单的动态光照效果比如用viewer.scene.light调整环境光强度或者临时添加一个点光源模拟闪电配合热力图的水深变化大屏演示的沉浸感会强很多。它不涉及自定义着色器纯粹是Cesium内置能力几分钟就能调完。这个项目的底座我已经跑稳了以后无论是接入雷达实测数据进行实时反演还是叠加单体化楼栋做内涝风险分级都能在现有框架上继续加不用推翻重来。
返回列表