news 2026/10/5 7:33:03

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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的矩形实体上,用颜色表示水深,用纹理扩散表示洪水演进。整条链路里完全不用自己写GLSL,Cesium内部已经帮我们把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 rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cesium@1.120.0/Build/Cesium/Widgets/widgets.css"> <script src="https://cdn.jsdelivr.net/npm/cesium@1.120.0/Build/Cesium/Cesium.js"></script> <script src="https://cdn.jsdelivr.net/npm/heatmap.js@2.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、north,Canvas尺寸是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。这个方案可以绕开自己写GLSL,Cesium内部会把这个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时钟驱动动画并同步UI

Cesium自带的时钟系统非常适合这种动态模拟。我不建议自己开一个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[type=range],监听onTick同步值,再允许用户拖动滑块来跳转模拟时刻,这样演示时就能随意回放,方便给领导讲“水位是几点涨起来的”。


4. 完整源码参考

下面给出一份可以直接复制运行的完整HTML。代码基于Cesium 1.120与Heatmap.js 2.0.5,内置了模拟数据,开箱即用。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Cesium 1.120 + Heatmap.js 2.0.5 动态洪水模拟</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cesium@1.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[type="range"] { width: 260px; } </style> </head> <body> <div id="cesiumContainer"></div> <div id="info">当前模拟时刻:--:--</div> <div id="controls"> <button id="btnPlay">暂停</button> <input type="range" id="timeRange" min="0" max="360" value="0" step="1"> <span id="timeLabel">0 min</span> </div> <script src="https://cdn.jsdelivr.net/npm/cesium@1.120.0/Build/Cesium/Cesium.js"></script> <script src="https://cdn.jsdelivr.net/npm/heatmap.js@2.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: true,Cesium会按不透明材质去渲染,黑色背景就露出来了。第二,Heatmap.js的gradient中,起始颜色透明度必须是0,比如rgba(0, 0, 255, 0),否则整个矩形会被一层半透明底色覆盖。第三,检查Canvas尺寸,如果宽高是0,画出来自然什么都没有。

还有一个隐蔽问题:Heatmap.js的容器如果尺寸不是整数,或使用了display: none,Canvas可能渲染异常。我为了避免影响页面布局,把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内置能力,几分钟就能调完。这个项目的底座我已经跑稳了,以后无论是接入雷达实测数据进行实时反演,还是叠加单体化楼栋做内涝风险分级,都能在现有框架上继续加,不用推翻重来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 7:32:38

KeyarchOS性能基线实战:UnixBench完整跑分指南

前阵子给一批新服务器做上线前的性能基线&#xff0c;几台同款机器装的是浪潮信息KeyarchOS&#xff08;KOS&#xff09;。业务同学反馈说某些批处理任务偶尔变慢&#xff0c;我需要先确认问题出在系统配置还是硬件本身&#xff0c;于是第一件事就是把UnixBench拉起来跑了一轮。…

作者头像 李华
网站建设 2026/10/5 7:31:56

单细胞热图改造:从颜色块到多维结构视图的实践指南

前些天整理单细胞项目结果&#xff0c;又被审稿人问了一句“你的热图除了颜色深浅还能看出什么”。这句话戳到我了。单细胞转录组分析里&#xff0c;热图几乎是标配&#xff0c;但绝大多数人画出来的热图&#xff0c;就是一个“表达量颜色块”&#xff0c;既看不出细胞亚群的差…

作者头像 李华
网站建设 2026/10/5 7:31:01

OneOS OTA远程升级全解析:从双区备份到灰度发布

1. 从一次“上门维护”开始&#xff1a;为什么物联网设备必须学会OTA升级做物联网开发的同仁应该都有过这种经历&#xff1a;产品已经铺到现场&#xff0c;甚至铺了几百上千台&#xff0c;突然发现某个固件版本存在一个隐蔽的逻辑bug&#xff0c;或者客户提了一个新需求需要改配…

作者头像 李华
网站建设 2026/10/5 7:31:01

一键化远程桌面启用脚本:Windows与Linux部署及排障指南

远程桌面这东西&#xff0c;属于那种“平时想不起来&#xff0c;真到用的时候急得跳脚”的功能。我最早真正较真去搞&#xff0c;是帮同事远程处理一台异地机房的服务器故障&#xff1a;机器放在现场&#xff0c;人在办公室&#xff0c;偏偏远程桌面没开、服务也没起来&#xf…

作者头像 李华
网站建设 2026/10/5 7:31:00

COCO转YOLO避坑指南:传送带异物检测数据集处理全流程

简介&#xff1a;传送带异物检测识别数据集聚焦工业生产线上的异物检测任务&#xff0c;可支持识别铁棍、垃圾等目标&#xff0c;面向计算机视觉算法工程师、工业质检系统开发者以及相关专业学生&#xff0c;能够为算法验证、项目演示和课程实验提供带标注的真实场景数据。数据…

作者头像 李华
网站建设 2026/10/5 7:30:49

人群密度目标检测数据集:8,000张图像 | 目标检测

人群密度目标检测数据集&#xff1a;8,000张图像 | 目标检测 源码数据分享 链接: https://pan.baidu.com/s/1O4OhWVQqKTHvONkQ7IpV7A?pwduv7c 提取码: uv7c 一、引言&#xff1a;人群密度检测的安全命题 在全球城市化进程中&#xff0c;大规模人群聚集已成为城市运行的常态场…

作者头像 李华