简介一个基于Cesium与Vue.js的3D地球飞行模拟实现方案面向前端开发者、GIS爱好者及需要构建动态三维场景的工程技术人员解决飞机模型沿预定航线平滑移动与交互控制的问题。压缩包采用7z格式整体大小约18.07MB体积适中便于快速获取与部署。方案完整覆盖了Cesium中Model实体的飞机模型加载、基于经纬度坐标点定义航线、利用TimeIntervalCollection制作平滑动画、通过监听Vue状态实现飞机位置与速度的实时同步以及使用Polyline绘制可视化航线条等关键环节同时借助Vue组件化封装控制面板实现飞行速度调节和视角切换等交互操作。此外还探讨了利用LOD动态调整模型细节的性能优化思路整体代码思路清晰、易于扩展对学习三维地理空间应用开发具有较好的参考价值。目前已有3859人学习/下载。1. cesium vue 的飞机航线飞行这份 Demo 帮你省掉两周排雷最近配合某公司做可视化大屏遇到了一个很典型的诉求用 Cesium Vue 3 在网页里加载一架飞机 3D 模型让它按照预定义的航线平稳飞行同时视角要能跟随、时间轴要能控制。网上能找到的片段代码很多但真正拿过来能跑通的很少——要么模型原地旋转不前进要么到达终点后继续飞向太空要么相机晃得人头晕。这份资源就是为解决这条链路打包的从航点数据结构、模型加载、插值算法、朝向计算到视角控制都有完整实现。适合刚接触 Cesium 的 Vue 开发者也适合要交付大屏但时间紧张的从业者。先看链路再动手动手之前你已经赢了半个项目。2. 航点数据与模型加载先把地基打牢飞机能飞起来的前提是数据源和模型都靠谱。很多 Demo 跑不起来不是 Cesium 的问题而是航点数据格式设计得不合理或者模型加载方式压根不适合动态实体。2.1 航点 JSON 结构经纬度、高度、时间戳我拿到这份资源的第一件事是看它的航点 JSON 长什么样。好的航点结构应该同时包含空间信息经纬度、高度和时间信息相对时间戳这样才能驱动 Cesium 的时间轴做插值计算。{ routeName: 某城市-某港口航线, speed: 120, waypoints: [ { lng: 116.39, lat: 39.90, height: 500, t: 0 }, { lng: 116.40, lat: 39.92, height: 800, t: 12 }, { lng: 116.45, lat: 39.91, height: 1000, t: 28 }, { lng: 116.50, lat: 39.88, height: 600, t: 45 } ] }这里height的单位是米t是相对起飞时间的秒数。我一般会多放一个speed字段因为后续如果要做速度调整直接改这个字段就行不用重新计算每个航点的时间戳。实际工作中这份 JSON 通常由后端航线规划接口生成前端只负责解析。要注意的一个细节是t必须是递增的而且相邻航点的时间间隔要和真实飞行速度匹配。如果两个航点空间距离一样但t相差很大飞行速度就会忽快忽慢。等会儿在第 3 章我会说怎么按距离反推时间。2.2 模型加载为什么选 GLB数据准备好了接下来是加载飞机模型。Cesium 里常见有两种方式一种是直接给 Entity 挂model属性加载 glTF/GLB另一种是用Model.fromGltfAsync加载后自己管理。做航线飞行模拟我强烈建议用第一种因为 Entity 自带时间轴插值能力配合SampledPositionProperty可以少写大量代码。const planeEntity viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.39, 39.90, 500), orientation: Cesium.Transforms.headingPitchRollQuaternion( Cesium.Cartesian3.fromDegrees(116.39, 39.90, 500), new Cesium.HeadingPitchRoll( Cesium.Math.toRadians(90), Cesium.Math.toRadians(0), 0 ) ), model: { uri: /static/models/plane.glb, scale: 1.0, minimumPixelSize: 128 } }) viewer.zoomTo(planeEntity)参数说明uri指向模型文件的静态路径scale是模型缩放倍数如果模型单位有问题主要调这个值minimumPixelSize表示模型在屏幕上最小渲染像素尺寸用来防止飞机飞远了变成一个小点消失。这里orientation的初始值很关键第一个航点的 Heading 如果和航线起始方向不一致飞机起飞瞬间会先猛转一下头需要仔细调。2.3 初始姿态校正机头到底朝哪这是最容易翻车的环节。Cesium 的 Heading 以正北为 0 度顺时针递增单位是弧度。但很多从建模软件导出的 GLB机头并不是朝正北的有的朝 X 轴正方向有的朝 Z 轴负方向。我见过某同学加载了一架机头朝东的飞机模型明明航线是往北飞飞机却横着飘过去看起来就像侧滑。解法有两个一是在建模软件里把模型摆正再导出这是最干净的办法二是在代码里对初始 HPR 做偏移补偿。常见做法是先在 Cesium 里加载模型把视角拉近手动转动 heading 找到机头正对正北的方向记录这个偏移量。如果模型机头朝东那初始 heading 就要设置成 90 度对应的弧度值。3. 让飞机动起来航点插值、朝向计算与时间轴绑定飞机在地图上显示不算什么真正的难点是让它按照航点平滑飞行并且姿态随航向变化。这一章把动画链路的核心部分拆开讲。3.1 为什么选 SampledPositionProperty 而不是 CallbackPropertyCesium 里让实体动起来有两种主流做法CallbackProperty和SampledPositionProperty。前者每次渲染时执行回调函数适合位置由实时计算决定的场景后者是预先填充时间-位置采样点Cesium 在时间轴上自动插值适合预定义的航线飞行。这份资源用的是SampledPositionProperty我认为选得对。航线飞行本质上是离线规划好的路径用采样方式性能更好而且自带插值能力不用自己逐帧算位置。const startTime Cesium.JulianDate.now() const positionProperty new Cesium.SampledPositionProperty() // 遍历航点把每个航点的时间和位置绑定 for (let i 0; i waypoints.length; i) { const wp waypoints[i] const time Cesium.JulianDate.addSeconds(startTime, wp.t, new Cesium.JulianDate()) const position Cesium.Cartesian3.fromDegrees(wp.lng, wp.lat, wp.height) positionProperty.addSample(time, position) } // 设置插值算法 positionProperty.setInterpolationOptions({ interpolationDegree: 3, interpolationAlgorithm: Cesium.HermitePolynomialApproximation }) planeEntity.position positionProperty逻辑说明addSample按时间顺序填入位置样本之后 Cesium 在任意时间点上都能根据这些样本算出当前位置。setInterpolationOptions里的interpolationDegree: 3表示三次插值HermitePolynomialApproximation是 Cesium 里比较平滑的一种算法。插值算法怎么选如果航点很密比如几十米一个点用 Linear 线性插值就够了如果航点稀疏几公里一个点用三次 Hermite 更平滑。但 Hermite 在急弯处可能出现过冲overshoot飞机轨迹会甩出去一小段这个坑在第 5 章会专门讲怎么规避。3.2 按距离分配时间别让速度忽快忽慢很多 Demo 的航点t是手动填的飞行速度极其不均匀。常见做法是根据航段距离和目标速度反推时间保证整条航线的速度稳定。function assignTimeByDistance(waypoints, speed) { let accumulatedTime 0 waypoints[0].t 0 for (let i 1; i waypoints.length; i) { const prevPos Cesium.Cartesian3.fromDegrees( waypoints[i - 1].lng, waypoints[i - 1].lat, waypoints[i - 1].height ) const currPos Cesium.Cartesian3.fromDegrees( waypoints[i].lng, waypoints[i].lat, waypoints[i].height ) // 计算两个航点之间的三维空间距离单位米 const distance Cesium.Cartesian3.distance(prevPos, currPos) accumulatedTime distance / speed // speed 单位米/秒 waypoints[i].t accumulatedTime } }这里speed是巡航速度。我之前在某项目里直接把 speed 设为 200结果发现飞机慢得像蜗牛因为航段距离是几百公里200 米/秒要飞很久。后来把 speed 调到 250 才接近真实巡航速度。调参时优先改 speed不要一个点一个点去改t。3.3 朝向计算让机头顺着航线走位置解决了姿态是下一步。最简单粗暴的做法是给entity.orientation绑定VelocityOrientationProperty让 Cesium 根据速度方向自动计算朝向。但这种方式在转弯时会瞬间改变方向看起来像瞬移扭头。更可控的做法是自己算朝向角然后做成SampledProperty(Cesium.Quaternion)。function computeHeadingDegrees(prev, curr) { const prevCarto Cesium.Cartographic.fromDegrees(prev.lng, prev.lat, prev.height) const currCarto Cesium.Cartographic.fromDegrees(curr.lng, curr.lat, curr.height) let deltaLng currCarto.longitude - prevCarto.longitude let deltaLat currCarto.latitude - prevCarto.latitude // 用当前纬度修正经度在球面上的实际长度 let heading Math.atan2( deltaLng * Math.cos(currCarto.latitude), deltaLat ) // Cesium 的 heading 以正北为 0顺时针为正atan2 返回逆时针角取负后归一化 heading -heading if (heading 0) { heading Cesium.Math.TWO_PI } return Cesium.Math.toDegrees(heading) }逻辑说明这个函数输入两个航点返回从第一个点指向第二个点的方位角度数。核心是atan2里对经度差乘以cos(latitude)因为地球表面经度 1 度的实际距离随纬度变化必须按当前纬度修正否则高纬度地区算出来的朝向会偏。算完 heading 之后把它做成时间-四元数采样const orientationProperty new Cesium.SampledProperty(Cesium.Quaternion) for (let i 1; i waypoints.length; i) { const prev waypoints[i - 1] const curr waypoints[i] const time Cesium.JulianDate.addSeconds(startTime, curr.t, new Cesium.JulianDate()) const position Cesium.Cartesian3.fromDegrees(curr.lng, curr.lat, curr.height) // 根据高度差和水平距离计算俯仰角 const elevationAngle Math.atan2( curr.height - prev.height, Cesium.Cartesian3.distance( Cesium.Cartesian3.fromDegrees(prev.lng, prev.lat), Cesium.Cartesian3.fromDegrees(curr.lng, curr.lat) ) ) const hpr new Cesium.HeadingPitchRoll( Cesium.Math.toRadians(computeHeadingDegrees(prev, curr)), elevationAngle, 0 ) const quaternion Cesium.Transforms.headingPitchRollQuaternion(position, hpr) orientationProperty.addSample(time, quaternion) } planeEntity.orientation orientationProperty注意这里的pitch用的是高度差和水平距离算出来的反正切值飞机爬升时抬头、下降时低头姿态会非常自然。如果不需要俯仰效果直接把 pitch 设为 0 即可。这份资源里默认是带俯仰计算的我建议保留。4. 视角跟随自动追踪与手动控制两种方案飞机飞起来了但屏幕上的视角不跟过去等于白干。视角跟随是这类 Demo 里观感最直观的部分也是新手最容易卡住的地方。4.1 trackedEntity一行代码启用自动跟随Cesium 提供了内置的相机追踪能力最简单的方式是设置viewer.trackedEntity。viewer.trackedEntity planeEntity // 同时确保时间轴在走 viewer.clock.shouldAnimate true viewer.clock.multiplier 1设置之后相机会自动以飞机为参考系飞机转向相机也跟着转。但这里有一个前提必须先把viewer.clock的时间范围设置好否则时间轴不动飞机动不了trackedEntity 自然也没效果。viewer.clock.startTime startTime.clone() viewer.clock.stopTime Cesium.JulianDate.addSeconds( startTime, waypoints[waypoints.length - 1].t, new Cesium.JulianDate() ) viewer.clock.currentTime startTime.clone() viewer.clock.shouldAnimate true viewer.clock.multiplier 1参数说明startTime对应起飞时刻stopTime对应最后一个航点的时刻multiplier是播放倍速1 是正常速度0.25 是慢速回放。这块代码是时间轴控制的公共部分第 5 章的“切后台跳变”问题和它也有关联。4.2 手动跟随相机从“盯着看”变成“上帝视角”trackedEntity的缺点是镜头完全锁死用户没法自由环视。如果要做大屏展示我一般会做一版手动跟随相机始终在飞机后方偏上但用户可以用鼠标调整视角偏移量。const offset new Cesium.Cartesian3(150, 0, 50) // 后方 150 米上方 50 米 viewer.clock.onTick.addEventListener(() { const currentTime viewer.clock.currentTime const position planeEntity.position.getValue(currentTime) if (!position) return // 把偏移量从飞机局部坐标转换到世界坐标 const hpr new Cesium.HeadingPitchRoll( 0, Cesium.Math.toRadians(-20), // 向下俯视 20 度 0 ) const transform Cesium.Transforms.headingPitchRollToFixedFrame(position, hpr) const cameraPos Cesium.Matrix4.multiplyByPoint( transform, offset, new Cesium.Cartesian3() ) viewer.camera.lookAt(position, Cesium.Cartesian3.subtract( cameraPos, position, new Cesium.Cartesian3() )) })逻辑说明每帧从positionProperty取当前时间点的位置算出相机应该在的位置然后lookAt看向飞机。offset的坐标系是飞机自身坐标系的所以飞机转向时偏移量跟着转镜头不会跑偏。这套做法比trackedEntity灵活但每帧都要算一次矩阵性能开销稍高在低端机器上建议只在需要交互时开启。4.3 时间轴联动暂停、倍速与进度控制航线飞行最怕交付时甲方说“飞慢点我要看转弯”所以时间轴控制必须做成可调的。// 暂停 viewer.clock.shouldAnimate false // 倍速播放 viewer.clock.multiplier 0.5 // 跳到某个时间点 const targetTime Cesium.JulianDate.addSeconds(startTime, 30, new Cesium.JulianDate()) viewer.clock.currentTime targetTime倍速调整的边界要小心multiplier设得太大比如 10 倍以上插值算法可能来不及算飞机位置会出现跳动设得太小0.1 倍以下飞机会肉眼可见地一帧一帧移动观感很差。我一般控制在 0.25 到 4 倍之间。5. 避坑这份 Demo 里最常见的 5 个翻车现场代码能跑通和能上线是两回事。以下 5 个坑是我在复现这个 Demo 时真实踩过的每条都按“现象 → 原因 → 解决”写清楚。5.1 飞机悬停原地螺旋桨转但位置不动现象飞机加载出来了螺旋桨动画也在转但就是停在起飞点不走。原因Cesium 的clock.shouldAnimate默认是false时间轴压根没往前走。positionProperty是按时间插值的时间不动位置自然不动。解决确保shouldAnimate true且clock.startTime必须早于或等于首航点的时间。如果startTime晚于首航点时间Cesium 会在时间轴起点就跳到第一个航点的位置看起来像卡住。5.2 到达终点后飞机不停继续沿切线飞出屏幕现象最后一个航点过后飞机不减速不停止沿着航线末端方向直直飞走。原因SampledPositionProperty在最后一个采样点之后默认继续外推extrapolate尤其配合 Hermite 插值时外推轨迹会很离谱。解决在clock.onTick里加判断当前时间超过stopTime就强制停止动画并固定位置viewer.clock.onTick.addEventListener((clock) { if (Cesium.JulianDate.greaterThan(clock.currentTime, stopTime)) { clock.shouldAnimate false clock.currentTime stopTime.clone() // 把实体位置固定到最后一个航点 const lastPos Cesium.Cartesian3.fromDegrees( waypoints[waypoints.length - 1].lng, waypoints[waypoints.length - 1].lat, waypoints[waypoints.length - 1].height ) planeEntity.position lastPos } })这样处理之后飞机在终点稳稳停住不会尴尬地冲向地平线。5.3 急弯处姿态剧烈抖动现象两个航点之间的夹角很小接近锐角飞机转弯时姿态会发生剧烈抖动甚至短暂倒扣。原因如果用了VelocityOrientationProperty转弯时速度向量变化剧烈四元数计算出来的朝向会出现跳变。另一种情况是 Hermite 插值在急弯处产生过冲位置曲线甩出去朝向跟着乱跳。解决尽量不用VelocityOrientationProperty改用第 3 章的手动朝向采样同时在急弯前后加密航点让转向过程有足够的中间帧。我一般会在转弯处额外插一个中点把 120 度锐角拆成两个 60 度角抖动量会显著下降。5.4 飞机加载出来是一个点或者大到盖住整个城市现象模型要么小到看不清要么大得把建筑全遮住。原因GLB 模型在建模软件里使用的单位不一致。有的软件里飞机翼展是 30厘米但 Cesium 按 1 单位 1 米解释模型就缩成 0.3 米反过来如果建模单位是千米模型直接变成庞然大物。解决先检查模型自带的 scale。用代码里的model.scale参数微调或者预览时用minimumPixelSize兜底model: { uri: /static/models/plane.glb, scale: 3.0, // 根据实际模型尺寸调整 minimumPixelSize: 128 // 兜底确保模型在屏幕上可见 }如果scale调到 100 还是没变化那就是模型内部嵌套了缩放节点得在建模软件里重置变换后再导出。5.5 浏览器切到后台再回来飞机位置跳变现象电脑锁屏或切换标签页一段时间回到页面发现飞机已经飞出航线终点或者位置和预期不符。原因Cesium 的时钟基于系统时间页面挂起时浏览器限制了定时器恢复后currentTime直接从系统时间继续导致时间轴往前跳了一大截插值出来的位置就飞了。解决监听页面的visibilitychange事件切出时记录暂停时刻回来时把clock.currentTime手动回退document.addEventListener(visibilitychange, () { if (document.hidden) { // 记下当前播放时间 window.__pausedTime viewer.clock.currentTime.clone() } else { // 恢复时把时间轴拉回暂停位置 if (window.__pausedTime) { viewer.clock.currentTime Cesium.JulianDate.clone(window.__pausedTime) } } })这个坑在长时间挂机演示时特别容易踩提前做防御处理能省去不少现场尴尬。6. 验证方法把单机演示变成可交付的航线模拟6.1 慢速回放检查轨迹交付前我习惯把multiplier设为 0.25 慢速回放一整遍。这时重点看三个点一是每个转弯处姿态是否平滑二是爬升和下降时俯仰角是否自然三是整条航线高度是否贴合地形。再把航线的 polyline 叠加显示出来对比飞机实际轨迹和规划航线的偏差viewer.entities.add({ polyline: { positions: waypoints.map(wp Cesium.Cartesian3.fromDegrees(wp.lng, wp.lat, wp.height)), width: 2, material: Cesium.Color.CYAN.withAlpha(0.8) } })如果飞机偏离 polyline问题基本出在插值算法的过冲上把interpolationDegree降到 2 或者加密航点就能解决。6.2 进阶循环飞行与多航线扩展要让演示更完整可以做循环飞行——飞机到达终点后自动回到起点重新飞。实现思路很简单在onTick里判断到达终点后把currentTime重置为startTimeviewer.clock.onTick.addEventListener((clock) { if (Cesium.JulianDate.greaterThan(clock.currentTime, stopTime)) { clock.currentTime Cesium.JulianDate.clone(startTime) } })多航线扩展也不复杂把航点数据和实体封装成一个RouteFlier类每条航线实例化一个对象各自维护独立的startTime和positionProperty互不干扰。这份资源扔掉了硬编码直接套用这个结构就能扩展成多机编队。从那以后我每次拿到航线模拟类项目都强制先跑一遍慢速回放确认转弯姿态和时间轴控制都符合预期再交付给下游。这个习惯帮我挡掉过好几次现场演示事故希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?