引言:在气象数据可视化领域,全球尺度的气象要素(如气压、浪高、降水量等)展示一直是个技术难点------既要保证数据的空间连续性,又要兼顾渲染性能与视觉效果。本文将分享基于Cesium引擎与heatmap.js实现全球气象可视化的完整思路,包括方案选型、核心实现及优化方向,希望能为类似需求提供参考。
一、需求与挑战
核心需求
实现全球范围的气象水文信息可视化,具体涵盖:
- 气压、浪高、降水量等大气要素
- 能见度、海表温度、海水温度等水文要素
- 以热力图形式直观呈现数值分布(基于经纬度坐标映射颜色)
技术挑战
在方案设计初期,我们面临两个关键问题:
- 渲染方式选择 :使用heatmap.js进行插值渲染时,需确定基于Cesium的
Entity
还是Primitive
实现全球覆盖 - 性能与精度平衡:全球数据量庞大(如1°分辨率网格约4万+点),如何避免加载卡顿与渲染延迟
二、方案探索:3D与2D的取舍
在正式开发前,我们先对3D和2D两种渲染方案进行了技术验证,最终根据实际场景选择了更优解。
1. 3D渲染方案:理想与现实的差距
思路 :通过网格计算将每个经纬度点的数值转换为高度,结合纹理渲染实现高低起伏的3D热力效果(类似地形可视化)。
优势 :视觉冲击力强,能直观体现数值的空间差异。
瓶颈:
- 全球范围计算复杂:当渲染范围覆盖全球或数据量超10000点时,网格拓扑计算与纹理映射的复杂度呈指数级增长,单帧渲染耗时可超过10秒
- 180°经线处理困难:在东西半球分界(-180°/180°经线)处,包围球计算与坐标归一化易出现断层,导致视觉割裂
- 动态更新性能差 :数据变化时需重新计算网格与纹理,主线程阻塞严重,无法满足实时性要求 结论:因性能与兼容性问题,3D方案仅适用于小范围高精度场景,全球尺度下放弃此方案。
2. 2D渲染方案:权衡后的选择
2D方案虽视觉效果略逊于3D,但在全球范围的稳定性与性能上更具优势。其核心思路是:用heatmap.js在Canvas上渲染热力图,将生成的图像作为Cesium实体的材质,实现全球覆盖。
三、2D方案实现细节 ### 1. 全球覆盖实体的选择:Primitive vs Entity
要实现无拼接、无锯齿的全球覆盖,需选择合适的Cesium实体承载热力图材质。我们测试了两种载体:
- 球(Sphere):因经纬度与球面纹理映射复杂(需处理极坐标畸变),放弃
- 矩形(Rectangle) :平面坐标映射简单,但需解决180°经线锯齿问题
关键发现: - 使用
Entity
渲染矩形时,-180°/180°经线处会出现随地球转动的锯齿线(因Entity内部坐标转换逻辑导致) - 改用Primitive
渲染矩形可避免此问题(直接操作WebGL底层绘制,坐标精度更高) - 核心代码(创建矩形Primitive):
js
_createRectangle(canvas) {
this.primitive = new cesium.Primitive({
geometryInstances: new cesium.GeometryInstance({
geometry: new cesium.RectangleGeometry({
rectangle: cesium.Rectangle.fromDegrees(
GEO_BOUNDS.minLon,
GEO_BOUNDS.minLat,
GEO_BOUNDS.maxLon,
GEO_BOUNDS.maxLat
),
vertexFormat: cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
height: 0 // 矩形高度
}),
id: 'rectanglePrimitive'
}),
appearance: new cesium.EllipsoidSurfaceAppearance({
material: new cesium.Material({
fabric: {
type: 'Image',
uniforms: {
image: canvas// 图片路径
}
},
translucent: true // 如果图片有透明部分,设为true
})
}),
show: false // 是否显示
});
this.viewer.scene.primitives.add(this.primitive)
}
2. 数据处理:经纬度与Canvas的映射逻辑
热力图需在Canvas上绘制,而气象数据以经纬度(-180 ~ 180°,-90~90°)表示,需解决两个核心问题:
(1)Canvas尺寸设计
参考Cesium全球影像的比例(2:1),选择5400×2700
像素(宽高比2:1),确保经纬度映射无拉伸。
(2)坐标映射函数 将经纬度转换为Canvas像素坐标,关键代码:
js
// 数据映射,将0-360°和-90°-90°映射到5400 * 2700的画布上
_latLngToXY(lon, lat) {
// 经度从0-360转换为-180-180
let adjustedLng = lon > 180 ? lon - 360 : lon;
// 限制经纬度在有效范围
// adjustedLng = Math.max(GEO_BOUNDS.minLon, Math.min(GEO_BOUNDS.maxLon, adjustedLng));
// lat = Math.max(GEO_BOUNDS.minLat, Math.min(GEO_BOUNDS.maxLat, lat));
// 计算坐标
const x = ((adjustedLng - GEO_BOUNDS.minLon) / (GEO_BOUNDS.maxLon - GEO_BOUNDS.minLon)) * MAP_WIDTH;
const y = ((GEO_BOUNDS.maxLat - lat) / (GEO_BOUNDS.maxLat - GEO_BOUNDS.minLat)) * parseInt(MAP_WIDTH / 2);
return {
x: Math.round(x),
y: Math.round(y)
};
}
(3)解决180°经线拼接问题
当数据点靠近180°经线时,热力图可能出现断裂。解决方案:对每个点添加±360°经度的"镜像点",模拟全球连续效果:
js
// 数据处理
_generateGlobalTemperatureData(data) {
const dataPoints = [];
const values = [];
for (let i = 0; i < data.length; i++) {
let { lat, lon, value } = data[i];
values.push(value);
let adjustedLon = lon > 180 ? lon - 360 : lon;
// 1. 添加原始点
const originalPoint = this._latLngToXY(adjustedLon, lat);
dataPoints.push({ ...originalPoint, value });
// 2. 添加由侧扩展点(+360°经度,对应画布右侧外部,模拟左侧区域的延续)
const rightExtendedPoint = this._latLngToXY(adjustedLon + 360, lat); // 经度+360,落在画布右侧外
dataPoints.push({ ...rightExtendedPoint, value });
// 3. 添加左侧扩展点(-360°经度,对应画布左侧外部,模拟右侧区域的延续)
const leftExtendedPoint = this._latLngToXY(adjustedLon - 360, lat); // 经度-360,落在画布左侧外
dataPoints.push({ ...leftExtendedPoint, value });
}
// 计算数据范围(增强对比度)
const min = values.length > 0 ? Math.min(...values) : 0;
const max = values.length > 0 ? Math.max(...values) : 0;
return { min: min, max: max, data: dataPoints };
}
3. 热力图渲染与集成
使用heatmap.js在Canvas上渲染处理后的数据,再将Canvas作为材质赋予矩形Primitive:
(1)创建热力图容器
js
_createHeatmapContainer() {
const el = document.createElement('div')
el.style.width = `${MAP_WIDTH}px`
el.style.height = `${parseInt(MAP_WIDTH / 2)}px`
el.style.display = "none"
document.body.appendChild(el);
return el
}
(2)渲染热力图并导出
js
_createHeatMapImage(el) {
this.heatmap = h337.create({
container: el,
radius: this.radius,
maxOpacity: this.maxOpacity,
minOpacity: this.minOpacity,
blur: this.blur,
gradient: this.gradient
})
const data = this._generateGlobalTemperatureData(this.data)
this.heatmap.setData(data)
return this.heatmap.getDataURL()
}
4. 功能封装 将上述逻辑封装为createHeatMap
类,便于复用与扩展:
js
class createHeatMap {
constructor(viewer, options = {}, data) {
this.viewer = viewer;
this.primitive = null; // 矩形Primitive
this.heatmap = null; // heatmap实例
this.data = data || [];
// 热力图配置(默认值)
this.radius = options.radius || 22;
this.gradient = options.gradient || { /* 色阶配置 */ };
// 初始化
this._initHeatMap();
}
// 初始化流程:创建容器→渲染热力图→创建矩形
_initHeatMap() {
const el = this._createHeatmapContainer();
const canvas = this._createHeatMapImage(el);
this._createRectangle(canvas);
}
// 其他方法:
// ...
}
四、性能优化方向
全球气象数据量大(如1°分辨率约4万点),需重点解决加载与渲染效率问题,目前规划以下优化方案:
1. LOD(细节层次)
- 为同一区域准备多分辨率数据(如低缩放级别用1°网格,高缩放用0.25°网格)
- 监听
Cesium.Camera
的zoom
事件,动态切换数据精度(缩放级别低时加载粗粒度数据)
2. 视口裁剪
- 通过
viewer.camera.computeViewRectangle()
获取当前视野的经纬度范围 - 仅加载视野内的数据,过滤无关点(可减少60%+的数据量)
3. 批处理与Web Worker
- 用
Primitive
批量绘制同类几何体,减少WebGL绘制调用 - 数据解析、坐标转换等耗时操作移至Web Worker,避免阻塞主线程
五、实现效果
导出图片

材质渲染

六、总结
本文通过对比3D与2D渲染方案,最终选择基于Cesium的Primitive
与heatmap.js实现全球气象可视化: - 核心思路:将热力图渲染为图片,作为矩形实体的材质覆盖全球 - 关键技术:经纬度与Canvas的坐标映射、±360°镜像点解决拼接问题 - 优势:兼顾全球覆盖的连续性与渲染性能,支持动态数据更新 后续将聚焦性能优化,通过LOD、视口裁剪等方案进一步提升大规模数据的处理能力。如果你有类似需求或优化建议,欢迎留言交流!