
简介Cesium飞行漫游是一份基于Cesium开源三维地球引擎实现的虚拟漫游功能资源主要面向WebGIS开发者、3D可视化学习者以及需要在浏览器中构建交互式场景的工程师帮助解决相机飞行控制与路径交互等典型需求。资源包共6854个文件压缩包大小约58.26MB包含大量JS、HTML、CSS前端代码以及PNG、JPG、GIF等图片素材同时包含b3dm、gltf、glb等地形与模型数据文件还提供JSON、CZML、KML等格式的路径和标注示例整体结构较完整。目前已获得5851人浏览学习。内容覆盖Cesium核心API调用、相机flyTo飞行、飞行暂停与方向控制、路线保存加载与清空退出等关键逻辑并配有前端界面、事件监听及相关工具脚本通过该资源可快速掌握在三维地球中实现漫游飞行的完整流程也能从包内大量示例文件中学到3D瓦片数据组织与前端性能优化的实用思路。对于从事三维GIS项目开发或准备入门Cesium的开发者具有较强的参考价值。1. 从一条路径说起Cesium飞行漫游到底解决了什么上次给某个产业园做汇报演示客户提了个听起来很小的需求沿着园区主干道低空飞一圈把每个路口的建筑和绿化带看清楚。我原以为用 Cesium 自带的相机飞行功能随手就能做结果手动操作起来完全不是那么回事——速度不好控制、转向生硬、飞到一半视角歪了还得重来。后来我把这段“上帝视角巡游”拆成了一个独立工程也就是今天这个 Cesium 飞行漫游项目只要给一串路径点组件会自动生成一条平滑的飞行轨迹支持匀速/变速、暂停/继续、第一人称/俯瞰视角切换。适合做园区导览、工程进度汇报、城市级三维场景的沉浸式演示技术栈是 Vue 3 Vite Cesium本地跑起来就能用。2. 整体架构设计先搞清楚一条飞行链路由哪几块拼成2.1 目录结构一个主组件撑起一条完整飞行链路拆开项目之后第一感受是代码量不大但模块切得非常干净。核心逻辑不在页面里而是拆成了几个独立的工具文件页面只负责组装。目录结构大概是这样的src/ ├── views/ │ └── FlyTour/ │ └── index.vue # 漫游页面初始化Viewer、挂载控件、组织流程 ├── core/ │ ├── viewer.js # 创建Cesium Viewer的工厂函数 │ ├── pathManager.js # 路径管理采样、插值、生成飞行路径 │ ├── cameraPose.js # 相机姿态计算heading/pitch/roll │ └── clockControl.js # 时钟控制播放、暂停、变速、跳转 ├── config/ │ ├── tourPath.json # 漫游路径点数据改成自己的坐标即可 │ └── viewPreset.js # 视角预设第一人称、俯瞰、跟踪三种模式 └── utils/ └── angle.js # 角度归一化、弧度转换工具从职责上看pathManager管“往哪飞”cameraPose管“怎么看”clockControl管“什么时候看”。页面组件只做两件事启动时把这些模块串起来运行时接受用户的按钮事件。这个分层的好处是后面想替换 Cesium 的某一层逻辑比如从路径漫游改成跟随车辆不需要动整个页面。2.2 核心模块设计路径、姿态、时钟三件事互不干扰飞行漫游的本质是相机的位置和姿态随时间连续变化。位置描述了“我在哪”姿态描述了“我看哪”时间描述了“什么时候”。项目里没有把这三个维度的数据混在一起算而是拆成了三个模块交替工作路径模块负责把用户给的稀疏路径点可能只有几十个点加密成高密度的平滑轨迹同时计算每个轨迹点对应的速度相机姿态模块在路径点的基础上根据当前点和下一个点的方向关系算出相机应该朝哪个方向看时钟模块对外暴露 play / pause / setSpeed 等接口内部通过 Cesium 的 Clock 驱动数据流动。这种设计的优势在于可以单独替换某一块。比如我只想改成“沿着道路中心线贴地飞”那我只需要替换 pathManager 里的插值逻辑姿态和时钟完全不用碰。这也是我在这个项目里最认同的一点——没有把所有代码塞进一个 vue 文件里。2.3 路径数据约定把飞行的“输入格式”说清楚tourPath.json是整个漫游的数据源头。它定义了一组路径点每个点包含经纬度坐标和环境信息[ { name: 园区北门, lon: 120.123456, lat: 30.123456, height: 80, duration: 5 }, { name: 研发楼路口, lon: 120.124567, lat: 30.124567, height: 80, duration: 8 } ]字段含义很直观lon/lat/height决定这个点的三维位置height是相对地面的高度单位米duration是从上一个点飞到当前点需要的时间单位秒。这个字段是整个飞行节奏的控制开关——想让某个重要建筑前多停留一会儿就把那个点的 duration 调大。我实际改数据时有个习惯先用地图工具量好每个关键路口的经纬度粗采一遍跑起来看效果再针对转弯生硬的地方加密路径点。后面要讲的插值算法就是让这些稀疏点之间也能走出顺滑曲线的关键。3. 核心代码拆解相机动起来之前先做对三件事3.1 初始化 Viewer别让默认控件拖慢帧率飞行漫游对帧率非常敏感卡顿直接导致眩晕。初始化时把不需要的默认控件关掉必要的地图控件保留// core/viewer.js import * as Cesium from cesium; export function createViewer(container) { const viewer new Cesium.Viewer(container, { animation: false, // 关闭动画控件我们自己控制时钟 timeline: false, // 关闭时间轴用自定义按钮 baseLayerPicker: false, // 关闭底图选择 geocoder: false, // 关闭搜索框 homeButton: false, // 关闭Home按钮防止误触离开漫游 sceneModePicker: false, // 锁定在3D模式漫游不切2D navigationHelpButton: false, fullscreenButton: false, // 保留信息框点击模型时能有弹窗展示属性 infoBox: true }); // 关键关闭碰撞检测避免相机被地形挡住 viewer.scene.screenSpaceCameraController.enableCollisionDetection false; return viewer; }这些配置项对应的是界面上各种默认控件。做飞行漫游时animation和timeline是默认打开最占性能的两个组件尤其是时间轴在路径较长时每一帧都在渲染刻度。关掉之后我们在页面上自己放播放/暂停按钮通过后面对 clockControl 的封装来控制。另一个重要的配置是enableCollisionDetection。如果不关相机在低空飞行时会被地形和建筑物模型顶住表现出“爬坡”或者直接穿模在园区这种楼栋密集的场景里几乎没法用。这个开关是我跑了一天后才发现的属于那种不影响报错但严重影响体验的配置项。3.2 路径插值用 CatmullRomSpline 让转弯不再“顿挫”路径点的原始数据通常是几十个点直接串成线飞的话转角处会产生明显的顿挫感。这里需要做光滑插值Cesium 自带的CatmullRomSpline就能解决不需要引入额外库// core/pathManager.js import * as Cesium from cesium; export function buildSmoothPath(points, tension 0.5) { // 把json里的经纬度高程转成Cartesian3坐标 const positions points.map(p { return Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height); }); // 用CatmullRom样条生成平滑曲线 const spline new Cesium.CatmullRomSpline({ times: points.map((_, i) i), // 时间参数从0递增 points: positions, tension: tension // 张力系数控制弯曲程度 }); // 按固定步长采样得到密集路径点 const samples []; const sampleCount 500; for (let i 0; i sampleCount; i) { const t (i / (sampleCount - 1)) * (points.length - 1); samples.push(spline.evaluate(t)); } return samples; }这里有两个关键参数。一个是tension范围 0 到 1值越小曲线越松弛转弯半径越大值越大曲线越贴近原始折线但我试过超过 0.7 后会在转角处出现轻微回弹。另一个是sampleCount控制采样密度500 对大多数园区路径够用路径特别长时可以调高到 1000但代价是后面姿态计算的循环时间变长。要注意的是times这里用了离散的整数索引而不是真实的时间戳因为这里只关心曲线的形状。真实的飞行速度控制是在下一步完成的采样点和时间一一对应之后再对每个点分配到达时间。3.3 相机姿态朝向切线俯视地面还是看着远方有了平滑路径点接下来要让相机沿着这些点飞行并且始终保持“看得到前面的路”。做法是每帧取当前点和下一个点计算两点之间的方向向量再把这个方向转成 Cesium 相机需要的 heading / pitch / roll// core/cameraPose.js import * as Cesium from cesium; import { normalizeAngle } from ../utils/angle.js; export function computeCameraOrientation(from, to, pitchOffsetDeg -12) { // 1. 计算行进方向向量 const direction Cesium.Cartesian3.subtract(to, from, new Cesium.Cartesian3()); Cesium.Cartesian3.normalize(direction, direction); // 2. 由方向向量求 heading航向角 const heading Math.atan2(direction.y, direction.x); // 3. pitch默认稍微向下俯视能看到地面细节 const pitch Cesium.Math.toRadians(pitchOffsetDeg); return { heading: normalizeAngle(heading), pitch: pitch, roll: 0 }; }解释一下这段的几何逻辑direction是当前点到下个点的切线方向atan2求出来的 angle 是这条切线与 X 轴的夹角对应到 Cesium 里就是相机的 heading。pitch 设置为负的 12 度表示相机略微向下看低空飞行时既能看清道路又不至于全是天空。normalizeAngle是我在工具类里写的一个角度归一化函数因为atan2返回的是(-PI, PI]区间而 Cesium 的 heading 习惯用 0 到 2PI 表示如果不做归一化相机会在某个角度跳变时突然旋转一整圈——这个问题后面避坑章节会详细讲。roll 这里固定为 0不做侧倾。只有在模拟无人机转弯时才需要让 roll 跟着转向倾斜做园区漫游保持水平视角更舒适。3.4 用 Clock 驱动漫游暂停、加速、回放全在这一行飞行漫游的驱动方式我在项目里换了两种思路一是用requestAnimationFrame手动更新相机位置二是用 Cesium 的Clock自动驱动。前者的好处是完全可控但每帧都要自己做时间累加、速度计算、位置插值代码容易越写越乱这个项目采用的是后者用SampledPositionProperty把路径点喂给时钟Cesium 自身会做时间和位置的匹配// core/clockControl.js import * as Cesium from cesium; export function createFlightClock(points) { const start Cesium.JulianDate.now(); const clock new Cesium.Clock({ startTime: start, currentTime: start, stopTime: Cesium.JulianDate.addSeconds(start, points.duration, new Cesium.JulianDate()), multiplier: 1.0, shouldAnimate: true }); // 用采样位置属性记录路径点在不同时间对应的位置 const positionProperty new Cesium.SampledPositionProperty(); const totalDuration points.reduce((sum, p) sum p.duration, 0); let elapsed 0; points.forEach(p { const time Cesium.JulianDate.addSeconds(start, elapsed, new Cesium.JulianDate()); positionProperty.addSample(time, Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height)); elapsed p.duration; }); return { clock, positionProperty }; }这段代码的巧妙之处在于把时间轴和路径点绑成一个表每个路径点对应一个时刻Cesium 会按当前时钟时间插值出相机的位置。调用方只需要一个clock.tick()循环不断把当前时刻的坐标取出来套上上一节算好的相机朝向传给viewer.camera.setView即可。multiplier是飞行速度的总开关1.0 表示按真实时间2.0 就是两倍速。暂停就是clock.shouldAnimate false回放就是multiplier -1.0。这些接口全部暴露在页面按钮上比自己在 rAF 里维护一个时间变量可靠得多。4. 把项目跑起来从依赖安装到换自己的数据4.1 三步启动安装依赖、起服务、看效果这个项目基于 Vite 构建依赖安装过程很常规。先克隆或解压工程然后执行以下命令# 安装依赖 npm install # 启动开发服务器 npm run dev启动后浏览器打开本地地址你会看到一个三维场景相机已经自动开始沿着默认路径飞行。页面左下角有四个控制按钮播放、暂停、加速、减速。默认路径是某园区的一条环线大约 60 秒飞完一圈。这里要单独提一下 Cesium 的静态资源问题。Cesium 的Assets目录包括天空盒、地形纹理、widgets.css需要在 Vite 里正确处理否则会出现“地图黑屏但 UI 正常”的玄学问题。这个项目的vite.config.js里已经配好了// vite.config.js import { defineConfig } from vite; import vue from vitejs/plugin-vue; import cesium from vite-plugin-cesium; export default defineConfig({ plugins: [ vue(), cesium() // 自动处理Cesium静态资源与模块导入 ] });vite-plugin-cesium会自动把 Cesium 的静态资源拷贝到构建目录并修正CESIUM_BASE_URL指向。如果你在自己项目里集成时没有这一步100% 会遇到运行时资源 404 的问题。4.2 改造路径点用自己的业务场景替换默认数据跑通默认工程后第一件必做的事是把自己的路径填进去。打开src/config/tourPath.json把里面的坐标改成你想要漫游的区域。我一般这样操作打开地图标点工具按顺序采集关键路口/建筑物的经纬度先采稀疏点每个转弯处一个点放到 json 里跑一遍观察转弯半径是否合适不合适就加密采样或者调整 tension给每个点设置合适的 duration控制飞行节奏。[ { name: 起点, lon: 121.473701, lat: 31.230416, height: 100, duration: 3 }, { name: 转弯1, lon: 121.474701, lat: 31.230416, height: 100, duration: 12 }, { name: 标志建筑, lon: 121.475701, lat: 31.229416, height: 80, duration: 20 } ]注意 height 字段我建议先统一给一个固定值比如 80 或 100 米跑通后再根据场景逐个调整。如果某个地方要展示建筑的顶部细节就把高度降到 40 米需要看到全景就抬到 200 米。每个点的 duration 尽量控制在 3 到 30 秒之间太短会显得突兀太长用户会失去耐心。4.3 调整飞行速度与视角预设飞行漫游给人最直观的舒适感来自于速度和视角的匹配。项目里把这些预设参数集中在viewPreset.js中预设模式pitch 俯角height 范围multiplier 倍速适用场景第一人称-12°30-80m1.0道路穿行、园区导览俯瞰-45°200-500m1.5区域鸟瞰、汇报开场跟踪-5°50-120m0.8跟随移动目标、工程巡检4.4 接入真实数据地形、影像、模型默认工程使用 Cesium 的在线地形和影像服务跑起来之后如果你需要完全离线或者接入自己的业务数据在viewer.js里替换即可。比如用自己的影像服务const imageryProvider new Cesium.ArcGisMapServerImageryProvider({ url: 你的影像服务地址 }); // 或者用自己的地形服务 const terrainProvider await Cesium.createWorldTerrainAsync(); viewer.terrainProvider terrainProvider;模型图层方面可以把gltf/glb格式的建筑模型直接拖到场景里用viewer.entities.add添加。需要注意模型坐标系要和路径点一致否则会出现相机飞在模型中间的尴尬情况。5. 飞行漫游常见问题排查从相机乱转到卡成幻灯片5.1 现象飞行过程中相机突然旋转一圈画面剧烈抖动表现直线飞行时一切正常一到某个转弯点相机像抽风一样猛地转一整圈然后画面恢复正常。原因atan2返回的 heading 范围是(-PI, PI]当方向向量从第一象限转到第二象限时角度会从接近 180 度突然跳到接近 -180 度。如果直接用这个原始值设置相机视角Cesium 会认为你要在极短时间把相机从 180 度转到 -180 度于是来了一次“快速回旋”。解决对 heading 做归一化处理把负角度转换成对应的正角度-PI变成PI并且保证相邻两帧的角度差不超过 180 度。我在utils/angle.js里写了normalizeAngle函数核心逻辑就是把计算结果映射到[0, 2PI)区间。从那以后每次做相机姿态计算我都会把角度归一化当成默认必须执行的步骤。5.2 现象暂停后再继续相机瞬移回暂停前的一个点表现漫游中途点击暂停画面停在半空点击继续后相机没有从当前位置接着走而是先闪回几秒前的位置再继续飞。原因暂停的操作是把clock.shouldAnimate设为 false那一刻clock.currentTime被冻结了。继续播放时如果直接把shouldAnimate恢复为 trueCesium 会从当前时间继续走。但如果你在暂停期间对 path 数据做过修改或者恢复时没有正确处理currentTime相机的采样位置就会重新进行插值看起来像“跳回去了”。解决暂停时用clock.currentTime记录备份恢复时先把currentTime重置为备份值再播放。具体代码如下function resume() { clock.currentTime pauseTime; // 恢复到暂停时刻 clock.shouldAnimate true; }这个问题最坑的地方在于它不是必现的只在采样点时间间隔较大时才出现。排查时就盯着 clock 的三兄弟——startTime、currentTime、stopTime大部分时钟相关的问题都出在这三个值不一致上。5.3 现象飞行卡顿帧率直线下降特别是转弯时表现直道飞行时帧数还行一进入转弯区域就明显掉帧严重时画面像幻灯片。原因第一转弯区间的路径点采样更密集每帧都要对多个点做姿态计算第二也是最容易被忽略的——setView每次传入高度值时Cesium 会触发地形和场景的重新加载频繁调用导致性能开销急剧上升。解决一个实用的优化是减少setView调用次数改为每两帧或每三帧设置一次相机。对于高速漫游而言30fps 的更新频率完全够用但性能开销直接减半。另一个优化是提前把一整段路径的 heading 计算好缓存起来不要在每帧循环里反复atan2const orientationCache samples.map((pos, i) { const next samples[Math.min(i 1, samples.length - 1)]; return computeCameraOrientation(pos, next); });预先算好这是这类场景最常见的优化手段飞行漫游的路径是固定的姿态也应该是确定性的完全不值得每帧重新算。5.4 现象相机穿模跑到建筑物或地形内部去了表现低空飞行时画面突然变成一片灰白或者黑白交错相机视角直接钻进了楼体模型或者地形里面。原因Cesium 默认开启了地形和模型碰撞检测但enableCollisionDetection配置没关的话相机和地表、建筑模型会产生碰撞反馈表现为画面抖动或穿模。解决在初始化时设置viewer.scene.screenSpaceCameraController.enableCollisionDetection false;这个操作在自动漫游场景里是必须的。做交互式浏览时可以保留碰撞检测但自动飞行路径是设计好的不关掉的话任何微小的路径偏差都会引起碰撞反馈。5.5 现象路径写了却看不到飞行轨迹线表现相机正常飞但场景里看不到路径线领导要看轨迹还得靠口述。原因很多工程默认不把路径添加到场景中只在配置里存了路径点没有用PolylineGraphics可视化。解决加上一条实体轨迹viewer.entities.add({ polyline: { positions: samples, width: 3, material: Cesium.Color.fromCssColorString(#00D8FF) } });轨迹可视化对调试也很有用——你可以直观地看到插值出来的路径是否贴着道路走转弯半径是否合理。6. 二次开发扩展方案从固定漫游到可交互的飞行系统6.1 方案一让相机跟随一个运动模型不改姿态模块只改位置来源。把cameraPose.js里的位置参数从“路径采样点”替换成“车辆模型的实时坐标”再把模型用viewer.entities.add挂到场景里就能做成“跟车漫游”。关键点在于模型转向时要同步更新 heading// 模型方向跟随路径切线方向 model.orientation Cesium.Transforms.headingPitchRollQuaternion( position, new Cesium.HeadingPitchRoll(heading, 0, 0) );6.2 方案二录制一条路径并回放我的另一个改造思路是把“固定路径配置”换成“运行时录制”。具体做法是监听鼠标或触摸事件实时采样相机的位置和姿态存入数组漫游结束后把采样数据导出成 json再次运行时就交给pathManager播放。这样非技术用户也能生成飞行轨迹不需要手量经纬度。6.3 方案三漫游过程录制为视频汇报场景经常需要把动画嵌入 PPT。用浏览器录屏往往有帧率不稳的问题更合适的做法是“关键帧导出”——每隔若干帧导出当前画面数据并保存最后用 ffmpeg 合成视频。这个方案对时序要求较高直接复用 clock 机制保证每帧导出的画面都对应正确的飞行时刻。这几条扩展方向不需要改架构都是在现有三个模块上做替换。我自己的习惯是每次改完路径配置强制走一遍流程——本地跑起来、重点看转弯处平滑度、检查暂停恢复是否跳变、确认帧率稳定然后再交给测试。这套流程帮我拦下过不少问题。希望这次的工程拆解和排坑记录能帮你在自己的项目里少走几步弯路。本文还有配套的精品资源点击获取