
简介这份资源是一份面向前端开发者的Cesium三维开发示例聚焦卫星雷达数据可视化适合需要学习三维GIS与遥感影像展示的工程师。它通过两个HTML页面分别展示气象卫星动态图和气象雷达动态图演示如何利用Cesium在浏览器中创建三维地球场景并将卫星雷达影像同步加载到对应地理位置上。资源总共包含11个文件其中2个HTML负责三维场景构建与动态播放控制9张PNG图片为不同时刻的卫星/雷达影像帧整体压缩包约2.88MBHDF等卫星雷达原始数据可先转换为PNG影像帧再通过Cesium按时间序列加载形成动态效果。目前已有1902人学习该资源。通过此示例可掌握Cesium基础API调用、影像图层添加、相机视角设置以及用多帧图片实现时间序列动态效果的基本思路为后续开发环境监测、灾害响应等三维GIS应用打下基础。1. 卫星雷达示例是 Cesium 三维开发里性价比最高的“跳板”刚把 Cesium 的new Viewer(...)跑通看到“卫星雷达示例”这个题目第一反应大多是这不就是画一个带扫描线的圆吗真做起来会发现它把前端三维开发里最难的一组问题一次占全了经纬高坐标与世界坐标如何互转Entity 与 Primitive 谁该承担高频更新CallbackProperty的刷新机制为什么有时不生效以及一个纯 HTML 页面要怎样配到既能看效果又不会把 CPU 烧满。卫星雷达这个例子好就好在规模小但五脏俱全。读完这套实现你可以直接拿去做低空监控雷达、气象雷达扇扫效果或智慧园区里的电子围栏。我会按一条线走先用 HTML 把 Cesium 场景拉起来再画轨道和覆盖圈再让扫描线转起来最后给几个能节约半天排错的参数和验证手段。新手可以照抄熟手可以重点看第 4、5 章的边界条件。2. 用 HTML 初始化 Cesium 雷达场景从 Viewer 到可复制的模板我见过很多用了半年 Cesium 的人还在每个页面里复制同一段八个false的 Viewer 配置出了问题先怀疑代码逻辑其实十次里有七次是初始化阶段埋的雷。初始化只要抓好三件事后面雷达示例就不会越写越乱。2.1 在 HTML 里引入 Cesium先管好资源路径和影像源第一件事是CESIUM_BASE_URL。直接打开一个引了Cesium.js的页面看到控制台提示Could not locate或者Unable to load基本都是这个环境变量没有指向静态资源目录。它决定 Workers、Assets、Widgets 的寻址你必须让它指向保存Cesium.js同级Build/Cesium目录。第二件事是widgets.css。不引 CSS初始化不报错但页面上控件位置全部乱掉按钮根本点不到。雷达示例里我们虽然关掉了大部分控件但 Cesium 内部依赖这份样式表维持场景容器和 canvas 的尺寸。第三件事是影像源。官方示例默认使用 Ion 的 Bing 影像需要配置Ion.defaultAccessToken。自己跑示例时要么配 token要么在 Viewer 参数里明确传baseLayer: false否则会看到控制台报 401地球也是灰的。底图对雷达效果不是必须的黑背景反而更容易看见扫描圈。下面是单 HTML 即可运行的初始化模板注意latest只用于原生示例演示!DOCTYPE html html langzh-CN head meta charsetutf-8 / meta nameviewport contentwidthdevice-width, initial-scale1 / titlecesium 卫星雷达示例 - html 三维开发实例/title style html, body, #cesiumContainer { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; background: #06121f; } /style script // CESIUM_BASE_URL 必须指向 Cesium 静态资源目录 // 否则 Worker、Assets、控件样式都会找不到。 window.CESIUM_BASE_URL https://cdn.jsdelivr.net/npm/cesiumlatest/Build/Cesium; /script link hrefhttps://cdn.jsdelivr.net/npm/cesiumlatest/Build/Cesium/Widgets/widgets.css relstylesheet / script srchttps://cdn.jsdelivr.net/npm/cesiumlatest/Build/Cesium/Cesium.js/script /head body div idcesiumContainer/div script const viewer new Cesium.Viewer(cesiumContainer, { animation: false, timeline: false, baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, infoBox: false, selectionIndicator: false, fullscreenButton: false, // 不接默认 Ion 影像方便没有 token 时直接打开 baseLayer: false }); viewer.scene.globe.enableLighting true; viewer.scene.skyAtmosphere.show true; // 打开按需渲染后面雷达动画要手动 requestRender viewer.scene.requestRenderMode true; viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 10000000), orientation: { heading: 0, pitch: Cesium.Math.toRadians(-90), roll: 0 } }); /script /body /html上面这段代码做了三件事先把CESIUM_BASE_URL指到 CDN 上的Build/Cesium目录然后创建Viewer时把所有与示例无关的控件关掉最后开启requestRenderMode并把相机移动到目标区域上空。requestRenderMode是 Cesium 的按需渲染模式打开后它不会每帧重绘只有手动调用viewer.scene.requestRender()才会刷新一帧。这个配置对雷达这类“只有扫描线在动”的场景很有用能显著降低 GPU 空转。关键参数说明参数值作用animation/timelinefalse去掉时间轴控件雷达扫描用preUpdate驱动baseLayerfalse避免未配置 Ion token 时影像请求失败requestRenderModetrue按需渲染配合requestRender()使用enableLightingtrue让地表有明暗变化雷达圈在暗色区域更明显2.2 第一个坐标基本功把经纬高变成 Cesium 的空间位置Cesium 的三维开发大量操作Cartesian3而不是经纬度数组。画卫星雷达时至少有三个地方要用到坐标换算卫星位置用Cesium.Cartesian3.fromDegrees(lon, lat, height)雷达覆盖圈圆心用Cesium.Cartesian3.fromDegrees(lon, lat, 0)轨道线上的连续点用Cesium.Cartesian3.fromDegreesArrayHeights。Cartesian3是地心旋转坐标系ECEF原点在地心单位是米。写雷达示例时最容易犯的错是把semiMajorAxis当成“多少度”来填或者把 800 公里直接填成 800导致图形飞到地下或画不出来。我们统一用米另外要注意fromDegrees的第三个参数是椭球面高度不是相对地表高度如果你开了地形需要结合heightReference来处理贴地对象。Cesium 中文文档里关于heightReference的例子很多但容易让人忽略它与height的区别heightReference只对 Entity 里的 billboard、label、point、ellipse 等生效且是相对地形的偏移放在position上的height才是轨道真实高度。卫星对地面扫描的轨道高度必须来自轨道数据不能靠heightReference从地面往上加。另外Cesium 里没有类似游戏引擎父子模型节点的概念雷达扫描圈和卫星点最好保持平级实体靠代码同步更新位置硬套父子关系后删除一个会连带删除另一个调试起来很痛苦。到这里初始化已经完成下一步是往场景里加卫星、轨道和雷达覆盖圈。3. 卫星雷达的轨道与覆盖圈Entity 坐标建模和性能边界我一般会把雷达场景分成两个部分静态底子用 Entity 写高频扫描用 Primitive 或动态材质写。卫星点、轨道线、雷达覆盖圈都是低频几何用 Entity 最顺手代码可读性也最好。下面这一段是一个能直接抄的addRadarBase函数。3.1 卫星点、轨道线、雷达覆盖圈的最小实现function addRadarBase(viewer, options {}) { const { lon 116.39, lat 39.9, height 600000, radius 800000 } options; // 雷达中心点既是轨道标注锚点也是后续扫描圈的圆心 const center Cesium.Cartesian3.fromDegrees(lon, lat, height); viewer.entities.add({ id: radar-center, position: center, point: { pixelSize: 10, color: Cesium.Color.CHARTREUSE }, label: { text: RADAR, font: 14px monospace, pixelOffset: new Cesium.Cartesian2(0, -20), fillColor: Cesium.Color.WHITE, showBackground: true, backgroundColor: Cesium.Color.BLACK.withAlpha(0.6), backgroundPadding: new Cesium.Cartesian2(8, 4) } }); // 雷达覆盖圈贴在地表的圆形 footprint const groundCenter Cesium.Cartesian3.fromDegrees(lon, lat, 0); const footprint viewer.entities.add({ id: radar-footprint, position: groundCenter, ellipse: { semiMajorAxis: radius, semiMinorAxis: radius, height: 0, material: Cesium.Color.CYAN.withAlpha(0.2), outline: true, outlineColor: Cesium.Color.CYAN.withAlpha(0.7) } }); // 轨道线这里用一个椭圆轨道模拟卫星绕飞 const orbitPositions []; for (let i 0; i 360; i 10) { const theta Cesium.Math.toRadians(i); orbitPositions.push( Cesium.Cartesian3.fromDegrees( lon 6 * Math.cos(theta), lat 4 * Math.sin(theta), height 300000 * Math.sin(2 * theta) ) ); } viewer.entities.add({ id: radar-orbit, polyline: { positions: orbitPositions, width: 1.5, material: new Cesium.PolylineGlowMaterialProperty({ color: Cesium.Color.GRAY.withAlpha(0.8), glowPower: 0.1 }) } }); return { footprint }; } // 调用 addRadarBase(viewer, { lon: 116.39, lat: 39.9, height: 600000, radius: 800000 });这个函数里PolylineGlowMaterialProperty让轨道在深色背景里带一点发光适合雷达示例的视觉聚焦ellipse的semiMajorAxis与semiMinorAxis相等时就是圆形覆盖区height: 0把圆贴在椭球表面。轨道点循环里同时改变经度、纬度和高度模拟的是一个倾斜轨道真实卫星轨道可以换成Cesium.SampledPositionProperty或服务器的 CZML 数据但抽换逻辑不变。参数表属性单位示例容易填错的点semiMajorAxis/semiMinorAxis米800000当成“度”覆盖圈飞到远处height米0相对 WGS84 椭球面不是相对地形pixelSize像素10相机拉远后基本看不见可适当加大polyline.width像素1.5发光材质下宽度不要超过 3否则糊成一片3.2 Entity 适合画底子Primitive 适合画高频扫描线Cesium 的 Entity 是数据驱动的上层封装写一个对象Cesium 在内部创建对应的 Primitive 并管理生命周期。对雷达示例中的卫星点、轨道线、覆盖圈Entity 的读写最方便。但一旦扫描线变成每秒更新 60 次的旋转扇面或者雷达回波目标变成几百个点Entity 的属性更新就会很啰嗦每次改positionsCesium 都要重新创建 Geometry 实例还要重新计算包围球。常用做法是高频目标直接用 Primitive。例如雷达回波用PointPrimitiveCollection扫描线用Primitive配合PolylineGeometry通过更新GeometryInstance的modelMatrix实现旋转而不是每次重造几何体。两者的边界可以看这张表对比维度EntityPrimitiveAPI 可读性好适合原型和展示层差需要自己组装 Geometry高频更新可用CallbackProperty但开销较大可复用 Geometry只改 Transform拾取与点击自带id与pick需要手动管理pickPrimitive雷达示例用途卫星、轨道、覆盖圈扫描扇区、回波粒子、距离波纹第 4 章我会用 Entity CallbackProperty快速实现扫描动画。你要清楚这是原型写法生产环境里如果雷达目标超过一百个必须把高频对象切到 Primitive否则帧率会明显下降。4. 雷达扫描动起来CallbackProperty、Canvas 贴图与帧更新雷达如果不转就是一个静态圆没法表达“扫描”这个动作。让动态效果不漂移、不卡顿重心在“每帧改什么”。Cesium 做这件事最直接的入口是CallbackProperty它接收一个函数函数返回值就是当前帧的属性值。第二个参数传false表示“这不是常量每次更新都要重新调用”很多人漏了这个false结果只有第一帧是初始角度之后场景再也不动。4.1 用 CallbackProperty 生成旋转扫描扇区下面的addRotatingSector会往场景里添加一个半透明扫描扇区。它的核心思路是让扇区的每个边界点围绕雷达圆心旋转每次CallbackProperty被调用时都基于当前角度重新计算一组 vertices。function addRotatingSector(viewer, options) { const { lon 116.39, lat 39.9, radius 600000, halfArc Cesium.Math.toRadians(25), speed Cesium.Math.toRadians(0.8) } options; const centerCarto new Cesium.Cartographic( Cesium.Math.toRadians(lon), Cesium.Math.toRadians(lat), 0 ); let bearing 0; const Re Cesium.Ellipsoid.WGS84.maximumRadius; function sectorBoundary() { const pts []; // 首位和末位都是圆心闭合多边形 pts.push(Cesium.Cartesian3.fromRadians( centerCarto.longitude, centerCarto.latitude, 0 )); for (let i -halfArc; i halfArc; i Cesium.Math.toRadians(1)) { const az bearing i; const dLon (radius / Re) * Math.sin(az) / Math.cos(centerCarto.latitude); const dLat (radius / Re) * Math.cos(az); pts.push(Cesium.Cartesian3.fromRadians( centerCarto.longitude dLon, centerCarto.latitude dLat, 0 )); } pts.push(Cesium.Cartesian3.fromRadians( centerCarto.longitude, centerCarto.latitude, 0 )); return pts; } const sector viewer.entities.add({ polygon: { hierarchy: new Cesium.CallbackProperty(sectorBoundary, false), material: Cesium.Color.ORANGE.withAlpha(0.4), outline: true, outlineColor: Cesium.Color.ORANGE.withAlpha(0.9), height: 0 } }); viewer.scene.preUpdate.addEventListener(() { bearing (bearing speed) % Cesium.Math.TWO_PI; viewer.scene.requestRender(); }); return sector; } addRotatingSector(viewer, { lon: 116.39, lat: 39.9, radius: 600000, halfArc: Cesium.Math.toRadians(25), speed: Cesium.Math.toRadians(0.8) });这段代码有几个要点。halfArc是整个扇区波束角的一半整体扇区角度是2 * halfArc常见卫星雷达扫描角在 20° 到 60° 之间示例里 25° 比较贴近真实单侧扫描。dLon计算里除以Math.cos(centerCarto.latitude)是为了补偿纬线弧长差纬度越高同样的经度差对应的地面距离越短。这个平面小范围近似在 600 公里半径内误差可接受如果你做的是几千公里轨道建议用Cesium.EllipsoidGeodesic计算大圆方向避免扫描圈在高纬度变形。speed是每一帧转过的弧度按 60 帧算0.8大约每秒转 48°视觉上像雷达扫描0.3则更接近真实载荷的慢扫速度。注意preUpdate回调里调用了viewer.scene.requestRender()这一步在requestRenderMode: true时不能省否则 Cesium 不会主动重绘扇区会一直停在第一帧。4.2 用 Canvas 画雷达渐变纹理替换外部图片有时候不希望扫描扇区用纯色填充而想让雷达覆盖圈中心亮、边缘淡表现出“能量衰减”。常见做法是用一张渐变图赋给ImageMaterialProperty。我更推荐直接用 Canvas 生成不产生额外 HTTP 请求颜色也能随时调整。function createRadarTexture(size 256) { const canvas document.createElement(canvas); canvas.width canvas.height size; const ctx canvas.getContext(2d); const gradient ctx.createRadialGradient( size / 2, size / 2, 0, size / 2, size / 2, size / 2 ); gradient.addColorStop(0, rgba(0, 255, 200, 0.8)); gradient.addColorStop(0.6, rgba(0, 255, 200, 0.25)); gradient.addColorStop(1, rgba(0, 255, 200, 0)); ctx.fillStyle gradient; ctx.fillRect(0, 0, size, size); return canvas; } const footprint viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0), ellipse: { semiMajorAxis: 800000, semiMinorAxis: 800000, height: 0, material: new Cesium.ImageMaterialProperty({ image: createRadarTexture(), transparent: true, color: Cesium.Color.CYAN.withAlpha(0.9) }) } });给ImageMaterialProperty传transparent: true是必须的否则 Canvas 的透明区域会被 Cesium 当作黑色背景扫描圈边缘会出现难看的方块。如果想换成 SVGimage也可以填 SVG 的 data URI但动态换色和渐变控制都不如 Canvas 直观。这个方法稍加改动就能做成雷达热力图改变gradient.addColorStop的颜色和 alpha就能得到红黄绿的能量衰减圈。5. 把雷达扫描做准的 3 个参数和一个验证技巧雷达示例做到“能转”只是第一步接下来要把“转的幅度”和“覆盖范围”做准。以下三个参数是我在做三维开发时最容易出错的地方。5.1 雷达半径用卫星高度和最低仰角算雷达覆盖圈不能拍脑袋填。假设卫星高度为h最低可观测仰角为e地球半径为Re则最远覆盖半径近似为R sqrt((Re h)^2 - (Re * cos(e))^2) - Re * sin(e)当e 0时退化成地平线距离公式R ≈ sqrt(2 * Re * h h^2)。例如 600 公里高度的卫星理论最大覆盖半径约 2829 公里如果要求最低仰角 10°覆盖半径会缩到 1858 公里左右。有了这个值再把它填到semiMajorAxis里覆盖圈才和卫星参数对得上。5.2 验证扫描圈点击场景反查经纬度验证覆盖圈有没有对准星下点最直接的手段是鼠标点击场景反查出实际经纬度const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((movement) { const cartesian viewer.camera.pickEllipsoid( movement.position, viewer.scene.globe.ellipsoid ); if (!cartesian) return; const carto Cesium.Cartographic.fromCartesian(cartesian); console.log( Cesium.Math.toDegrees(carto.longitude).toFixed(5), Cesium.Math.toDegrees(carto.latitude).toFixed(5), (carto.height || 0).toFixed(2) ); }, Cesium.ScreenSpaceEventType.LEFT_CLICK);点击雷达覆盖圈内部任意点在控制台比较经纬度与卫星星下点。差几度通常说明CallbackProperty里的圆心点没有闭合或经纬度换算时漏了高度参数。如果pickEllipsoid返回 undefined优先检查viewer.scene.globe.show是否为 true以及相机是否穿到了地下。最终要调好的参数表参数位置作用踩坑点halfArc扫描扇区控制雷达波束宽度填 50 而不是转 50°扇区只有一半semiMajorAxis覆盖圈雷达覆盖半径把公里当米覆盖圈缩小 1000 倍speed扇区更新扫描角速度在requestRenderMode下不requestRender()永远不转本文还有配套的精品资源点击获取