简介面向前端开发者的Cesium卫星雷达三维可视化入门示例以可运行的HTML页面为载体演示如何在三维场景中集成真实卫星雷达图像适合对WebGL、地理空间数据可视化感兴趣的初学者快速上手也适合GIS工程师参考项目组织方式。压缩包共11个文件包括9张卫星与气象雷达PNG图像和2个HTML动态页面整体仅2.88MB。图像文件可作为Cesium影像图层的本地数据源页面则直接展示如何通过Cesium API加载图像并配合时间轴生成动态效果。这一组合的价值在于既能通过真实样本观察卫星雷达数据的原始形态又能借助完整HTML代码理解从数据到三维场景的映射思路包括相机控制、图层叠加、时序切换等关键操作。已有1904人学习下载适合希望以轻量案例切入Cesium开发、拓宽前端技能边界的读者。1. 拆开这份 Cesium 卫星雷达示例它不只有两个 HTML而是完整的遥感数据演示第一次解压「cesium卫星雷达.zip」时我预感又是一堆需要配置服务端的半成品结果看到的是 4 张卫星 PNG、5 张雷达 PNG 和两个 HTML。真正让我意外的是这些文件命名里藏着完整的时间序列气象卫星动态图.html 和气象雷达动态图.html 已经把 Cesium 三维场景搭好了打开就能看到卫星云图和雷达回波在 3D 地球上切换。它不是给你一个炫酷但不可改的 demo而是给了「数据 加载代码 动画逻辑」三件套。适合两类人想在三天内把 Cesium 接入气象/雷达数据展示的前端以及想搞懂影像贴图、多时相轮播和坐标对齐的开发者。下面我按“数据从哪来、怎么贴上去、怎么动起来”的顺序把它拆透。2. 先从数据链路看起HDF 转 PNG 与文件命名的时间规律2.1 资源包里的文件清单与分组逻辑解压后文件分三组看起来乱其实分组很清楚目录/位置文件作用sate/SATE_L1_F2G_VISSR_MWB_NOM_FDI-201906171300.HDF.png卫星云图13 时sate/SATE_L1_F2G_VISSR_MWB_NOM_FDI-201906171400.HDF.png卫星云图14 时sate/SATE_L1_F2G_VISSR_MWB_NOM_FDI-201906171500.HDF.png卫星云图15 时sate/SATE_L1_F2G_VISSR_MWB_NOM_FDI-201906171600.HDF.png卫星云图16 时radar/201906211112.PNG 等 5 张气象雷达回波图11:12、11:24、11:30、11:36、11:42根目录气象卫星动态图.html / 气象雷达动态图.htmlCesium 三维展示入口文件名不是随便起的。以SATE_L1_F2G_VISSR_MWB_NOM_FDI-201906171500.HDF.png为例SATE 代表卫星数据L1 是级别一产品F2G 可以理解为全盘扫描Full DiskVISSR 是可见光红外自旋扫描辐射计MWB 是水汽波段NOM 是标称投影Normalized ProjectionFDI 是 Full Disk Image后面的数字串是 UTC 时间 2019 年 6 月 17 日 15 时 00 分。所以这一组文件其实就是某个时次的气象卫星云图每一小时一张。radar 目录下的命名更直白201906211142.PNG就是 2019 年 6 月 21 日 11 时 42 分的雷达回波图。资源包给的是 11:12 到 11:42 之间 6 分钟间隔的 5 帧刚好可以做 30 秒级别的短动画。这个时间规律很重要后面做动态轮播时帧与帧之间的切换间隔应该对应真实的数据采集间隔而不是随便设个 1 秒。比如雷达图 6 分钟一帧动画里每帧停留时间就按数据间隔的比例压缩视觉才自然。2.2 从 .HDF 到 .HDF.png用 GDAL 完成格式转换资源包里已经是转换好的 PNG但如果你从气象卫星地面站拿到原始数据往往是.HDF或.HDF5文件。浏览器不能直接解码 HDFCesium 的影像图层也不认。所以第一步是用 GDAL 把 HDF 转成 PNG/JPEG。常见做法是gdal_translate -of PNG -outsize 10% 10% \ SATE_L1_F2G_VISSR_MWB_NOM_FDI-201906171500.HDF \ SATE_L1_F2G_VISSR_MWB_NOM_FDI-201906171500.HDF.png这条命令把 HDF 文件转成 PNG同时用-outsize 10% 10%把尺寸缩小到原来的 10%。原因是原始标称投影全盘图往往有 5500×5500 像素直接贴进 Cesium 会白白占用显存加载也慢前端展示缩到 10% 后肉眼几乎看不出差别。如果只要某个区域比如中国区域可以加-srcwin x y width height裁剪或者先用gdalinfo查看投影和波段信息gdalinfo SATE_L1_F2G_VISSR_MWB_NOM_FDI-201906171500.HDF输出里会看到Upper Left、Lower Right的经纬度坐标还有投影描述。这些角点坐标就是后面在 Cesium 里设置 rectangle 的依据。如果 gdalinfo 结果显示的是投影坐标而不是经纬度记得用gdalwarp -t_srs EPSG:4326先转成 WGS84 经纬度再输出 PNG。我一般会新建一个convert.sh把循环转换也写进去批量处理多时次文件。2.3 Cesium 里的角色分配卫星图当底图雷达图当叠加层这个资源包把两种数据分开做成了两个 HTML是有道理的。卫星云图覆盖全球或大区域适合作为三维地球的影像层而雷达回波通常只覆盖一个省份或流域范围小但时效性强应该作为半透明叠加层盖在底图上面让用户看得到回波位置又不遮挡地形和地物。所以在 Cesium 里卫星图我用SingleTileImageryProvider直接加载雷达图同样用这个 Provider但要多设置一个 rectangle经纬度范围。两张图都贴到地球上但图层顺序不一样雷达图层要放在卫星图层的上面alpha设为 0.5-0.7不然盖得严严实实底下什么也看不见。这里还要提醒一点.HDF.png虽然是 PNG但它是从 HDF 转出来的可能不带地理信息。Cesium 不会自动去读 PNG 里的坐标标签它需要你在代码里明确告诉它“这张图覆盖哪块经纬度”。这就是为什么资源包的实现里几乎没有影像配置而把重点放在 rectangle 上。后面第三章我会把加载代码写出来你不要只抄代码要理解那个 rectangle 是从哪来的。3. 把 PNG 贴到三维地球SingleTileImageryProvider 加载与图层管理3.1 最小可运行环境先起一个不带默认底图的 ViewerCesium 的 Viewer 默认会加载 Ion 的 Bing 底图但在本地项目里没有配置 token 时这个默认加载会报错或长时间白屏。做卫星雷达展示时我通常先把默认底图关掉专心加载自己的影像。初始化代码像这样!DOCTYPE html html langzh-cn head meta charsetutf-8 titleCesium 卫星雷达示例/title link hrefCesium/Widgets/widgets.css relstylesheet script srcCesium/Cesium.js/script /head body div idcesiumContainer stylewidth:100%;height:100vh;/div script const viewer new Cesium.Viewer(cesiumContainer, { baseLayer: false, // 不同版本可能是 imageryProvider: false baseLayerPicker: false, animation: true, timeline: true, fullscreenButton: true, geocoder: false }); /script /body /html这里的baseLayer: false在 Cesium 1.9x 以后是关闭默认影像层的写法老版本里写作imageryProvider: false。你用的版本如果报错切换一下参数名就行。animation和timeline开启是因为后面做动态雷达动画要用到 Cesium 的时钟控件baseLayerPicker关掉是为了避免用户把底图切走导致卫星图被覆盖。初始化只是第一步。资源包里的卫星动态图 HTML 其实还会设置一个初始相机位置把你带到目标区域上空。常见写法是viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(120, 30, 8000000), orientation: { heading: 0, pitch: -Cesium.Math.PI_OVER_TWO, roll: 0 } });fromDegrees的第三个参数是高度单位米。如果你要展示中国区域经纬度放在 120, 30 附近高度 800 万米可以看到整个东亚。参数别照抄要根据你的数据覆盖范围来定。这个阶段最容易犯的错是忘记设置相机结果加载完影像发现镜头还在地球背面。3.2 加载单张 PNGSingleTileImageryProvider 与 rectangle 的作用Cesium 加载一张本地影像最简单的方式是SingleTileImageryProvider。它适合“整张图一次性显示”的场景不需要瓦片切割。资源包里的卫星云图就是这种4 张全盘图一张图覆盖整个可见圆面。加载代码async function addSatelliteImage(url, rectangle) { const provider await Cesium.SingleTileImageryProvider.fromUrl(url, { rectangle: rectangle }); const layer viewer.imageryLayers.addImageryProvider(provider); return layer; } // 全盘卫星图覆盖范围这里用全球作为示意 const fullDiskRect Cesium.Rectangle.fromDegrees(-180, -90, 180, 90); const satLayer await addSatelliteImage( sate/SATE_L1_F2G_VISSR_MWB_NOM_FDI-201906171500.HDF.png, fullDiskRect );SingleTileImageryProvider.fromUrl是新版 Cesium 的异步构造写法老版本用new Cesium.SingleTileImageryProvider({ url, rectangle })。两者的区别是异步构造返回 Promise需要await。rectangle参数严格决定了图片怎么贴到地球上它把图片的四条边对齐到你给的经纬度范围。如果范围给小了图片会被裁掉给大了图片会被拉伸变形。卫星全盘图通常不是全球而是类似一个椭圆但作为底图直接覆盖全球也可以反正边缘都是黑色。雷达图不一样。雷达回波图一般只覆盖 300-500 公里半径正确做法是把 rectangle 设置成雷达图左下角和右上角的经纬度。比如const radarRect Cesium.Rectangle.fromDegrees(100, 20, 130, 45); const radarLayer await addSatelliteImage(radar/201906211142.PNG, radarRect); radarLayer.alpha 0.65; // 设置透明度让底图透出来fromDegrees(west, south, east, north)四个参数分别是西经、南纬、东经、北纬。雷达图范围我从资源包的文件名里无法直接读出但你打开 PNG 看一眼图片边缘的经纬网线或者在 GIS 软件里用gdalinfo -projwin验证一下就能得到准确值。这里 100,20,130,45 只是示例实际项目里千万不要瞎填否则回波位置会偏出去。图层加载后alpha是最常用的属性。它控制整个影像层的透明度0 表示完全透明1 表示完全不透明。雷达图设 0.5-0.7 比较合适既能看清回波强度又不掩盖底图。如果你发现雷达图边缘有黑边把alpha调低可以稍微掩盖但根治方法还是把 PNG 的边缘做成透明通道这在避坑章节我会详细讲。3.3 图层顺序让雷达图盖在卫星图上面但别盖住 UICesium 的imageryLayers是数组结构后添加的图层默认显示在上层。所以先加载卫星图再加载雷达图雷达图自然在卫星图上面。如果你加载顺序反了雷达图会被卫星图遮住。这时可以用imageryLayers.raiseToTop或者lowerToBottom来调整层级viewer.imageryLayers.raiseToTop(radarLayer);注意raiseToTop操作的是图层对象不是 Provider。和图层相关的透明度、显隐都通过 layer 对象操作。如果你一开始不小心先把雷达图加入后来才加卫星图卫星图会把雷达图盖住。别问我怎么知道的做动态切换时图层顺序一乱回波就“消失”了其实是被底图盖住了。再补充一个实用点如果你有多个雷达图层不同时显示建议控制layer.show false而不是直接移除。因为移除后重新创建 Provider又是一次完整的图片解码切换会有明显卡顿。隐藏图层是最廉价的控制方式代价只是多占一点显存。资源包只有 5 帧雷达图全部加载后占的内存完全可以接受所以用 show 开关最流畅。4. 动态雷达怎么做帧序列切换与 Cesium 时钟 / timeline 联动4.1 气象雷达动态图的实现原理不是视频是帧序列资源包里的气象雷达动态图.html从文件名就能看出来它不是把 5 张 PNG 做成视频而是用前端手段切换图片。原理很简单把 5 张雷达图按时间顺序排好然后每隔一段时间换一张。这就像翻页动画只是页是图片翻页动作由定时器驱动。在 Cesium 场景里这个“换图”的本质是更换SingleTileImageryProvider的 url。但前面说过SingleTileImageryProvider 创建后它的 url 是只读的不能直接改。所以常见做法有两种第一种是创建一个新的 Provider替换掉旧的 layer 的 Provider。这是最通用的做法适用于任何图片帧序列。第二种是预先创建 5 个图层对象都加载同一区域的不同时相图然后用layer.show切换显示。这种适合帧数少、图层大小固定的场景切换最快。资源包只有 5 帧建议用第二种。我给出一段可运行的切换逻辑const radarUrls [ radar/201906211112.PNG, radar/201906211124.PNG, radar/201906211130.PNG, radar/201906211136.PNG, radar/201906211142.PNG ]; // 先创建所有图层并隐藏 const radarLayers []; for (let i 0; i radarUrls.length; i) { const provider await Cesium.SingleTileImageryProvider.fromUrl( radarUrls[i], { rectangle: radarRect } ); const layer viewer.imageryLayers.addImageryProvider(provider); layer.show false; layer.alpha 0.65; radarLayers.push(layer); } radarLayers[0].show true; // 默认显示第一帧这段代码先循环加载 5 张图全部加到图层集合里然后只把第一帧显示出来。showfalse的图层不会渲染但数据已经解码在内存里切换时只改一个布尔值速度飞快。这也是动态展示的“后悔药”万一某一帧加载失败其它帧还能正常显示不会整个动画崩掉。4.2 用 Cesium Clock 驱动帧切换而不是 setInterval很多人一做动画就写setInterval但在 Cesium 场景里我强烈建议用viewer.clock。原因有两点第一Cesium 的 timeline 控件直接绑定 clock用户拖拽时间轴就能控制动画进度这是 setInterval 做不到的第二Cesium 时钟由 requestAnimationFrame 驱动帧率更平滑而且能避免标签页后台运行时的定时器堆积。示例写法let currentRadarIndex 0; viewer.clock.shouldAnimate true; viewer.clock.multiplier 2; // 动画加速倍率便于观察 viewer.clock.startTime Cesium.JulianDate.fromIso8601(2019-06-21T11:12:00Z); viewer.clock.stopTime Cesium.JulianDate.fromIso8601(2019-06-21T11:42:00Z); viewer.clock.currentTime viewer.clock.startTime; viewer.clock.onTick.addEventListener(function(clock) { // 根据当前时间计算应该显示第几帧 const elapsed Cesium.JulianDate.secondsDifference(clock.currentTime, clock.startTime); const frameDuration 30; // 每帧显示 30 秒压缩后的时长 const index Math.floor(elapsed / frameDuration) % radarUrls.length; if (index ! currentRadarIndex) { radarLayers[currentRadarIndex].show false; radarLayers[index].show true; currentRadarIndex index; } });这段代码的核心在secondsDifference和Math.floor。它把时间轴上的流逝时间映射到帧索引。frameDuration 30表示时间轴每走 30 秒就切换一帧配合 multiplier2实际现实里 15 秒就换一帧。这个值你可以按需调想要动画快速循环就调小想要仔细看某一帧就调大。使用时钟驱动的另一个好处是用户点击 timeline 上的某个时刻currentTime跳变onTick 事件随即触发动画直接跳到对应帧。这就是“拖拽时间轴回看某一时刻”的实现逻辑。如果你不想让用户拖得太快导致频繁切换可以在事件回调开头加一个阈值判断只有时间差超过一定值才切换避免闪现。4.3 用户交互暂停、帧前进、回到最新时刻动态雷达除了自动轮播通常还要提供手动控制。Cesium 自带的 animation 控件已经有播放/暂停按钮但如果项目里不用这个控件就需要自己加。一个常见做法是点击图层触发暂停document.getElementById(toggleBtn).addEventListener(click, function () { viewer.clock.shouldAnimate !viewer.clock.shouldAnimate; }); // 单帧步进暂停后点击一次进一帧 function stepFrame() { viewer.clock.shouldAnimate false; const next (currentRadarIndex 1) % radarLayers.length; radarLayers[currentRadarIndex].show false; radarLayers[next].show true; currentRadarIndex next; }手动步进时要把shouldAnimate设为 false否则时钟还在走onTick 会跟你的手动切换打架。我一般在步进函数里先停钟再切帧这样点击一次只切一帧。这类交互代码看起来简单但资源包里的实现很可能没有完全覆盖把它补上会让自己用起来顺手很多。4.4 卫星图同样可以做时间轮播雷达图能动画卫星图也能。资源包里的气象卫星动态图.html就是把 4 张不同整点时刻的全盘图按同样逻辑切换。卫星图由于是底图切换时最好不要透明淡入淡出直接换图即可。如果两小时的云图差异不大可以加frameDuration拉长让动画慢一点视觉上像云在缓慢移动。真实项目里多时次卫星图的间隔是 1 小时如果压缩到 3 秒一帧云的位移会显得跳跃。我会把 frameDuration 设成 5 秒以上配合浏览器插件看才不会有“抽搐感”。5. 避坑与排查卫星雷达落地上最常踩的五个坑5.1 坑一把 .HDF 直接给浏览器白屏现象在 Cesium 里用 url 加载.HDF文件场景加载不出来控制台提示Could not load image或者图层显示为空白一块。原因浏览器和 Cesium 只支持 PNG、JPEG、WebP 等 Web 图片格式HDF 是科学数据格式浏览器没有解码器。即使 Cesium 底层用 WebGL也绕不开图片解码这一关。解决先用 GDAL 把 HDF 转成 PNG 或 JPEG再交给 Cesium。转换方法见 2.2 节。注意资源包里的文件名是.HDF.png说明作者在转换后没改扩展名习惯你自己做项目时最好统一命名成.png避免后端 mime 类型判断出错。5.2 坑二经纬度范围填错雷达回波整体漂移现象图片贴到地球上后位置不对回波应该落在东海结果跑到四川或者图片被拉成细长条。原因rectangle的参数顺序写错或者拿到的角点坐标是投影坐标系而不是 WGS84 经纬度。雷达图 PNG 本身不带地理坐标Cesium 只能靠你给的四个角点去定位。解决先确认数据源的坐标系统。中国气象雷达数据多为兰勃特投影或极射投影直接读出的角点单位不是经纬度。我一般用gdalwarp -t_srs EPSG:4326把图像重投影到经纬度再转 PNG然后用重投影后的文件获取角点。资源包里如果直接用原始雷达拼图很可能投影信息在转换时就丢了挨个检查四个角点是基本功。检查方法很简单在代码里把 rectangle 的四个值打印出来用 Cesium 的ScreenSpaceEventHandler点一下图中明显的地标对比 Cesium 返回的经纬度和已知地标位置。5.3 坑三PNG 透明通道变成黑底盖住底图现象雷达图加载后本来应该是透明的区域变成了黑色方块底图完全看不见。原因图片本身没有 alpha 通道或者 PNG 是非透明格式。Cesium 在 GPU 上把 RGB 直接渲染黑色区域就是 RGB 全零不透明。这和一个普通 PNG 放进 PS 里看到黑底是一个道理。解决在转换雷达图时把无回波区域抠掉导出带透明通道的 PNG。用 GDAL 比较麻烦我通常用 Python 的 PIL 处理from PIL import Image import numpy as np img Image.open(radar/201906211142.PNG).convert(RGBA) data np.array(img) # 假设回波强度在图像里是灰度值背景接近黑色或特定颜色 r, g, b, a data[:,:,0], data[:,:,1], data[:,:,2], data[:,:,3] mask (r 30) | (g 30) | (b 30) # 简单阈值实际可以更复杂 data[:,:,3] mask * 255 Image.fromarray(data).save(radar_alpha.PNG)这段代码把 RGB 三个通道里像素值都小于 30 的暗区域设为全透明保留回波亮区。阈值 30 是经验值雷达回波图背景一般是深蓝或黑色回波是绿黄红阈值设低一点不会把弱回波误删。处理后加载到 Cesium雷达图边缘就漂亮了。如果你不想写脚本就在 PS 里用魔棒选中背景删掉再导出 PNG-24。资源包里原始 PNG 我没验证过是否带透明通道你打开如果看到背景是白色而非透明那这一步逃不掉。5.4 坑四动态切换内存泄漏切几轮就卡死现象动画前 30 秒流畅切到第 10 轮后帧率明显下降浏览器内存持续增长最后标签页崩溃。原因每次切换都创建一个新的SingleTileImageryProvider并 addImageryProvider但旧的图层没有移除导致图层和纹理对象无限堆积。Cesium 不会自动回收你添加的图层除非你调用layer.destroy()。解决要么用预先创建的 show 开关方案4.1 节要么每次切换时先移除旧图层再添加新图层。移除要记得销毁function replaceLayer(oldLayer, newProvider, rect) { if (oldLayer) { viewer.imageryLayers.remove(oldLayer, true); // 第二个参数 true 表示销毁 } const layer viewer.imageryLayers.addImageryProvider(newProvider); layer.alpha 0.65; return layer; }remove(layer, true)里的true是销毁参数不传的话只是把图层从集合移除纹理仍留在 GPU 显存里。这是性能优化的关键细节也是大多数 i 卡顿的根源。真实项目中我发现很多开发者以为remove就完事了结果内存只增不减。你在本地做演示看不出问题放到线上数据量大时就彻底翻车。5.5 坑五本地打开正常部署到服务器后图片加载失败现象双击 index.html 看得很爽用 nginx 部署后雷达图区域一片空白控制台报跨域错误No Access-Control-Allow-Origin header。原因Cesium 通过 fetch/XHR 加载影像图片浏览器对跨域请求有限制。本地 double-click 打开是 file:// 协议和图片同源所以正常放到 http:// 服务器后如果图片路径是相对路径同域问题不大但如果图片静态资源被单独放在别的域名比如 CDN就会触发跨域。解决把 HTML、JS、图片全部放在同一个域下部署时用相对路径。如果图片确实在另一个域给那个域开 CORSnginx 配置里加add_header Access-Control-Allow-Origin *;。开发阶段如果用了 vite/nginx 代理可以把图片接口代理到本地绕过跨域。这个问题看起来简单但很值得排查遇到部署后白屏先打开 F12 看 network 里 PNG 请求的状态如果是CORS error直接想到这个坑。6. 收尾技巧参数调优与用锚点验证对齐6.1 把参数抽成配置别写死资源包里的 HTML 是写死的路径和范围你要改造自己的项目时建议建一个配置文件集中管理const CONFIG { satellite: { urls: [sate/...1300.HDF.png, sate/...1400.HDF.png], rectangle: Cesium.Rectangle.fromDegrees(-180, -90, 180, 90), frameDuration: 5, alpha: 1 }, radar: { urls: [radar/201906211112.PNG, radar/201906211124.PNG], rectangle: Cesium.Rectangle.fromDegrees(100, 20, 130, 45), frameDuration: 1.5, alpha: 0.65 } };这样调位置只改 rectangle调速度只改 frameDuration不动业务代码。我一般在交付时会把这份配置单独抽成config.js同事接手也看得懂。6.2 锚点验证用三个已知地物确认影像没漂移雷达图贴得准不准肉眼不一定看得出来尤其你只有回波没有底图参照时。我的习惯是找三个明显的地标例如大连半岛、太湖、某条河流的拐弯。在 Cesium 里加三个点标记然后加载影像看标记是否严格压在影像对应位置上。示例const anchors [ { lon: 121.6, lat: 38.9, name: 大连 }, { lon: 120.1, lat: 31.2, name: 太湖 }, { lon: 114.3, lat: 30.6, name: 武汉 } ]; anchors.forEach(p { viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(p.lon, p.lat), point: { pixelSize: 8, color: Cesium.Color.RED }, label: { text: p.name, font: 14px sans-serif } }); });如果点标记和影像上的地标差得超过 2-3 个像素说明 rectangle 边界有偏差。这时逐步微调 rectangle 的 west/south 或 east/north每次改 0.5 度刷新再看。我踩过最深的一个坑是把 east 和 west 写反了图是反的但单看影像看不出来用锚点一放就暴露了。从那以后我每次做影像加载都强制先放三个锚点再写动画逻辑成本很低但给我省下了大量找漂移的时间。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?