简介:这份资源是面向具备一定GIS开发基础的三维Web开发者整理的Cesium自主漫游实战源码包,针对三维场景中物体自由移动这一常见需求,给出可运行的完整实现方案。资源包共3个文件,以html页面为主体承载Cesium场景与漫游逻辑,辅以inscode工程配置和gitignore忽略规则,整体约6KB,轻量易读,下载后可直接运行调试。核心思路围绕CallbackProperty逐帧驱动小车实体的位置与朝向,并结合键盘事件监听,用WASD或方向键实时更新运动状态,最终将漫游能力封装为独立class,便于在项目中复用与扩展。目前已有68人学习下载,适合想快速掌握Cesium交互控制、理解实体动态更新机制的开发者参考,也可作为三维GIS项目中漫游模块的起步模板,帮助减少从零搭建的时间成本。
1. 从一段能跑起来的 Cesium 自主漫游说起
很多人第一次做 Cesium 自主漫游,都是被一个很朴素的需求逼出来的:场景里有一条路线,想让相机自己沿着路线走一遍,中途还能拐弯、抬头看建筑、在某个点位停两秒。听起来简单,真动手才发现官方Camera.flyTo只能点对点,viewer.trackedEntity又绑死在实体上,镜头朝向完全不受控。于是「自主漫游」这四个字,本质上是在问一件事:怎么用代码接管相机的位置和朝向,让它按我给的轨迹和节奏走。
这篇笔记就围绕这个目标展开,从坐标系、采样、插值一路讲到可运行的源码结构,中间会给出能直接抄的代码块和参数表。适合两类人:一类是刚接触 Cesium、想快速跑通一条漫游路线的开发者;另一类是已经用过flyTo、但被朝向抖动和速度突变折磨过的熟手。下面所有代码都基于 Cesium 的公开 API,不依赖任何私有封装,你复制到自己的 HTML 里就能验证。
2. 自主漫游的底层逻辑:相机到底被谁控制
2.1 为什么 flyTo 和 trackedEntity 都不够用
先把两个最容易被误用的 API 说清楚。camera.flyTo的本质是一次性动画,它接收一个终点destination和一个orientation,内部用缓动函数把相机从当前位置插值过去。问题在于它只有起点和终点,中间路径是引擎自己算的,你没法在中途插入「拐弯」或者「减速」。而viewer.trackedEntity是让相机跟随一个实体,朝向由viewer.trackedEntity的viewFrom决定,本质上是把相机挂在了实体的局部坐标系上,你想让镜头独立于实体朝向做动作,基本做不到。
自主漫游要的是第三种模式:相机位置由一条我定义的轨迹决定,相机朝向由另一条我定义的目标点序列决定,两者可以不同步。这就意味着必须绕开flyTo,直接操作camera.position和camera.direction,或者用camera.setView每帧刷新。前者性能好但容易和 Cesium 内部的相机控制器打架,后者稳定但要注意刷新频率。
2.2 用 SampledPositionProperty 把路线变成时间函数
Cesium 里描述「随时间变化的位置」的标准工具是SampledPositionProperty。它的思路是:你给若干个带时间戳的Cartesian3关键点,它内部用插值算法生成任意时刻的位置。这正好对应漫游的需求——路线是空间上的折线,但漫游是时间上的推进。
// 创建采样位置属性,用于描述相机随时间变化的轨迹 const positionProperty = new Cesium.SampledPositionProperty(); // 关键点:时间 + 经纬高(弧度制经度、纬度,高度单位米) const waypoints = [ { time: 0, lon: 116.3900, lat: 39.9100, height: 500 }, { time: 10, lon: 116.3920, lat: 39.9110, height: 300 }, { time: 20, lon: 116.3950, lat: 39.9120, height: 200 }, { time: 30, lon: 116.3980, lat: 39.9130, height: 350 } ]; waypoints.forEach((wp) => { // JulianDate.addSeconds 把相对秒数转成 Cesium 的绝对时间 const t = Cesium.JulianDate.addSeconds( Cesium.JulianDate.now(), wp.time, new Cesium.JulianDate() ); const pos = Cesium.Cartesian3.fromDegrees(wp.lon, wp.lat, wp.height); positionProperty.addSample(t, pos); }); // 设置插值方式:位置用线性,避免过冲 positionProperty.setInterpolationOptions({ interpolationDegree: 1, interpolationAlgorithm: Cesium.LinearApproximation });这段代码的关键在setInterpolationOptions。默认插值是拉格朗日多项式,次数高的时候会在关键点之间产生「过冲」,相机可能突然甩出去再拉回来,视觉上非常难受。把interpolationDegree设成 1 就是线性插值,路径变成折线,速度均匀,适合大多数漫游场景。如果你想要平滑转弯,可以设成 2 或 3,但一定要配合足够密的关键点,否则过冲会更明显。
2.3 朝向控制:用 VelocityOrientationProperty 还是手动算
位置解决了,朝向是第二个坑。Cesium 提供了一个VelocityOrientationProperty,它会根据位置属性的速度方向自动生成朝向,也就是「相机始终看向前进方向」。用法很简单:
// 根据速度方向自动生成朝向,相机始终看向运动方向 const orientationProperty = new Cesium.VelocityOrientationProperty(positionProperty);但它有两个限制。第一,当速度接近零时方向会抖动甚至翻转,因为速度向量太小,数值不稳定。第二,它只让相机看向前进方向,你没法让相机在前进的同时侧头看旁边的建筑。如果你的漫游需要「边走边看特定目标」,就得手动构造朝向。
手动构造的思路是:每一帧拿到相机当前位置position,再拿到你想看的目标点target,用Cesium.Transforms或者直接算方向向量,然后调用camera.setView。下面是一个最小实现:
// 手动朝向:每帧让相机看向指定目标点 function lookAtTarget(viewer, position, target) { // 计算从相机指向目标的单位向量 const direction = Cesium.Cartesian3.normalize( Cesium.Cartesian3.subtract(target, position, new Cesium.Cartesian3()), new Cesium.Cartesian3() ); // up 用默认的世界坐标上方向,避免翻滚 const up = new Cesium.Cartesian3(0, 0, 1); viewer.camera.setView({ destination: position, orientation: { direction: direction, up: up } }); }这里up用(0,0,1)是地心坐标系下的「上」,在大多数俯视或平视场景够用。如果你要做第一人称贴地漫游,up需要根据当前位置的地表法线来算,否则相机会有轻微的倾斜感。这个细节后面避坑章节会展开。
3. 把源码跑起来:从 HTML 到可运行的漫游
3.1 最小可运行页面结构
Cesium 的引入方式有两种:CDN 和本地构建。做自主漫游调试时我建议用 CDN,省去构建配置,改完刷新就能看效果。下面是一个完整的 HTML 骨架,包含容器、脚本引入和初始化。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>自主漫游最小示例</title> <!-- Cesium 官方样式,必须引入,否则控件错位 --> <link href="https://cdn.example.com/cesium/Widgets/widgets.css" rel="stylesheet" /> <style> /* 容器必须有明确高度,否则地球不显示 */ html, body, #cesiumContainer { width: 100%; height: 100%; margin: 0; padding: 0; } </style> </head> <body> <div id="cesiumContainer"></div> <script src="https://cdn.example.com/cesium/Cesium.js"></script> <script src="./roam.js"></script> </body> </html>注意两个点:widgets.css不引入的话,时间轴、Home 按钮这些控件会叠在一起;容器高度不设的话,Cesium 默认 canvas 高度是 0,你会看到一个空白页,这是新手最常见的翻车点。
3.2 初始化 Viewer 时必须关掉的几个开关
Viewer的默认配置对漫游很不友好,时间轴、动画控件、地理搜索都会干扰视线,而且默认相机会被鼠标操作打断。下面是我常用的初始化配置:
const viewer = new Cesium.Viewer('cesiumContainer', { // 关闭不需要的控件,减少干扰 timeline: false, animation: false, geocoder: false, homeButton: false, sceneModePicker: false, baseLayerPicker: false, navigationHelpButton: false, // 关闭默认影像,换成纯色底图便于观察轨迹 imageryProvider: false, // 关闭地形,避免高度采样带来的额外异步 terrainProvider: undefined }); // 关闭鼠标操作对相机的接管,否则漫游会被拖动打断 viewer.scene.screenSpaceCameraController.enableRotate = false; viewer.scene.screenSpaceCameraController.enableTranslate = false; viewer.scene.screenSpaceCameraController.enableZoom = false; viewer.scene.screenSpaceCameraController.enableTilt = false; viewer.scene.screenSpaceCameraController.enableLook = false;把screenSpaceCameraController的几个开关全关掉,是自主漫游能稳定运行的前提。否则用户一拖鼠标,你的setView和 Cesium 内部控制器就会互相覆盖,表现为镜头疯狂抖动。调试阶段可以先不关,确认轨迹正确后再关掉。
3.3 用 preRender 驱动每一帧的相机更新
Cesium 提供了scene.preRender事件,它在每一帧渲染前触发,是更新相机的最佳时机。不要用setInterval,因为它的频率和渲染帧率不同步,会导致画面撕裂。
// 记录漫游开始时间 const startTime = Cesium.JulianDate.now(); viewer.scene.preRender.addEventListener(() => { // 计算从开始到现在经过的秒数 const elapsed = Cesium.JulianDate.secondsDifference( Cesium.JulianDate.now(), startTime ); // 超过总时长后停止更新,避免时间越界 if (elapsed > 30) return; // 当前绝对时间 const currentTime = Cesium.JulianDate.addSeconds( startTime, elapsed, new Cesium.JulianDate() ); // 从位置属性中取出当前时刻的位置 const position = positionProperty.getValue(currentTime); if (!position) return; // 目标点:这里简单用下一个关键点,实际可换成任意实体 const target = Cesium.Cartesian3.fromDegrees(116.3980, 39.9130, 0); lookAtTarget(viewer, position, target); });getValue在时间超出采样范围时会返回undefined,所以必须判空。elapsed > 30这个判断对应关键点里的最大时间 30 秒,实际项目里应该用positionProperty.getValue的返回值来动态判断,而不是写死。
3.4 参数表:影响漫游观感的五个量
| 参数 | 作用 | 常用取值 | 调大后的效果 |
|---|---|---|---|
| interpolationDegree | 位置插值次数 | 1 | 路径更平滑,但可能过冲 |
| interpolationAlgorithm | 插值算法 | LinearApproximation | 换成 Hermite 更平滑但更慢 |
| 关键点时间间隔 | 决定速度 | 5~10 秒 | 间隔越大速度越快 |
| 相机高度 | 决定视角远近 | 200~500 米 | 越高视野越广,细节越少 |
| preRender 内计算量 | 决定帧率 | 尽量只做取值 | 计算越多帧率越低 |
这张表里的五个量,基本决定了漫游看起来是「顺滑」还是「一顿一顿」。我一般先把关键点时间间隔设成 5 秒,跑一遍看速度,太快就加大间隔,太慢就减小。高度则根据场景尺度调,城市级漫游 300 米左右比较舒服。
4. 避坑与排查:自主漫游最常见的五个翻车现场
4.1 相机抖动:现象是画面高频小幅晃动
现象:漫游过程中镜头持续轻微抖动,像手持拍摄。原因通常是preRender里同时调用了setView和 Cesium 内部控制器,两者每帧互相覆盖。解决:确认screenSpaceCameraController的五个开关全部关闭;如果还抖,检查lookAtTarget里的up向量是否每帧变化,固定成(0,0,1)能消除大部分抖动。
4.2 速度突变:关键点之间突然加速或减速
现象:相机在某个关键点前后速度明显不一致。原因多半是插值算法用了默认的拉格朗日,次数高时在关键点附近产生过冲。解决:把interpolationDegree降到 1,或者改用HermiteApproximation并保证关键点时间间隔均匀。如果关键点本身疏密不均,速度必然不均,这是数学决定的,只能通过加密关键点来缓解。
4.3 朝向翻转:相机在速度接近零时突然掉头
现象:相机走到某个位置后突然 180 度翻转。原因是用VelocityOrientationProperty时速度向量接近零,方向数值不稳定。解决:要么保证关键点之间始终有足够速度,要么改用手动朝向,用固定的目标点来算方向,彻底绕开速度向量。
4.4 高度穿地:相机钻进地形或建筑内部
现象:漫游到某些位置时相机低于地表。原因是没有开启地形碰撞检测,或者关键点高度设得太低。解决:如果场景有地形,开启viewer.scene.globe.depthTestAgainstTerrain = true,并在设置位置时用Cesium.sampleTerrainMostDetailed采样地表高度,把相机高度设成地表高度加一个偏移量。没有地形时,手动保证关键点高度大于场景内最高建筑。
4.5 时间越界:漫游结束后相机卡住不动
现象:漫游跑完一遍后画面静止,再调用getValue返回undefined。原因是SampledPositionProperty只在采样时间范围内有值。解决:在preRender里判断elapsed是否超过最大时间,超过就移除监听或者把时间取模循环。取模循环能让漫游无限重复,适合做展示。
5. 进阶技巧:让漫游从「能跑」到「好看」
5.1 用 CatmullRom 样条做平滑转弯
线性插值的路径是折线,转弯处有明显的棱角。如果想让路径平滑,可以把关键点先转成CatmullRomSpline,再采样成密集点喂给SampledPositionProperty。这样既保留了平滑,又避免了高次插值的过冲。
// 把稀疏关键点转成 CatmullRom 样条,再重采样成密集点 const controlPoints = waypoints.map((wp) => Cesium.Cartesian3.fromDegrees(wp.lon, wp.lat, wp.height) ); const spline = new Cesium.CatmullRomSpline({ times: waypoints.map((wp) => wp.time), points: controlPoints }); // 以 0.5 秒为间隔重采样,得到密集的平滑轨迹 const smoothProperty = new Cesium.SampledPositionProperty(); for (let t = 0; t <= 30; t += 0.5) { const pos = spline.findPoint(t); const jd = Cesium.JulianDate.addSeconds( Cesium.JulianDate.now(), t, new Cesium.JulianDate() ); smoothProperty.addSample(jd, pos); }CatmullRomSpline的findPoint接收的是参数t,不是绝对时间,所以这里直接用相对秒数。重采样间隔 0.5 秒是个经验值,太密会增加内存,太疏会丢失平滑效果。
5.2 用插值让朝向也平滑过渡
手动朝向如果每帧直接setView,在目标点切换时会有跳变。解决办法是对朝向也做插值:把每个关键点对应的方向向量预先算好,用SampledProperty存起来,运行时按时间取值。这样朝向和位置一样是连续变化的,视觉上更自然。
5.3 验证漫游是否正确的三个检查点
第一,把positionProperty的采样点画成Polyline显示出来,确认轨迹和预期一致。第二,在preRender里打印elapsed和position,确认时间推进和位置变化同步。第三,把screenSpaceCameraController全部关掉后跑一遍,确认没有外部输入干扰。这三个检查点能覆盖九成的漫游问题。
我自己做这类漫游时,习惯先把关键点数量控制在 5 个以内跑通,确认相机能动、朝向对、时间不越界,再逐步加密关键点和加平滑。一上来就写几十个点,出了问题根本不知道是插值、朝向还是时间的问题。希望帮到你。
本文还有配套的精品资源,点击获取