简介这份资源围绕 Cesium 与克里金插值展开面向需要在三维地球场景中呈现连续温度场的 WebGIS 开发者与地理信息专业学习者。它解决的核心问题是如何把离散、不规则分布的温度观测点通过变异函数分析与空间插值平滑转换为覆盖整个三维空间的连续温度表面从而在 Cesium 地球模型上直观展示温度变化趋势。压缩包共 585 个文件约 7.27MB以 321 个 js 脚本为主体配合 141 个 png、53 个 jpg 图像资源以及 29 个 json 配置、27 个 css 样式、10 个 svg 图标和少量 wasm、html、xml 等文件构成一套可直接运行的前端工程结构。目前已有 167 人学习下载。读者可从中获取克里金插值的实现思路、Cesium 场景集成方式、温度数据渲染流程与工程目录组织参考适合作为三维气象可视化或空间插值练习的起步模板。1. 当温度数据遇上三维地球这套 Cesium 克里金插值方案能解决什么如果你手头有一堆气象站点的离散温度数据想把它铺成一张覆盖整个区域的热力图再叠加到 Cesium 的三维地球上那你大概率绕不开「插值」这一步。站点就那么几十上百个直接画点太单薄画成色斑又不够连续克里金插值就是用来把离散点变成连续曲面的经典手段。这个项目做的事情很具体在 Cesium 场景里加载站点温度数据用克里金算法算出网格化的插值结果再以贴地纹理或图元的方式渲染出来让温度分布在地球上「看得见」。它适合做气象可视化、环境监测大屏、农业墒情展示这类需要空间连续表达的开发者也适合想搞明白插值结果怎么落到 Cesium 图元上的前端同学。整套逻辑不依赖后端出图浏览器里就能算完、画完。2. 技术选型与整体架构从站点数据到地球表面的一条链路2.1 为什么是 Cesium 克里金而不是别的组合先把这个组合的合理性说清楚不然后面改起来容易走偏。Cesium 负责三维场景、相机、地形和图层管理它本身不提供任何插值能力你给它什么纹理或几何它就画什么。克里金插值负责把「点」变成「面」它的优势在于不是简单按距离加权而是引入了变异函数模型能根据数据的空间自相关性来分配权重出来的曲面比反距离加权更贴合真实分布尤其在站点稀疏的地方不会出现明显的「牛眼」伪影。常见做法是前端用kriging.js这类纯 JS 库做计算它不依赖任何后端服务输入站点坐标和值输出一个网格矩阵。然后把这个矩阵转成 Cesium 能吃的格式——要么是Canvas贴图挂到Rectangle或Entity上要么是逐网格构造Primitive。前者性能好、代码少后者灵活但顶点数一上去就卡。这个项目走的是贴图路线因为温度场本质是连续标量场用纹理表达最自然也最省 GPU。技术栈大致是Cesium 做三维底座kriging.js 做插值计算Canvas 做中间渲染载体原生 JS 或框架做数据组织。没有引入 WebGL 着色器手写插值因为那属于另一个量级的复杂度对大多数业务场景没必要。2.2 目录结构与模块职责一个能跑起来的工程目录通常长这样我按职责拆开讲project/ ├── src/ │ ├── main.js // 入口初始化 Cesium Viewer │ ├── data/ │ │ └── stations.js // 站点数据含经纬度和温度值 │ ├── kriging/ │ │ ├── kriging.js // 插值库或 npm 引入 │ │ └── interpolate.js // 封装输入站点输出网格矩阵 │ ├── render/ │ │ ├── canvasLayer.js // 把网格矩阵画到 Canvas │ │ └── cesiumLayer.js // 把 Canvas 挂到 Cesium 实体 │ └── config.js // 插值范围、网格分辨率、色带配置 ├── index.html └── package.jsoninterpolate.js是核心封装层它把 kriging.js 的调用细节藏起来对外只暴露一个函数给站点数组和边界范围返回二维数组。canvasLayer.js负责把数值映射成颜色这里要处理色带、透明度和无效值。cesiumLayer.js负责把 Canvas 变成 Cesium 的Entity或ImageryLayer并处理贴地、层级和刷新。config.js把可调参数集中改范围、改分辨率不用翻业务代码。提示站点数据建议用[lon, lat, value]的三元组数组不要用对象数组插值库内部循环时对象取值比数组索引慢站点上千时差距明显。2.3 数据流一次插值渲染的完整链路链路是这样的main.js初始化 Viewer 后从stations.js拿到站点数组传给interpolate.js。interpolate.js先根据config.js里的边界范围生成网格坐标再调用 kriging.js 的train和predict得到每个网格点的插值温度。这个二维矩阵交给canvasLayer.js后者创建一个离屏 Canvas逐像素或逐网格填色色带由温度范围映射。最后cesiumLayer.js把这个 Canvas 转成Entity的rectangle材质或者用SingleTileImageryProvider挂成图层加到 Viewer 上。整条链路是同步的站点不多时几十毫秒算完。站点上千、网格 200×200 以上时计算会阻塞主线程常见做法是丢进Web Worker算完再回主线程画。这个项目默认没上 Worker但interpolate.js的接口设计留了口子改成异步返回 Promise 不影响调用方。3. 核心模块拆解插值计算、Canvas 着色与 Cesium 挂载3.1 插值封装把 kriging.js 包成业务能用的函数kriging.js 的原生 API 有点绕直接调容易懵。我一般会在interpolate.js里包一层把「训练模型」和「预测网格」两步合并成一个函数。下面这段是核心逻辑// interpolate.js import kriging from ./kriging.js; /** * 对站点数据做克里金插值返回网格矩阵 * param {Array} stations - [[lon, lat, value], ...] * param {Object} bounds - { minLon, maxLon, minLat, maxLat } * param {Number} gridSize - 网格边长如 100 表示 100x100 * returns {Array} 二维数组[row][col] 插值结果 */ export function interpolate(stations, bounds, gridSize 100) { // 提取坐标和值kriging.js 要求分开传 const lons stations.map(s s[0]); const lats stations.map(s s[1]); const values stations.map(s s[2]); // 变异函数模型选 gaussian温度场通常连续性好gaussian 比 spherical 平滑 const model gaussian; const sigma2 0; // 无块金效应时设 0有测量误差可调大 // 训练传入所有站点得到变异函数参数 const variogram kriging.train(values, lons, lats, model, sigma2); // 生成网格坐标 const grid []; const lonStep (bounds.maxLon - bounds.minLon) / (gridSize - 1); const latStep (bounds.maxLat - bounds.minLat) / (gridSize - 1); for (let row 0; row gridSize; row) { const rowData []; for (let col 0; col gridSize; col) { const lon bounds.minLon col * lonStep; const lat bounds.minLat row * latStep; // 逐点预测这是最耗时的部分 const val kriging.predict(lon, lat, variogram); rowData.push(val); } grid.push(rowData); } return grid; }逻辑说明kriging.train接收值数组、经度数组、纬度数组和模型名返回一个变异函数对象里面包含拟合出的块金值、基台值和变程。kriging.predict接收单个经纬度和这个对象返回该点的插值结果。双重循环里逐点预测是性能瓶颈gridSize每翻一倍计算量翻四倍。参数说明model可选gaussian、spherical、exponential温度场用gaussian出来的曲面最平滑sigma2是块金效应站点数据本身有测量误差时设一个小的正值比如 0.1能让曲面不那么「死板」gridSize建议 80 到 150 之间再大浏览器算起来就吃力了而且纹理分辨率超过屏幕像素也没意义。3.2 Canvas 着色把温度值映射成颜色拿到网格矩阵后不能直接扔给 Cesium得先画到 Canvas 上。这一步的关键是色带映射和无效值处理。插值结果里可能出现NaN或超出合理范围的值得兜住。// canvasLayer.js /** * 把插值网格画到 Canvas * param {Array} grid - 二维数组 * param {Object} colorRange - { min, max } 温度范围 * param {Array} colorStops - [[0, [r,g,b]], [0.5, [r,g,b]], [1, [r,g,b]]] * returns {HTMLCanvasElement} */ export function gridToCanvas(grid, colorRange, colorStops) { const rows grid.length; const cols grid[0].length; const canvas document.createElement(canvas); canvas.width cols; canvas.height rows; const ctx canvas.getContext(2d); const imageData ctx.createImageData(cols, rows); const { min, max } colorRange; const span max - min || 1; for (let row 0; row rows; row) { for (let col 0; col cols; col) { let val grid[row][col]; // 无效值兜底用最小值填充避免出现黑块 if (!isFinite(val)) val min; // 归一化到 0-1 let t (val - min) / span; t Math.max(0, Math.min(1, t)); const [r, g, b] interpolateColor(t, colorStops); const idx (row * cols col) * 4; imageData.data[idx] r; imageData.data[idx 1] g; imageData.data[idx 2] b; imageData.data[idx 3] 200; // 透明度200 左右既看得见底图又不糊 } } ctx.putImageData(imageData, 0, 0); return canvas; } // 色带插值在 colorStops 之间做线性过渡 function interpolateColor(t, stops) { for (let i 0; i stops.length - 1; i) { const [t0, c0] stops[i]; const [t1, c1] stops[i 1]; if (t t0 t t1) { const k (t - t0) / (t1 - t0); return [ Math.round(c0[0] (c1[0] - c0[0]) * k), Math.round(c0[1] (c1[1] - c0[1]) * k), Math.round(c0[2] (c1[2] - c0[2]) * k) ]; } } return stops[stops.length - 1][1]; }逻辑说明createImageData创建与网格同尺寸的像素缓冲逐像素写 RGBA。interpolateColor在预设色带节点之间做线性插值比如从蓝色低温过渡到红色高温。透明度设 200 是为了让底图地形还能透出来全不透明会盖住山脉水系。参数说明colorRange的min和max决定色带映射的上下限设得太窄会导致大量点被截断到两端设得太宽则中间颜色区分度低建议取站点值的 5% 和 95% 分位数colorStops至少三个节点蓝-白-红是最常用的温度色带想更精细可以加到五六个节点。3.3 Cesium 挂载把 Canvas 变成地球上的图层最后一步是把 Canvas 贴到 Cesium 上。有两种常见做法我分别说下适用场景。第一种是Entityrectangle材质适合单个区域、需要跟随实体属性管理的场景// cesiumLayer.js import * as Cesium from cesium; /** * 把 Canvas 作为矩形材质挂到 Viewer * param {Cesium.Viewer} viewer * param {HTMLCanvasElement} canvas * param {Object} bounds - 经纬度范围 * returns {Cesium.Entity} */ export function addCanvasEntity(viewer, canvas, bounds) { const entity viewer.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees( bounds.minLon, bounds.minLat, bounds.maxLon, bounds.maxLat ), material: new Cesium.ImageMaterialProperty({ image: canvas, transparent: true }), // 贴地避免悬空 height: 0, classificationType: Cesium.ClassificationType.TERRAIN } }); return entity; }第二种是SingleTileImageryProvider适合把插值结果当成一个独立图层能控制层级、透明度和显隐// 作为影像图层挂载适合需要图层管理的场景 export function addCanvasLayer(viewer, canvas, bounds) { const provider new Cesium.SingleTileImageryProvider({ url: canvas.toDataURL(), rectangle: Cesium.Rectangle.fromDegrees( bounds.minLon, bounds.minLat, bounds.maxLon, bounds.maxLat ) }); const layer viewer.imageryLayers.addImageryProvider(provider); layer.alpha 0.8; // 图层整体透明度 return layer; }逻辑说明Entity方式把 Canvas 当材质Cesium 内部会把它转成纹理优点是能跟其他实体一起管理缺点是贴地时如果地形起伏大矩形是平面的会跟地形有穿插。ImageryLayer方式把 Canvas 转成 dataURL 再包成 Provider优点是能利用 Cesium 的图层混合和层级控制缺点是 dataURL 转换有性能开销Canvas 大时明显。参数说明classificationType设为TERRAIN表示贴地形分类适合地形起伏区域alpha控制图层透明度0.8 左右既能看清温度分布又不完全遮住底图height: 0配合贴地如果地形有高度矩形会浮在椭球面上而不是地形上需要改用GroundPrimitive或clampToGround。注意Canvas 尺寸不要超过 2048×2048部分 GPU 对超大纹理支持不好会静默失败变成黑块。网格 150×150 对应 Canvas 150×150完全够用。4. 避坑与常见问题那些让我返工三次的细节4.1 插值结果全是一个值或大片 NaN现象渲染出来整片同色或者大片透明。原因通常是站点数据里混入了无效值比如null、undefined或字符串kriging.js 训练时不会报错但变异函数拟合会崩预测结果全是NaN。解决在interpolate入口先过滤一遍stations.filter(s isFinite(s[2]))同时检查经纬度是否在合法范围。我一般还会在控制台打印站点数量和值域确认数据没问题再往下走。4.2 温度场出现「牛眼」同心圆现象每个站点周围出现一圈圈同心圆像靶子。原因是变异函数模型选错了或者块金效应设得太大。spherical模型在站点密集时容易出牛眼换成gaussian或exponential能缓解。另外sigma2设 0 时曲面最平滑设大了反而引入噪声。解决温度场统一用gaussiansigma2保持 0除非站点数据本身有测量误差。4.3 Canvas 贴到 Cesium 后颜色发暗或偏色现象Canvas 里看着正常的颜色贴到地球上变暗了。原因是 Cesium 默认对影像图层做了伽马校正和色彩空间转换而 Canvas 的ImageData是 sRGB 空间。解决在ImageMaterialProperty里设color: Cesium.Color.WHITE不做额外调制或者把 Canvas 的 alpha 通道设成 255 再整体调图层alpha。如果还偏检查viewer.scene.highDynamicRange是否开启关掉它颜色会正常很多。4.4 站点上千时页面卡死现象调用插值函数后浏览器无响应几秒。原因是双重循环逐点预测是 O(n²) 复杂度站点数和网格数乘积上去就爆。解决把interpolate丢进Web Worker主线程只负责传数据和收结果。Worker 里不能直接操作 DOM但 Canvas 可以在主线程画Worker 只返回网格矩阵。另一个办法是降低gridSize80×80 在大多数屏幕上已经看不出颗粒感。4.5 矩形贴地后跟地形穿插现象温度图层一半埋在山里一半浮在空中。原因是rectangle是平面几何而地形有起伏。解决改用GroundPrimitive配合GeometryInstance或者用SingleTileImageryProvider挂成影像图层影像图层默认贴地。如果必须用 Entity设heightReference: Cesium.HeightReference.CLAMP_TO_GROUND但矩形四个角会各自贴地中间还是平面地形起伏大时依然有误差。5. 二次开发与改造从能跑到好用还差哪几步5.1 把插值计算挪进 Web Worker默认版本在主线程算站点一多就卡。改造思路是把interpolate.js整个搬进 Worker主线程通过postMessage传站点和边界Worker 算完把网格矩阵传回来。注意网格矩阵是二维数组结构化克隆有开销可以转成Float32Array一维数组再传主线程收到后按行还原。下面是个 Worker 的骨架// kriging.worker.js import { interpolate } from ./interpolate.js; self.onmessage function (e) { const { stations, bounds, gridSize } e.data; const grid interpolate(stations, bounds, gridSize); // 转成一维 Float32Array减少传输开销 const flat new Float32Array(gridSize * gridSize); for (let r 0; r gridSize; r) { for (let c 0; c gridSize; c) { flat[r * gridSize c] grid[r][c]; } } self.postMessage({ flat, gridSize }, [flat.buffer]); };主线程这边用new Worker(new URL(./kriging.worker.js, import.meta.url), { type: module })创建收到消息后把flat还原成二维再走 Canvas 流程。传输时带上[flat.buffer]是转移所有权避免拷贝大矩阵时能省几十毫秒。5.2 色带做成可配置支持动态切换温度色带在不同业务里要求不一样气象常用蓝白红农业可能用绿黄红。把colorStops抽到config.js再暴露一个setColorStops方法切换时重新跑一遍gridToCanvas和图层更新。注意不要重新插值网格矩阵缓存起来只重画 Canvas。这样切换色带是毫秒级用户感知不到延迟。5.3 加时间轴做多时刻温度动画温度数据通常有时序每个小时一组站点值。改造思路是预计算所有时刻的网格矩阵缓存成数组然后用 Cesium 的Clock驱动每个 tick 切换当前网格并重画 Canvas。预计算会占内存100 个时刻 × 100×100 网格 × 4 字节 ≈ 4MB可以接受。如果时刻更多改成按需插值加 LRU 缓存避免一次性算完卡死。5.4 验证改造是否正确的几个检查点改完之后别急着上线按这几个点过一遍站点值域和插值结果值域是否一致差太多说明变异函数拟合有问题边界四角的插值值是否合理外推区域容易出极端值切换色带后图层是否正常刷新有没有残留旧 CanvasWorker 模式下连续触发多次插值有没有内存泄漏URL.revokeObjectURL该调就调。我自己的习惯是每次改完插值参数或渲染方式都先拿一组已知分布的测试站点跑一遍比如三个站点呈三角形、值分别是 0、50、100看插值曲面是不是从冷到热平滑过渡。这个「三角形测试」帮我逮住过好几次变异函数参数写反的低级错误。从那以后我每次动插值相关代码都强制走一遍这个测试再往下做。希望帮到你。本文还有配套的精品资源点击获取