简介:面向前端开发者的 Cesium 三维开发示例包,聚焦卫星雷达数据在三维场景中的可视化,演示如何通过 HTML 页面加载遥感底图并构建动态效果。压缩包共 11 个文件,含 2 个 HTML 示例与 9 张 PNG 卫星/雷达影像图,整体约 2.88MB,结构简洁,适合直接打开浏览器对照学习。HTML 示例分别实现了气象雷达动态图和气象卫星动态图,PNG 图片提供了对应时间片的影像数据,可作为理解数据组织与渲染流程的辅助材料。目前已吸引 1904 人学习下载,适合正在研究 Cesium API、地理空间数据展示或三维前端开发的初中级开发者。借助该示例,可以快速熟悉 Cesium 场景初始化、影像图层叠加、相机控制与动态刷新等关键操作,也能了解从常见遥感影像格式到三维地球的接入思路,为环境监测、气象展示、灾害响应等应用提供可直接改造的参考实现。
1. 用Cesium在HTML里做卫星雷达,第一步不是画波束
"cesium卫星雷达示例"这个问题,前端第一反应通常是去找一个卫星模型和一个圆锥体模型,塞进Cesium场景里。但真正常见的结果是模型有了,卫星不动,波束也不会跟着地面走。原因是卫星雷达示例的技术核心不在模型,而在时钟、坐标系和Entity/Primitive的取舍。这里不依赖任何框架,从HTML文件起步,用Cesium把雷达的扫描链路搭出来:卫星沿轨道飞行、波束指向星下点、回波脉冲闪烁,再叠加3D Tiles看真实地形效果。适合前端开发工程师,也适合刚接手Cesium的GIS开发;如果你已经在用Cesium画点线面,下面也有Entity与Primitive、动态材质和性能调优的实用信息。
2. Cesium项目初始化:用HTML搭出可运行的三维场景
2.1 最小HTML页面:从CDN加载Cesium并创建一个Viewer
先把页面跑起来,再谈雷达。Cesium可以直接用CDN,不需要Node工程,一份HTML就能承载完整的三维开发原型。下面这个页面是卫星雷达示例的基础容器,后面所有实体都挂到同一个viewer上。
<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <title>cesium卫星雷达示例</title> <style> html, body, #cesiumContainer { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; } </style> <script src="https://unpkg.com/cesium/Build/Cesium/Cesium.js"></script> <link href="https://unpkg.com/cesium/Build/Cesium/Widgets/widgets.css" rel="stylesheet"> </head> <body> <div id="cesiumContainer"></div> <script> const viewer = new Cesium.Viewer('cesiumContainer', { animation: false, timeline: false, baseLayerPicker: true, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, infoBox: false, selectionIndicator: false, shouldAnimate: true }); </script> </body> </html>这段HTML做了三件事:样式把容器撑满浏览器,CDN把Cesium的JavaScript和默认控件样式拉进来,最后用配置项初始化Viewer。其中animation和timeline默认是开启的,但卫星雷达示例的时间轴完全由代码控制,这两个控件会挡住三维视线,所以关掉。shouldAnimate: true让场景时钟在加载后自动前进,否则后面轨道和波束永远不会动。打开页面如果没报错,会看到Cesium默认的地球影像和鼠标拖动交互,这就可以进入下一步了。
2.2 Viewer参数表:雷达场景关什么、留什么
实际项目里,很多人直接new Cesium.Viewer('container'),把默认UI全部留下,结果雷达波束还没画,屏幕先被一堆按钮占满。三维开发里养成一个习惯:每个控件都要有留下的理由。卫星雷达示例的常用参数配置如下:
| 参数 | 默认 | 雷达示例里的设置 | 原因 |
|---|---|---|---|
| animation | true | false | 雷达时间由clock控制,不需要手动按钮 |
| timeline | true | false | 关闭时间线,界面留给雷达信息 |
| baseLayerPicker | true | true | 切换底图便于观察雷达覆盖区在不同影像下的对比 |
| geocoder | true | false | 搜索框和雷达场景无关 |
| homeButton | true | false | 相机位置由flyTo指定 |
| sceneModePicker | true | false | 卫星雷达必须用3D视角才能看出波束方向 |
| navigationHelpButton | true | false | 帮助按钮在演示时遮挡内容 |
| infoBox | true | false | 点击图形时避免弹出默认属性弹窗 |
| selectionIndicator | true | false | 选中特效在雷达场景里没有意义 |
| shouldAnimate | false | true | 让时钟自动前进,卫星和波束才会持续运动 |
shouldAnimate是雷达示例里最容易被忽略的参数。很多人的卫星不动,不是轨道代码写错了,而是时钟没有启动。初始化时直接给true,比之后单独写一行viewer.clock.shouldAnimate = true更直观。还需要注意,关闭animation和timeline后,viewer.clock仍然存在,只是UI不显示,代码控制不受影响。
2.3 确定观察位置:flyTo的目标是经纬度和高度
卫星雷达需要同时看到卫星轨道和地面覆盖区,相机高度选在几百公里才合适。太近看不到轨道,太远又看不清波束。用flyTo把相机放到目标区域上空:
viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(104.0, 35.0, 700000), orientation: { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(-90), roll: 0 }, duration: 3 });fromDegrees的三个参数分别是经度、纬度、高度,单位是米,700000就是700公里。pitch: -90表示垂直向下看,此时卫星从画面中斜穿过去时,地面的椭圆覆盖区会从视野边缘滑入,演示节奏比较舒服。如果雷达是侧视扫描,可以把pitch改成-60度,模拟卫星倾斜观察地面的感觉。flyTo是异步过渡,期间时钟照常走,不影响后续的轨道和波束生成。
3. 雷达波束建模:轨道、几何体和参数换算
3.1 卫星轨道先算出来:SampledPositionProperty与CallbackProperty分工
要让波束跟着卫星走,先得有卫星位置。Cesium里驱动位置常见有两条路:SampledPositionProperty和CallbackProperty。前者把一系列时间点上的坐标预先存好,Cesium在采样点之间做插值,适合轨道固定的场景;后者每帧调用一个函数动态算出当前位置,适合轨道会被用户实时修改的场景。卫星雷达示例里,轨道参数一般是固定的,用SampledPositionProperty更省心,代码也好查错。
const start = Cesium.JulianDate.fromIso8601('2024-01-01T00:00:00Z'); const stop = Cesium.JulianDate.fromIso8601('2024-01-01T01:40:00Z'); const totalSeconds = Cesium.JulianDate.secondsDifference(stop, start); const orbit = new Cesium.SampledPositionProperty(); for (let i = 0; i <= 360; i++) { const t = i / 360; const time = Cesium.JulianDate.addSeconds(start, totalSeconds * t, new Cesium.JulianDate()); const phase = t * 2 * Math.PI; const inclination = Cesium.Math.toRadians(50); const lat = Math.asin(Math.sin(inclination) * Math.sin(phase)) * 180 / Math.PI; const lon = 104 + Math.atan2(Math.cos(inclination) * Math.sin(phase), Math.cos(phase)) * 180 / Math.PI; orbit.addSample(time, Cesium.Cartesian3.fromDegrees(lon, lat, 500000)); } const satellite = viewer.entities.add({ position: orbit, point: { pixelSize: 10, color: Cesium.Color.YELLOW }, path: { resolution: 200, material: Cesium.Color.WHITE.withAlpha(0.3), width: 1 } });这段代码用360个点近似一圈轨道,周期1小时40分钟,接近常见低轨卫星。inclination是轨道倾角,这里取50度;phi由球面三角近似算出经纬度,满足演示需求。addSample把时间和坐标绑定,Cesium会自动在相邻采样点之间插值。path里的resolution控制轨道线的平滑程度,和轨道采样无关,调大只会增加轨迹线顶点数。如果改用CallbackProperty,可以把上面的循环放到回调函数里,每个时间点临时计算一次,适合想拖拽改变轨道的交互场景。
3.2 雷达波束的几何表达:Ellipse、Polyline还是Cylinder
雷达波束在三维场景里本来是一个锥体,但Cesium没有直接提供"圆锥体"实体,常见做法是用下面几种方式近似:
| 表达方式 | 雷达场景用途 | 性能 | 可维护性 |
|---|---|---|---|
| Entity Ellipse | 贴地覆盖区 | 中 | 高 |
| Entity Polyline | 波束中心线 | 高 | 高 |
| Entity Cylinder | 体波束近似 | 中 | 中 |
| Primitive + 自定义Geometry | 精确锥形/批量目标 | 高 | 低 |
对前端开发和演示场景,最直观的是Ellipse加Polyline组合:椭圆表示雷达落在地上的探测范围,线表示卫星到照射区的波束方向。Cylinder也能看,但圆柱的上下半径一样,放在斜视雷达里容易穿帮。Primitive性能最好,能画真正的锥体,但要自己维护Geometry和材质,代码量明显增加。卫星雷达示例只有一个波束目标,Entity完全够用。
3.3 用CallbackProperty把波束绑到卫星当前位置
波束必须实时跟着卫星走。做法是把卫星当前坐标投影到地面,把这个地面点作为椭圆覆盖区的中心,同时画一条从卫星到地面点的Polyline。这里必须用CallbackProperty,因为时间每帧都在变,星下点也跟着变。
function getGroundPosition(time, result) { const satPos = satellite.position.getValue(time); if (!satPos) return undefined; const carto = Cesium.Cartographic.fromCartesian(satPos); carto.height = 0; return Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, 0, result); } viewer.entities.add({ position: new Cesium.CallbackProperty(getGroundPosition, false), ellipse: { semiMinorAxis: 120000, semiMajorAxis: 180000, material: new Cesium.ColorMaterialProperty( new Cesium.CallbackProperty(function (time) { return Cesium.Color.RED.withAlpha(0.3); }, false) ), outline: true, outlineColor: Cesium.Color.RED } }); viewer.entities.add({ polyline: { positions: new Cesium.CallbackProperty(function (time) { const from = satellite.position.getValue(time); const to = getGroundPosition(time); return from && to ? [from, to] : undefined; }, false), width: 2, material: Cesium.Color.RED } });CallbackProperty的第二个参数是isConstant,传false表示每帧都要调用。getGroundPosition把卫星笛卡尔坐标转成Cartographic,再把高度改成0,回到椭球表面,这一步就是星下点投影。椭圆半轴的单位是米,180km和120km只是一个起始值,具体换算看下一节。这里的material包了一层CallbackProperty,暂时返回固定透明度,是为了第4章做脉冲闪烁准备的。Polyline的positions回调返回数组,Cesium会把它解析成一条线段。
如果雷达是侧视而不是垂直照射,可以在Cartographic上手动加经度或纬度偏移。比如在carto.longitude上加0.5度,模拟波束偏离星下点。但侧视涉及大地方向换算,先让垂直波束跑通,再改偏移量会更容易定位问题。
3.4 斜距、入射角和椭圆半轴的换算
雷达参数往Entity属性上映射时,最容易混淆的是半轴和距离。常见做法是:椭圆长半轴约等于斜距乘以方位向波束宽度的正切值;短半轴约等于斜距乘以距离向波束宽度的正切值。垂直照射时不需要除入射角,斜视时再除以入射角的余弦。
const slantRange = 500000; // 斜距 500 km const incidence = Cesium.Math.toRadians(30); // 入射角 30 度 const beamAz = Cesium.Math.toRadians(2.0); // 方位向波束宽度 const beamEl = Cesium.Math.toRadians(1.0); // 距离向波束宽度 const semiMajor = slantRange * Math.tan(beamAz) / Math.cos(incidence); const semiMinor = slantRange * Math.tan(beamEl);把semiMajor和semiMinor直接填进ellipse的semiMajorAxis和semiMinorAxis即可。斜距500km对应的覆盖椭圆会非常大,如果画面被红色盖满,优先减小slantRange,而不是调透明度。这个换算没有考虑椭球曲率和波束扫描角,但作为三维开发原型足够定位参数范围。实际产品里还要根据轨道高度重新算入射角。
4. 让雷达动起来:时钟驱动、回波脉冲与性能坑
4.1 Clock设置:multiplier决定雷达转多快
Cesium默认时钟按真实时间走,卫星一圈90分钟,演示时没人愿意等。需要把轨道时间范围绑定到viewer.clock上,再放大multiplier。
viewer.clock.startTime = start; viewer.clock.stopTime = stop; viewer.clock.currentTime = start; viewer.clock.multiplier = 60.0; viewer.clock.shouldAnimate = true;multiplier是场景时间与真实时间的比率,60表示场景时间1秒等于真实1秒的60倍,一圈轨道大约90秒看完。如果设成3600,卫星飞得太快,波束看起来像整圈扫描而不是连续移动。这里有个高频坑:startTime和stopTime必须和上一节轨道采样的时间范围一致,否则SampledPositionProperty在时间范围外没有数值,卫星会凭空消失。另外currentTime要设置在startTime之后,不能拿stopTime当当前时间再往前推,Cesium内部对时间区间的处理会直接丢弃轨道数据。
4.2 回波脉冲:让椭圆透明度按频率闪烁
卫星雷达的回波不是常亮光,而是周期性闪烁。把场景时间换算成秒,用正弦函数控制透明度即可。这里只需要替换第3章ellipse的material部分,不需要新建实体。
const pulseInterval = 2.0; // 秒 function pulseAlpha(time) { const sec = Cesium.JulianDate.secondsDifference(time, start); const v = (Math.sin(sec * 2 * Math.PI / pulseInterval) + 1) / 2; return 0.05 + 0.45 * v; } // 把3.3的material部分替换为: material: new Cesium.ColorMaterialProperty( new Cesium.CallbackProperty(function (time) { return Cesium.Color.RED.withAlpha(pulseAlpha(time)); }, false) )pulseAlpha返回0.05到0.5之间的透明度,2秒一个周期,既有"心跳"感又不刺眼。ColorMaterialProperty的构造函数接受一个Property对象,所以可以直接把CallbackProperty塞进去;如果直接传Cesium.Color.RED,透明度会被固定。想要多颗雷达不同闪烁节奏,就给每个实体创建独立的pulseInterval闭包变量,互不干扰。
4.3 Entity和Primitive的区别:这个示例什么时候换Primitive
Entity是Primitive的封装,开发效率高,单个对象更新方便;Primitive更贴近渲染层,适合大量同构几何体批量绘制。卫星雷达场景通常只有一颗卫星和一个波束,Entity够用。如果要做雷达网,上百个椭圆同时闪烁,Entity每次材质更新都对应单独draw call,这时换成Primitive,把几何数据合并成一次绘制,FPS会明显改善。经验值:页面里动态Entity超过50个,先打开draw call统计,再决定要不要换Primitive;不是换了就一定快,Primitive的更新逻辑也要自己管理,反而更易出错。
4.4 三个高频性能坑:granularity、回调复用和Entity创建位置
雷达场景把性能问题暴露得很快,因为椭圆覆盖区很大,材质又是每帧更新。三个高频坑值得先讲:
- 椭圆
granularity被忽略。EllipseGeometry默认细分很细,小区块没事,覆盖到几百公里时顶点数会爆炸。显式设置granularity: Cesium.Math.toRadians(2),能减少顶点,代价是边缘有棱角。 - CallbackProperty函数里创建新对象。每帧都
new Cesium.Cartesian3()会带来内存压力和GC停顿。前面getGroundPosition里的result参数就是为此准备的,Cesium会把同一个result对象传进来,函数里尽量复用。 - 在CallbackProperty里调用
viewer.entities.add。这是错误做法,动态实体应该提前创建完,回调里只更新已有属性。否则每帧都add,场景实体数量疯狂增长,帧率直接崩掉。
正确姿势是先创建实体,再在onTick里更新已有属性:
const radarEntity = viewer.entities.add({ // 定义时就把所有图形字段写好 }); viewer.clock.onTick.addEventListener(function () { // 这里只改 radarEntity 的已存在属性 });onTick每帧触发一次,适合做一次性边界判断;但如果是持续变化的坐标和材质,直接用CallbackProperty更简洁。两者同时存在时,优先CallbackProperty,onTick留来做那些需要跨帧累积的逻辑,比如计数或状态切换。
5. 从Cesium示例到前端产品:3D Tiles、Entity/Primitive验证与调试技巧
5.1 让雷达扫过真实地形:Ion Token和3D Tiles
椭圆投影默认压在椭球表面,切换真实地形后会发现覆盖区没有跟着山体走。要叠加真实地形和城市建筑,需要先配置Cesium ion的token,再异步加载地形和3D Tiles。
Cesium.Ion.defaultAccessToken = '你的_ion_token'; async function addTerrainAndTileset() { const terrain = await Cesium.createWorldTerrainAsync(); viewer.terrainProvider = terrain; const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(1234567); viewer.scene.primitives.add(tileset); } addTerrainAndTileset();createWorldTerrainAsync返回Promise,fromIonAssetId换成你在Ion里创建的资产ID。地形加载后,getGroundPosition里的carto.height = 0就变成贴椭球面,雷达波束末端应该落到地形表面。简化处理可以调用viewer.scene.globe.getHeight(carto)查询当前地形高度,但这个查询有性能开销,放在每帧回调里时要做好节流。无论后续接vue还是react,Cesium都只认这个div容器;组件卸载时记得调用viewer.destroy()释放WebGL上下文,避免切换页面后显卡内存泄漏。
5.2 两分钟验证波束和性能:debugShowFramesPerSecond和自记FPS
产品化之前先验证两件事:雷达参数有没有对应上、帧率稳不稳。Cesium自带了简单的帧率显示开关:
viewer.scene.debugShowFramesPerSecond = true;打开后画面左上角会出现FPS和render time,能快速判断椭圆闪烁时是否掉帧。如果想看更平滑的曲线,可以用clock tick自己记录两帧间隔:
let lastTick = performance.now(); viewer.clock.onTick.addEventListener(function () { const now = performance.now(); const dt = now - lastTick; if (dt > 0) { console.log('fps:', Math.round(1000 / dt)); } lastTick = now; });这段代码会在控制台不断输出帧率,配合右上角的绘制统计能定位大部分性能问题。如果看到FPS周期性下跌,先检查是不是卫星转过某片区域时,地形或3D Tiles在大量加载;如果FPS一直低,则优先怀疑椭圆granularity和CallbackProperty里创建新对象的问题。打开requestRenderMode后再看FPS,如果明显上涨,说明场景大部分时间不需要重绘,问题就出在雷达回调每帧都强制渲染。这个技巧对HTML原型和前端集成都有效。
本文还有配套的精品资源,点击获取