资讯详情

Cesium飞行漫游核心机制:相机控制、路径插值、地形避让实战解析

📅 2026/10/17 6:12:08 | 华诺云谱 👁 阅读
Cesium飞行漫游核心机制:相机控制、路径插值、地形避让实战解析
简介Cesium飞行漫游是基于WebGL开源库Cesium构建的三维地球交互项目面向前端GIS开发者与三维可视化学习者解决在真实地形场景中实现相机平滑飞行、路线设定与交互浏览的问题。资源共6854个文件压缩包58.26MB包含3129个JavaScript逻辑文件、808个HTML页面结构、923个PNG图片及383个JPG纹理同时配有CSS、JSON、GLSL着色器以及b3dm、gltf、pnts等三维瓦片与模型数据目录完整、格式丰富。目前已有5849人学习浏览适合用来研究Cesium的Viewer与Camera控制机制。通过内置的飞行漫游实现可参考flyTo调用、飞行暂停与前后移动、路线点保存和加载、清空路线及退出飞行等完整交互流程尤其适合希望快速搭建三维漫游原型或深入理解相机动画原理的开发者入手后可直接运行调试并根据项目结构扩展自己的功能。1. 飞行漫游不是飞起来就完事Cesium 里相机动画才是核心很多人做 Cesium 飞行漫游以为就是viewer.clock一跑、相机跟着路径点走一遍就完事。实际做出来要么镜头穿地、要么转弯时画面甩得像坐过山车、要么用户手动拖一下地图漫游状态就乱了。Cesium 飞行漫游的本质不是“模型在飞”而是“相机在飞”——路径插值、朝向计算、时钟控制、地形避让这几件事必须协同好才能出来一条丝滑的巡检航线。这篇文章我会把飞行漫游从路径设计到相机控制再到踩坑排查按一套可复现的流程拆给你。适合刚接触 Cesium 的开发者也适合做过但没深究过飞行参数的熟手照着做就能在项目里落地一段不穿地、不眩晕、可控可停的飞行巡检动画。2. 让相机先动起来viewer.camera.flyTo 的局限与 lookAt 的正确用法2.1 flyTo 能做漫游吗能但只是一锤子买卖Cesium 里最常见的飞行函数是camera.flyTo。它接收一个目的地和相机姿态然后内部自动做插值和缓动两三秒把人从 A 点带到 B 点。这个函数用于“快速定位到某个位置”非常合适比如用户点击列表里的一栋楼视野飞过去看一眼。但如果你的目标是“沿着一条路径持续漫游途经十几个点并且停下来看每个点的细节”flyTo 根本不够用。首先flyTo 的destination和orientation是最终状态中间过程完全交给 Cesium 内置的缓动算法。你没法在中间某个时刻插入“降低飞行速度观察左侧建筑”这样的动作。其次flyTo 每次调用都会打断上一次飞行连续调用多个点的 flyTo 时动画是“跳变”的不是衔接的——上一个点还没看稳下一个点的动画就强制接管了。我做模拟项目X的厂区巡检时试过用一个循环依次调用 flyTo 到各个巡检点结果画面的表现是每隔 3 秒突然转头换个方向没有任何连续感。所以要说清楚flyTo 是“跳点飞行”不是“漫游飞行”。漫游飞行需要的是你手动控制相机每一帧的位置和朝向。2.2 lookAt 才是漫游的基础单元和 flyTo 不同camera.lookAt是“把相机放到一个相对于目标点的偏移位置并朝向该目标点”。它的核心参数是一个HeadingPitchRange对象viewer.camera.lookAt( Cesium.Cartesian3.fromDegrees(116.391, 39.907, 50), new Cesium.HeadingPitchRange( Cesium.Math.toRadians(0), // heading相机朝向0 为正北 Cesium.Math.toRadians(-30), // pitch俯仰角负值表示从上方往下看 200 // range相机与目标点的距离单位米 ) )逻辑说明lookAt的意思不是把相机“移”到目标点上而是以目标点为球心在你的HeadingPitchRange所定义的球面位置上放置相机并始终看向球心。所以它天然适合漫游中的一个“观察点”——比如巡检路线上的某个设备、某段大坝坝面、某块边坡。参数说明heading 是绕 Z 轴的方向角0 度指正北90 度正东pitch 是俯仰角正值表示抬头仰视负值表示俯视-90 度就是垂直往下拍range 是相机到观察点的距离单位米。这三个值合起来决定了一个“固定的观察姿势”。但注意lookAt一旦设置相机就被“锁定”了用户用鼠标拖拽会发现视角转不动。所以在漫游结束后或某个观察动作完成后要调viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY)把相机的变换矩阵重置掉否则后续交互会莫名其妙失灵。2.3 用 Camera.setView 做瞬间定位漫游前的底座在写真正的路径漫游之前还有一个基础动作要掌握camera.setView。它用于瞬间把相机摆到一个指定位置不做任何过渡动画。漫游路径的起点、中途的检查点、以及中途暂停后恢复漫游的位置都需要先用它来“断电重启式”摆正相机viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(116.391, 39.907, 300), orientation: { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(-45), roll: 0 } })逻辑说明setView的orientation里的 heading 是相机朝向的角度pitch 是俯仰roll 默认保持 0。它比 flyTo 更快、更可控多用于“瞬间切换到一个观察位置”的场景。漫游路径的第一帧通常用 setView 来定位而不是 flyTo因为 flyTo 的缓动会从“上一个位置”飞到起点如果你的上一个位置在十万八千里外这一下飞行动画会非常突兀。参数说明这里的destination是相机所在的世界坐标不是观察目标的坐标。你可以理解为“相机站在哪”而 orientation 是“镜头对着哪”。如果你把 destination 设到地下相机会因为地形遮挡看到一片黑这在实际项目里经常遇到——路径点设计时就必须保证所有相机的 destination 高于地表不只高一点点要考虑到地形起伏。3. 路径设计采样点、插值算法与地形贴合3.1 路径点的来源手写经纬度还是路线采集飞行漫游的路径点通常有三种来源。第一种是手工指定——你在 Cesium 的 3D 场景里点选一系列位置记录下经纬度和高度然后写成一个数组。第二种是从 GeoJSON 或 Shapefile 里读取一条已有的线比如一条道路中线、一条输电线路走廊。第三种是从模型的包围盒或关键设施点位自动生成巡检点。我一般会建议混合用先从业务数据里拿到兴趣点设备、杆塔、坝段然后用“偏离中线 固定镜头朝向”的方式生成每个兴趣点对应的相机位置。举个例子一段大坝巡检坝轴线是一串坐标你希望相机从坝前 50 米上空飞过斜视坝面。那你需要在每个坝段中心点的经纬度基础上沿垂直于坝轴线的方向偏移 50 米高度抬升 80 米pitch 朝下看。手写路径点最忌讳的是“点与点之间距离太大”。如果两个采样点相隔几公里插值出来的路径可能直穿山体或建筑物。所以设计路径的关键不是“点要多”而是“点密 高度合理”。在转弯较多、地形起伏剧烈的区域采样距离应控制在 50~100 米平地直行路段可以放到 300~500 米。3.2 Catmull-Rom 插值为什么不是线性插值有了路径点之后要生成一个平滑的飞行轨迹。你用线性插值在两点之间做匀速直线运动会看到明显的折角转弯时画面突然甩一下。Cesium 自带的CatmullRomSpline这个插值类就是干这个的。它保证曲线通过所有控制点同时在每个点处的一阶导数连续也就是说“速度不会突变”。const positions [ Cesium.Cartesian3.fromDegrees(116.391, 39.907, 200), Cesium.Cartesian3.fromDegrees(116.393, 39.908, 220), Cesium.Cartesian3.fromDegrees(116.396, 39.909, 260), Cesium.Cartesian3.fromDegrees(116.398, 39.907, 240) ] const spline new Cesium.CatmullRomSpline({ times: [0, 1, 2, 3], points: positions })逻辑说明times数组必须和points一一对应表示每个点对应的“时间刻度”。你可以不写 0、1、2、3 这种等差的时序而是按实际飞行距离给每个点分配不同的时间值。比如直行段距离长、时间跨度和转弯段距离短、时间跨度短这样速度就能自然调节。参数说明CatmullRomSpline的firstTangent和lastTangent是可选的分别用于控制起点和终点的切线方向。如果你希望起飞时是垂直向上的就在起点设一个朝上的切向量希望落地前是平缓下滑的就在终点设一个水平切向量。我通常会在首尾各设置一个 tangent否则默认算法会让起点和终点处出现一段奇怪的加速曲线。3.3 地形采样路径必须贴着地形走设计路径时最容易忽略的是地形高度变化。你用手工点的方式在二维地图上画了一条路线高度全部填 200 米但真实地形可能中间有一个 300 米高的山包相机就会从山里穿过去画面变成一片土黄色或直接闪到地下。解决方法是做“地形采样”。利用 Cesium 的sampleTerrainMostDetailed把每个路径点的高度替换成真实地形高度再叠加一个你期望的离地高度偏移量const positions [ Cesium.Cartesian3.fromDegrees(116.391, 39.907), Cesium.Cartesian3.fromDegrees(116.396, 39.909), Cesium.Cartesian3.fromDegrees(116.398, 39.907) ] // 先把经纬度坐标转为 Cartographic const cartographics positions.map(p { const carto Cesium.Cartographic.fromCartesian(p) return Cesium.Cartographic.fromDegrees( Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude) ) }) Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, cartographics) .then((updatedCartographics) { const adjusted updatedCartographics.map(c { return Cesium.Cartesian3.fromDegrees( Cesium.Math.toDegrees(c.longitude), Cesium.Math.toDegrees(c.latitude), c.height 150 // 离地 150 米避免地形遮挡 ) }) // 用 adjusted 重新创建 CatmullRomSpline })逻辑说明sampleTerrainMostDetailed异步获取地形高度回调里拿到的cartographic.height是真实海拔。对它加上一个安全偏移量之后路径点才可用于飞行插值。这个偏移量不要统一写死要在起伏大的区域用小偏移30~50 米以贴近地形看细节在平坦区域用大偏移100~200 米让视野开阔。参数说明sampleTerrainMostDetailed对请求次数有限制一次传几百个点没问题但不要频繁调用。如果你的路径点超过 500 个建议先做抽稀或分块采样。另外terrainProvider必须在创建 viewer 时就指定好地形数据默认的 EllipsoidTerrainProvider 是光滑椭球面没有真实地形高度采样结果会出现“全部是 0”的情况导致路径点落入地下。3.4 路径可视化调试时把路径画出来路径设计完以后一定要先把它渲染出来看一眼再跑动画。这一步能帮你抓住 80% 的穿地和突刺问题。我在每次做飞行漫游的第一步就是先在场景里画一条 polyline然后沿着 polyline 撒几个 billboard 作为途经点标记const polyline viewer.entities.add({ polyline: { positions: adjustedPositions, width: 4, material: Cesium.Color.YELLOW.withAlpha(0.8), clampToGround: false } }) adjustedPositions.forEach((p, index) { viewer.entities.add({ position: p, point: { pixelSize: 8, color: Cesium.Color.CYAN } }) })逻辑说明clampToGround: false意味着线条保持你设定的三维坐标不贴地。这样你能直观看到路径在立体空间里的形状检查有没有哪一段高度异常。把路径画出来之后你还可以整体抬高路径高度——不用重新生成每个点直接用矩阵变换把路径整体上移。参数说明clampToGround如果设为 truepolyline 会被压到地形上完全失去你设定的高度信息。调试飞行路径时建议始终保持 false。pixelSize是 billboard 点的大小设 8~12 像素在远距离下看得很清楚。用 CYAN 和 YELLOW 两种颜色区分路径线和采样点比单色更直观。提示调试路径时把viewer.clock.shouldAnimate设为 false手动拖动时间轴逐帧查看相机位置的变化。这比直接播放动画更容易定位问题点。4. 相机动画逐帧驱动从 Spline 到 Camera 的映射逻辑4.1 用 Clock.onTick 驱动每一帧都重新计算相机姿态路径有了接下来就是逐帧驱动相机沿着路径移动。Cesium 的时钟系统viewer.clock提供了onTick事件每一帧渲染前都会触发。在这个回调里你需要根据当前时钟时间从样条曲线上取相机位置再根据当前和下一帧的位置计算相机朝向最后把结果赋给camera。const startTime Cesium.JulianDate.now() const duration 60 // 秒 const stopTime Cesium.JulianDate.addSeconds(startTime, duration, new Cesium.JulianDate()) viewer.clock.startTime startTime.clone() viewer.clock.stopTime stopTime.clone() viewer.clock.currentTime startTime.clone() viewer.clock.clockRange Cesium.ClockRange.CLAMPED viewer.clock.canAnimate true viewer.clock.shouldAnimate true viewer.clock.onTick.addEventListener(function (clock) { const elapsed Cesium.JulianDate.secondsDifference(clock.currentTime, startTime) const t elapsed / duration // 归一化时间 if (t 0 || t 1) return // 从样条曲线上取位置 const position spline.evaluate(t) // 计算下一帧的位置用于确定朝向 const tNext Math.min(t 0.005, 1) const positionNext spline.evaluate(tNext) // 计算相机朝向看前方路径点而不是水平向前 const direction Cesium.Cartesian3.subtract(positionNext, position, new Cesium.Cartesian3()) Cesium.Cartesian3.normalize(direction, direction) // 设置相机位置和朝向 viewer.camera.setView({ destination: position, orientation: { direction: direction, up: Cesium.Cartesian3.clone(Cesium.Cartesian3.UNIT_Z) } }) })逻辑说明核心逻辑是把归一化时间 t 映射到样条曲线的evaluate方法上得到相机在三维空间中的位置。setView在这里的作用是“硬设定”相机到指定位置和方向不做插值动画。因为每帧都在设帧率 60 的情况下视觉上就是连续动画。参数说明tNext的0.005是经验值它决定了朝向计算的“预视距离”。太大会导致转弯时朝向滞后太小会导致相机朝向抖动。实际上这个值应该和飞行速度联动速度越快预视距离越大。如果你需要精确控制可以在路径设计中记录每个时间点的速度然后动态计算tNext。4.2 朝向计算direction 和 up 到底怎么定上面代码里相机朝向用的是direction positionNext - position即“看向路径前方”。这在飞行场景中是合理的——相机的视线方向与运动方向一致就像坐在车头往前看。但巡检场景里我们往往不是“往前看”而是“往侧下方看”——比如查看大坝坝面或边坡时视线应该偏向路径的一侧这就是“动态朝向”的问题。常见做法是先计算路径的切线方向作为 forward 向量再用叉积求出“右向”和“上向”然后用三角函数让视线方向在 forward 和 right 之间做偏移。下面这段代码实现了一个向路径右侧偏 30 度的观察方式function getCameraOrientation(position, positionNext, pitchOffset) { const forward Cesium.Cartesian3.subtract(positionNext, position, new Cesium.Cartesian3()) Cesium.Cartesian3.normalize(forward, forward) const up Cesium.Cartesian3.clone(Cesium.Cartesian3.UNIT_Z) const right Cesium.Cartesian3.cross(forward, up, new Cesium.Cartesian3()) Cesium.Cartesian3.normalize(right, right) // 视线从 forward 方向水平向右偏 30 度 const angle Cesium.Math.toRadians(30) const lookDir Cesium.Cartesian3.lerp(forward, right, Math.sin(angle), new Cesium.Cartesian3()) Cesium.Cartesian3.normalize(lookDir, lookDir) // 添加俯仰角向下看或平视 const finalDir Cesium.Cartesian3.lerp(lookDir, up, Math.sin(pitchOffset), new Cesium.Cartesian3()) Cesium.Cartesian3.normalize(finalDir, finalDir) return { direction: finalDir, up: Cesium.Cartesian3.clone(up) } }逻辑说明先算出路径的 forward 向量和 right 向量然后用lerp在 forward 和 right 之间做一个矢量插值。Math.sin(angle)作为插值系数角度为 0 时视线朝前角度 90 度时视线完全朝右。之后再用同样的方式在水平视线和竖直向上之间做俯仰插值。两个插值叠加后视线就变成了“向右偏 30 度 向下看 20 度”的斜向观察视角。参数说明pitchOffset是俯仰偏量传Math.toRadians(-20)表示向下看 20 度传正值则抬头。注意lerp得到的向量必须在最后做normalize否则相机朝向会带上非单位向量的问题导致画面畸变。4.3 速度控制匀速飞行 vs 关键点停留匀速飞行的实现上面已经给出了。但如果巡检要求在每个设施点停留 3 秒看一眼就要在路径里加入“停留段”。有两种常见做法。第一种是改 times 数组——让两个位置点对应同一个或非常接近的时间这样相机运动到该点后几乎不动// 路径点A - B - C在 B 点停留 3 秒 const times [0, 0.4, 0.4, 1] const points [ Cesium.Cartesian3.fromDegrees(116.391, 39.907, 200), Cesium.Cartesian3.fromDegrees(116.393, 39.908, 220), Cesium.Cartesian3.fromDegrees(116.393, 39.908, 220), Cesium.Cartesian3.fromDegrees(116.396, 39.909, 260) ]逻辑说明times 里0.4出现了两次对应的位置都是同一个点。Catmull-Rom 插值遇到重复时间点时在该处会形成一个“暂停”相机会在 B 点停住。但因为 Catmull-Rom 算法本身对重复时间点的处理会导致速度不连续暂停结束后会有一段加速过程。参数说明这种方法的缺点是暂停的“停”不够干脆会有微小的漂移。更稳妥的做法是拆分动画飞到 B 点后用viewer.clock.shouldAnimate false暂停时钟3 秒后再设回 true 并继续。我一般用后一种因为你可以利用暂停时间做更多动作比如切换巡检标注信息。4.4 停止与重置给漫游一个刹车飞行漫游的结束处理很容易被忽略。动画到终点后相机应该停在一个合理的观察位置上而不是悬在最后一个路径点的半空中继续晃。同时时钟要复位否则用户手动拖地图时相机会被 onTick 里的 setView 强制拉回。function stopFlight() { viewer.clock.shouldAnimate false viewer.clock.onTick.removeEventListener(onTickCallback) // 重置相机变换矩阵解除漫游锁定 viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY) // 把相机放在终点附近朝一个概括性视角看下去 const endCarto Cesium.Cartographic.fromCartesian(spline.evaluate(1)) viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees( Cesium.Math.toDegrees(endCarto.longitude), Cesium.Math.toDegrees(endCarto.latitude), endCarto.height 200 ), orientation: { heading: 0, pitch: Cesium.Math.toRadians(-30), roll: 0 } }) }逻辑说明removeEventListener必须传原来的回调引用所以在注册 onTick 时要把回调函数单独保存为变量。lookAtTransform(Matrix4.IDENTITY)是释放之前所有 lookAt 锁定的标准做法。参数说明复位后的setView不是为了继续飞而是为了把镜头放在一个舒服的总结视角。heading 设 0 表示正北方向pitch -30 度让视野往下倾斜 30 度看终点的全局情况。这个视角不要和飞行视角差异太大否则用户会感觉“突然闪了一下”。5. 漫游避坑指南地形穿模、相机抖动和交互冲突的三类实战问题5.1 相机穿地画面突然变成一片土黄色现象飞行过程中画面在某一段突然看不到场景而是显示地形的内部颜色或者直接变成黑色。原因最常见的就是路径点高度低于地形高程。也就是你编写路径点时没有做地形采样或者采样后的偏移量过小插值曲线在两点的中间段穿过了山脊。还有一个原因是CatmullRomSpline插值时端点切线方向朝下导致曲线在端点附近形成一个低垂的弧度即使端点本身高出地面中间段仍然触地。解决首先确保每个路径点都做了sampleTerrainMostDetailed采样。然后在每个点的高度上加一个安全冗余常规场景加 50~100 米地形起伏大的区域加 150~300 米。最后检查 spline 的firstTangent和lastTangent确保起点切线朝上或水平不要朝下。如果还穿就在可疑区域加密路径点加密后的 Catmull-Rom 曲线会更贴合你预设的高度走廊。5.2 转弯时相机抖动画面在拐角处出现高频震颤现象直线飞行正常一到转弯处画面就开始小幅度左右抖动像视频里加了震动特效。原因转弯时路径曲率变大tNext预视距离固定不变导致朝向计算出现“过冲”。具体来说在弯道处 spline 的切线方向变化很快你用固定的t 0.005取下一帧位置前后两帧的位置连线可能偏离真实切线方向很大换算成朝向角的变化会一帧大、一帧小视觉上就是抖动。解决把固定的tNext改为动态计算。方法是先根据当前飞行速度估算出每帧前进的欧氏距离然后设定一个固定的“视距”比如 30 米反推 t 的增量const speed 100 // 米/秒 const frameInterval 1 / 60 // 假设 60 帧 const stepDistance speed * frameInterval // 粗略估算当前速度下 t 每帧的增量 const tStep stepDistance / totalPathLength const tNext Math.min(t Math.max(tStep * 3, 0.01), 1)逻辑说明tStep乘以 3 是让视野范围超出实际行进距离给相机朝向一个“预视”的余量。在弯道处这个余量让朝向变化更平缓不会贴着切线左右摆。你可以把这几个参数打印出来对比抖动前后弯道处的取值变化。参数说明speed应使用 path 设计时的实际速度。如果你的动画时长和路径长度的比是 60 秒飞 6 公里那平均速度就是 100 米/秒。把 speed 和 totalPathLength 打印到控制台可以减少很多盲猜。5.3 用户拖拽后镜头被拉回漫游和手动交互的冲突现象飞行漫游过程中用户想拖动鼠标看路边的一栋楼结果手一松镜头瞬间被拉回原来的飞行路径上。原因onTick回调里每帧执行camera.setView这个操作会无条件覆盖相机状态包括用户通过鼠标交互对相机作的修改。Cesium 的鼠标控制本质上也是改 camera但它是增量式的修改你的 setView 是重置式修改两者冲突时你这边赢所以用户拖完马上被拽回来。解决加入一个“用户干涉”标志位。监听场景的鼠标事件在漫游中如果用户触发拖拽就暂停自动飞行把控制权交给用户viewer.screenSpaceEventHandler.setInputAction(function () { if (isFlying) { stopFlight() } }, Cesium.ScreenSpaceEventType.LEFT_DOWN) viewer.screenSpaceEventHandler.setInputAction(function () { if (isFlying) { stopFlight() } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE) // 仅当 LEFT_DOWN 时触发逻辑说明LEFT_DOWN事件表示用户按下了鼠标这时候应该立刻停掉自动漫游。这比等用户拖完再判断好得多因为 LEFT_DOWN 的瞬间用户就已经表达了对当前自动控制的不满。MOUSE_MOVE 事件的回调里要判断是否按下左键否则鼠标在场景里飘过也会触发暂停误伤率太高。参数说明这种方案等于把“停止漫游”的操作权重给了用户。飞行的结束状态可以用 4.4 里的 stopFlight 函数来接管让相机平稳复位。如果你希望用户释放鼠标后能“继续自动驾驶”那需要在 mouse up 时重新从当前相机位置计算新的路径继续飞行实现起来要额外做一个“接续路径”的逻辑工作量大不少不太建议第一版做。5.4 飞行结束后视角被锁定lookAtTransform 忘记重置现象漫游结束后用户拖动鼠标发现场景完全无法旋转像是被钉死了一样。原因在路径设计时为了从固定角度观察某个点你调用过camera.lookAt或者lookAtTransform设置了观察矩阵。飞行结束后如果没有重置这个矩阵相机就一直保持“锁定目标点”的状态任何交互都失效。解决在所有结束路径的代码里强制把 lookAtTransform 重置为Matrix4.IDENTITY放在第一步。也就是不管什么原因导致漫游结束自然结束、用户打断、代码异常都要在停止逻辑的第一行执行这个重置。你可以把 stopFlight 函数里的一行提到最前面。6. 进阶技巧把速度曲线揉进路径让飞行像“有经验的驾驶员”操作自动漫游最容易看出“机器感”的地方是速度起飞和停止都很突兀转弯也不减速。一个简单的改进是给速度加一个梯形斜坡——起飞时从 0 加速到巡航速度结束前提前减速转弯段按曲率自动降速。实现上不需要改插值算法只改 times 数组的分配逻辑即可。方法如下先把路径的累计长度算出来然后把它离散成 N 个采样点给每个点分配一个“设计速度”。速度在进入转弯前线性递减出弯后线性递增。最后用每个采样点的路径距离除以该处速度得到每段所需时间累加就得到最终 times 数组function buildTimesWithSpeed(points, speeds) { let cumulativeDistance 0 const distances [] for (let i 1; i points.length; i) { const d Cesium.Cartesian3.distance(points[i - 1], points[i]) cumulativeDistance d distances.push(cumulativeDistance) } const times [0] let elapsed 0 for (let i 0; i distances.length; i) { const segSpeed (speeds[i] speeds[i 1]) / 2 // 平均速度 const segTime (distances[i] - (i 0 ? distances[i - 1] : 0)) / segSpeed elapsed segTime times.push(elapsed) } const total times[times.length - 1] return times.map(t t / total) // 归一化到 [0, 1] }逻辑说明每个采样点对应一个设计速度段速度取两端的平均值时间等于距离除以平均速度。这样一个弯道密集的路径每个点的 time 间隔会比较大插值后的曲线在时间轴上自动实现“转弯时慢、直行时快”的效果。参数说明speeds数组的长度必须和points一致每个速度值单位是米/秒。设计速度的赋值没有绝对标准但有几个经验值可以参考平直路段设 80~150 米/秒转弯段设 30~60 米/秒靠近观察点时设 10~20 米/秒。你可以在场景里用 speed 标签可视化每一段的速度调整到看起来“舒服”为止。另外还有一个很容易被忽视的技巧飞行中相机的 up 向量不要一直用 UNIT_Z。当路径接近竖直方向时比如从山顶俯冲到山谷UNIT_Z 和 forward 方向夹角太小叉积结果不稳定画面会突然翻转。解决方法是动态切换 up 向量——当 forward 与 UNIT_Z 夹角小于 30 度时改用相机的历史 up 向量插值过渡而不是直接跳function computeUpVector(forward, lastUp) { const angle Cesium.Math.acosClamped( Cesium.Cartesian3.dot(forward, Cesium.Cartesian3.UNIT_Z) ) if (Cesium.Math.toDegrees(angle) 30) { return Cesium.Cartesian3.clone(Cesium.Cartesian3.UNIT_Z) } // 用上一个 up 向量的垂直分量平滑过渡 const right Cesium.Cartesian3.cross(forward, lastUp, new Cesium.Cartesian3()) const newUp Cesium.Cartesian3.cross(right, forward, new Cesium.Cartesian3()) return Cesium.Cartesian3.normalize(newUp, newUp) }逻辑说明acosClamped是 Cesium 提供的一个安全的反余弦函数输入被限制在 [-1, 1] 区间避免因浮点误差出现 NaN。如果 forward 接近竖直方向就用 lastUp 重新做一次正交基构建得到一个新的 up。this 的 lastUp 在每帧渲染时更新为上一帧的 up 值。从那以后我每次做 Cesium 飞行漫游都会强制自己走一遍这个流程画路径、采样地形、可视化检查、预演速度曲线、加用户中断处理。这套流程看着多但能在一开始挡住绝大部分穿地和眩晕问题。动画这种东西做出来“能跑”容易做出来“能用”就得靠这些细节一点一点堆。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑