戰(zhàn):樣條插值平滑路徑與姿態(tài)控制)
簡介面向具備一定GIS基礎(chǔ)的Cesium開發(fā)者這份可運(yùn)行源碼完整演示了自主漫游功能的實(shí)現(xiàn)流程適合在三維場景交互、數(shù)字孿生或智慧城市項(xiàng)目中復(fù)用。資源以WASD及方向鍵作為輸入入口監(jiān)聽鍵盤事件切換小車的移動狀態(tài)并利用CallbackProperty在每幀動態(tài)更新實(shí)體的位置與朝向最終將整套控制邏輯封裝為class降低調(diào)用成本。壓縮包共3個文件含主體HTML頁面、項(xiàng)目配置及輔助說明文件包體僅6KB結(jié)構(gòu)簡單便于直接運(yùn)行和修改。目前已有66人學(xué)習(xí)查看對想快速掌握Cesium鍵盤漫游原理的開發(fā)者有一定參考價值。除源碼外配套內(nèi)容還解釋了Cesium應(yīng)用初始化、事件綁定與實(shí)體更新的關(guān)鍵細(xì)節(jié)可直接替換模型或調(diào)整參數(shù)適配個性化場景。 前陣子接了某個城市級巡檢演示項(xiàng)目提需求的時候?qū)Ψ秸f得很輕巧“就像無人機(jī)航拍那樣讓相機(jī)自己沿著路線飛一遍就行?!苯Y(jié)果真做起來才發(fā)現(xiàn)這個“飛一遍”里全是細(xì)節(jié)路徑怎么定義、相機(jī)姿態(tài)怎么平滑過渡、速度怎么控制、走到一半怎么暫停還有最要命的——怎么讓三維場景在漫游過程中不抖、不卡、不穿模。這期間我反復(fù)查了 Cesium 官方文檔和社區(qū)里的零散貼子最后整理出一套能直接跑起來的自主漫游方案也把踩過的坑一并記了下來這篇文章就是完整復(fù)盤。如果你正在做 Cesium 里的自動巡檢、路線預(yù)覽、建筑環(huán)繞展示或者數(shù)字孿生體漫游這篇實(shí)戰(zhàn)筆記應(yīng)該能幫你省掉不少彎路。文章會把方案選型、環(huán)境搭建、核心源碼、排錯記錄全部攤開講代碼不是偽代碼是從我項(xiàng)目里抽出來的可運(yùn)行版本標(biāo)題里寫的“可運(yùn)行源碼”不是噱頭。1. 先說結(jié)論自主漫游不只是相機(jī)沿路徑走一遍很多第一次接觸漫游需求的同學(xué)會以為只要給相機(jī)設(shè)置好一系列坐標(biāo)點(diǎn)再每秒改一下視角位置就算完成了。真這么做你得到的只會是一段跳變劇烈、卡頓明顯、觀感很差的“瞬移視頻”。自主漫游背后其實(shí)是三個維度的聯(lián)動位置連續(xù)變化、姿態(tài)連續(xù)變化、場景資源按需加載。少了任何一環(huán)效果都撐不住。1.1 三種“會動”的漫游姿勢對比在 Cesium 生態(tài)里常見的自主漫游方案可以分成三類我用自己的話做個對比表格方案實(shí)現(xiàn)方式優(yōu)點(diǎn)缺點(diǎn)適用場景關(guān)鍵幀跳轉(zhuǎn)式直接設(shè)置 viewer.camera.setView 按坐標(biāo)點(diǎn)逐幀切換代碼量最少畫面跳變嚴(yán)重毫無平滑可言不推薦用于最終效果只適合調(diào)試樣條插值巡航用 CatmullRomSpline 或 HermiteSpline 插值位置配合球面插值處理姿態(tài)平滑、可控、性能開銷可控需要額外處理姿態(tài)插值和速度均勻性巡檢路線、建筑環(huán)繞、無人機(jī)路徑預(yù)覽物理驅(qū)動漫游給相機(jī)掛速度、加速度、碰撞半徑實(shí)時計算下一幀位置真實(shí)感最強(qiáng)支持交互避障實(shí)現(xiàn)復(fù)雜調(diào)試成本高游戲化場景、步行漫游、碰撞場景我這次選的是第二類樣條插值巡航。原因很簡單項(xiàng)目要求是“給一條預(yù)定義路線讓相機(jī)自動飛過去”不需要用戶實(shí)時控制方向但必須畫面順滑、能隨時暫停繼續(xù)。樣條插值在數(shù)學(xué)上成熟、代碼可控性強(qiáng)跑起來也穩(wěn)。1.2 這次方案選用的完整技術(shù)路徑整個技術(shù)路線可以概括成一句話用樣條曲線描述路徑用雙軌插值控制位姿用 Clock 事件驅(qū)動刷新。也就是說路徑不只是位置坐標(biāo)數(shù)組而是拆成兩條插值軌道位置軌道經(jīng)緯度高度構(gòu)成三維坐標(biāo)走 CatmullRomSpline 插值確保曲線平滑經(jīng)過所有途經(jīng)點(diǎn)姿態(tài)軌道每個路徑點(diǎn)額外記錄相機(jī)的 heading航向角、pitch俯仰角、roll翻滾角走線性插值或球面線性插值確保視角方向也在連續(xù)變化。這樣做的收益是路線拐彎時相機(jī)不會突然甩頭從高空俯視切換到平視建筑時也不會瞬間跳變。后面第 3 節(jié)會重點(diǎn)說姿態(tài)插值的坑。2. 項(xiàng)目骨架與 Cesium 環(huán)境搭建跑不起來的代碼等于廢紙自主漫游的核心邏輯雖然不依賴特定的工程結(jié)構(gòu)但如果環(huán)境沒搭好后面所有調(diào)優(yōu)都會變得很痛苦。先說一句實(shí)在話Cesium 的版本迭代不算慢網(wǎng)上很多老教程用的 API 已經(jīng)廢棄了照抄很容易在控制臺看到一堆 deprecation 警告。2.1 Vite Cesium 的版本組合建議我這次用的是 Vite 5 Cesium 1.119 的組合。選 Vite 而不是 Webpack主要是開發(fā)體驗(yàn)好、熱更新快而且 Vite 對 Cesium 的支持比早期好太多了不需要再做復(fù)雜的靜態(tài)資源拷貝配置。依賴安裝沒有什么玄學(xué)直接npm create vitelatest cesium-route-tour -- --template vanilla cd cesium-route-tour npm install cesium但有個關(guān)鍵點(diǎn)必須提醒Cesium 的靜態(tài)資源Assets、Workers、Widgets需要被正確加載。如果你不想折騰 CesiumVitePlugin可以用最樸素的方式在index.html里引入link hrefnode_modules/cesium/Build/Cesium/Widgets/widgets.css relstylesheet / script srcnode_modules/cesium/Build/Cesium/Cesium.js/script然后在你自己的main.js里通過全局的Cesium對象訪問 API。這個方式不潮但它真的不會出幺蛾子。官方教程里推薦的import * as Cesium from cesium配合 Vite 時必須處理靜態(tài)資源路徑對于純漫游功能來說反而容易把問題復(fù)雜化。2.2 基礎(chǔ)場景配置光照、地形與默認(rèn)相機(jī)角度漫游效果的“質(zhì)感”很大程度上取決于基礎(chǔ)場景。我在代碼里固定做了這幾項(xiàng)設(shè)置viewer new Cesium.Viewer(cesiumContainer, { baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, animation: true, timeline: true, shouldAnimate: true, }); viewer.scene.globe.enableLighting true; viewer.scene.globe.depthTestAgainstTerrain true;enableLighting打開后場景會隨太陽位置實(shí)時模擬光照漫游時建筑和地形的明暗變化能明顯增強(qiáng)真實(shí)感這也是很多實(shí)戰(zhàn)效果看起來“高級”的原因。depthTestAgainstTerrain必須打開否則相機(jī)貼近地表時會出現(xiàn)穿透地形的穿幫畫面。地形這里我用的是 Cesium 默認(rèn)離線地形。如果你做的是單體建筑漫游、不需要真實(shí)地理地形可以完全不加載地形服務(wù)直接把路徑點(diǎn)設(shè)在一個局部坐標(biāo)系里。但要注意Camera 飛行的角度和高度必須跟你自己定的地形基準(zhǔn)面匹配不然相機(jī)會陷入地下或者懸空。如果你準(zhǔn)備接入真實(shí)地形服務(wù)本地切片或在線服務(wù)都行但一定要在初始化 Viewer 之后顯式設(shè)置 terrainProvider 不然默認(rèn)的 Ellipsoid 地形會讓路徑高度計算和真實(shí)地表完全脫節(jié)。3. 漫游路徑核心位置與姿態(tài)的雙軌插值說到自主漫游繞不開的就是插值。位置插值相對直觀姿態(tài)插值才是真正考驗(yàn)細(xì)節(jié)的地方。這一節(jié)我把兩部分的原理和實(shí)現(xiàn)拆開講代碼可以直接抄。3.1 路徑點(diǎn)定義與 CatmullRomSpline 使用要點(diǎn)路徑點(diǎn)我習(xí)慣用一個結(jié)構(gòu)體數(shù)組來描述const routePoints [ { lon: 120.15, lat: 30.28, height: 800, heading: 0, pitch: -45, roll: 0, duration: 5 }, { lon: 120.16, lat: 30.29, height: 600, heading: 30, pitch: -30, roll: 0, duration: 5 }, { lon: 120.18, lat: 30.28, height: 400, heading: -60, pitch: -20, roll: 0, duration: 5 }, ];每個點(diǎn)除了經(jīng)緯度和高度還帶了一個duration字段表示從前一個點(diǎn)飛到當(dāng)前點(diǎn)需要多少秒。為什么要單獨(dú)設(shè)置 duration因?yàn)椴煌穆窂蕉物w行速度不一定相同。比如起飛段速度可以快一點(diǎn)到了目標(biāo)建筑上空就得慢下來仔細(xì)看。基于這些點(diǎn)構(gòu)造樣條const positions routePoints.map(p Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height)); const spline new Cesium.CatmullRomSpline({ times: cumulativeTimes, // 例如 [0, 5, 10, 15] points: positions });這里有個新手高頻錯誤times數(shù)組必須從 0 開始且單調(diào)遞增。如果你的路徑點(diǎn)很多建議先按duration計算累計時間再傳給 CatmullRomSpline。如果不設(shè)置 times默認(rèn)會按均勻參數(shù)生成實(shí)際效果會變成“每個點(diǎn)之間的飛行時間相同”而這不是我們想要的。Cartesian3 可以直接傳入 CatmullRomSpline因?yàn)樗鼉?nèi)部會自動處理坐標(biāo)維數(shù)。但注意Cesium 的樣條曲線默認(rèn)是在三維笛卡爾空間做的所以經(jīng)緯度高度會被轉(zhuǎn)成 Cartesian3 之后再做曲線插值。對于跨越大范圍地理區(qū)域的路線這種插值在球面上會有一點(diǎn)點(diǎn)偏離但對城市級巡檢和小場景漫游完全沒問題。3.2 heading/pitch/roll 插值最容易踩的坑位置可以三維插值姿態(tài)卻不能直接對著 heading 做線性插值。原因有兩個第一heading 有角度循環(huán)問題。比如當(dāng)前航向角是 350 度下一幀的目標(biāo)角是 10 度如果直接線性插值相機(jī)會從 350 度逆時針轉(zhuǎn)到 10 度多轉(zhuǎn)了 340 度畫面表現(xiàn)為突然大幅甩頭。正確的做法是先把角度差歸一到 (-180, 180] 區(qū)間function normalizedAngleDiff(a, b) { let diff b - a; while (diff Math.PI) diff - 2 * Math.PI; while (diff -Math.PI) diff 2 * Math.PI; return diff; }第二Cesium 的相機(jī)視角在 pitch 接近 ±90 度時會出現(xiàn)萬向鎖效應(yīng)也就是所謂“抖動”。如果你的巡檢路線包含從高空垂直往下看的視角pitch 接近 -90 度heading 會突然變得敏感插值稍有不慎畫面就會劇烈旋轉(zhuǎn)。我最終的方案是把每個路徑點(diǎn)的 heading、pitch、roll 也作為插值控制參數(shù)用平滑的smoothstep或者slerp做過渡。具體代碼在 3.3 里一起給出整體邏輯是當(dāng)前時間 t 落在哪兩個路徑點(diǎn)之間計算出局部進(jìn)度 u0 到 1位置用 spline 接口直接獲取姿態(tài)用當(dāng)前點(diǎn)和下一個點(diǎn)的 heading/pitch/roll 做帶角度歸一化的線性插值再用一個緩動函數(shù)做平滑。3.3 速度控制與勻速運(yùn)動的實(shí)現(xiàn)邏輯路徑是均勻參數(shù)化的卡特莫爾樣條但地理坐標(biāo)空間里的“均勻參數(shù)”不代表“均勻地面速度”。這個問題常被忽略你在局部坐標(biāo)系里看起來勻速的樣條經(jīng)過經(jīng)緯度轉(zhuǎn) Cartesian3 之后實(shí)際地面速度可能忽快忽慢。解決思路是重采樣。我先對 spline 做密集采樣比如每幀間隔 50ms 取一個點(diǎn)計算相鄰點(diǎn)之間的距離累加得到總路程再根據(jù)總路程和總時長反推出勻速情況下每一幀應(yīng)該到達(dá)的總路程位置最后用二分查找或者線性掃描找到對應(yīng)的時間參數(shù) t。這樣位置插值在視覺上就是勻速的。這個邏輯對漫游體驗(yàn)至關(guān)重要。如果直接用原始 sample 函數(shù)獲取位置你會看到相機(jī)在路徑平緩處飛得很快、在拐彎處突然卡頓觀感極差。4. 漫游控制器的完整實(shí)現(xiàn)附核心源碼有了路徑、姿態(tài)、速度這三塊基石接下來就是把它們封裝成一個可復(fù)用的漫游控制器。我寫了一個獨(dú)立的類RouteTourController包含啟動、暫停、繼續(xù)、終止、變速五個核心接口和一個內(nèi)部的 tick 回調(diào)。4.1 控制器類設(shè)計狀態(tài)機(jī)與 tick 驅(qū)動控制器用狀態(tài)機(jī)管理漫游生命周期IDLE空閑、RUNNING運(yùn)行中、PAUSED暫停、FINISHED結(jié)束。狀態(tài)切換對應(yīng)了每幀處理邏輯的開關(guān)。核心實(shí)現(xiàn)如下class RouteTourController { constructor(viewer, routePoints) { this.viewer viewer; this.routePoints routePoints; this.state IDLE; this.clock viewer.clock; this.spline null; this.totalDuration 0; this.accumulatedTime 0; this.speedFactor 1; this._precompute(); } _precompute() { const times []; let acc 0; times.push(acc); for (let i 1; i this.routePoints.length; i) { acc this.routePoints[i].duration; times.push(acc); } this.totalDuration acc; const positions this.routePoints.map(p Cesium.Cartesian3.fromDegrees(p.lon, p.lat, p.height) ); this.spline new Cesium.CatmullRomSpline({ times, points: positions }); // 預(yù)計算姿態(tài)控制點(diǎn) this.yawPoints this.routePoints.map(p Cesium.Math.toRadians(p.heading)); this.pitchPoints this.routePoints.map(p Cesium.Math.toRadians(p.pitch)); this.rollPoints this.routePoints.map(p Cesium.Math.toRadians(p.roll)); } start() { if (this.state RUNNING) return; this.state RUNNING; this.accumulatedTime 0; this.clock.shouldAnimate true; this.removeCallback this.viewer.clock.onTick.addEventListener(this._tick.bind(this)); } pause() { this.state PAUSED; } resume() { if (this.state ! PAUSED) return; this.state RUNNING; } stop() { this.state FINISHED; if (this.removeCallback) { this.removeCallback(); this.removeCallback undefined; } } _tick(clock) { if (this.state ! RUNNING) return; const deltaSeconds clock.clockDelta; this.accumulatedTime deltaSeconds * this.speedFactor; if (this.accumulatedTime this.totalDuration) { this.accumulatedTime this.totalDuration; this._applyPose(this.accumulatedTime / this.totalDuration, 1); this.stop(); return; } const progress this.accumulatedTime / this.totalDuration; this._applyPose(progress, this.accumulatedTime); } _applyPose(progress, currentTime) { if (!this.spline) return; const position this.spline.evaluate(currentTime); // 找到當(dāng)前所處的路徑段 const times this.spline.times; let segIndex 0; for (let i 0; i times.length - 1; i) { if (currentTime times[i] currentTime times[i 1]) { segIndex i; break; } } const segStart times[segIndex]; const segEnd times[segIndex 1]; const u (currentTime - segStart) / (segEnd - segStart); const smoothU Cesium.Math.smoothstep(u, 0, 1); let heading this.yawPoints[segIndex] normalizedAngleDiff(this.yawPoints[segIndex], this.yawPoints[segIndex 1]) * smoothU; let pitch this.pitchPoints[segIndex] (this.pitchPoints[segIndex 1] - this.pitchPoints[segIndex]) * smoothU; let roll this.rollPoints[segIndex] (this.rollPoints[segIndex 1] - this.rollPoints[segIndex]) * smoothU; this.viewer.camera.setView({ destination: position, orientation: { heading, pitch, roll } }); } }這個控制器有幾點(diǎn)細(xì)節(jié)需要特別說明onTick監(jiān)聽的是viewer.clock的 tick 事件而不是自己寫requestAnimationFrame循環(huán)。好處是能和 Cesium 的時間系統(tǒng)聯(lián)動暫停時間軸時漫游也會停播放速度可以直接通過clock.multiplier控制。clock.clockDelta表示上一幀到這一幀的秒數(shù)用它累加漫游時間能保證不同幀率下的漫游速度一致。如果你在 144Hz 的顯示器和 60Hz 的顯示器上跑同一個任務(wù)漫游到同一個點(diǎn)的時間是一樣的不會因?yàn)樗⑿侣什煌兯?。normalizedAngleDiff就是 3.2 里寫的角度歸一化函數(shù)實(shí)際代碼中我把它定義成了模塊內(nèi)工具函數(shù)避免每次 tick 都重復(fù)計算。4.2 暫停、繼續(xù)、重新開始的按鍵控制自主漫游實(shí)際演示時最常被現(xiàn)場提出來的需求就是“暫停一下停在當(dāng)前視角我講兩句”。按鍵控制是剛需。我的實(shí)現(xiàn)非常簡單直接在頁面監(jiān)聽鍵盤document.addEventListener(keydown, (e) { if (e.key ) { e.preventDefault(); if (controller.state RUNNING) { controller.pause(); } else if (controller.state PAUSED) { controller.resume(); } } if (e.key r || e.key R) { controller.start(); } });暫停期間畫面要保持靜止所以_tick里必須判斷狀態(tài)只讓 RUNNING 狀態(tài)下的邏輯執(zhí)行。這個看起來理所當(dāng)然但很多人會忘記在 pause 時停掉時間軸導(dǎo)致clock.shouldAnimate還在跑相機(jī)不動但場景里的小車、粒子卻在動演示時就穿幫了。4.3 把控制器接到 Cesium 相機(jī)上實(shí)際接入時記得先確保三維場景已經(jīng)初始化完成。建議在viewer.scene.globe.tileLoadProgressEvent或者viewer.scene.postRender里等待首幀渲染完成后再調(diào)用controller.start()否則有可能出現(xiàn)相機(jī)已經(jīng)飛到終點(diǎn)但地形瓦片才剛開始加載的尷尬情況。另外如果你的場景里有大量 3D Tiles 模型建議在漫游開始前預(yù)先加載模型所在區(qū)域的瓦片。一個實(shí)用做法是先用viewer.camera.flyTo飛到路線起點(diǎn)上空等viewer.scene.tileLoadProgressEvent觸發(fā)的進(jìn)度歸零后再啟動漫游。實(shí)測下來這個“預(yù)加載延遲啟動”的動作能顯著減少漫游過程中出現(xiàn)的瓦片彈出。5. 實(shí)測環(huán)節(jié)從代碼到順暢漫游的排錯記錄代碼寫完之后真正折磨人的是各種詭異的現(xiàn)象。我把自己在調(diào)試過程中遇到的幾個高頻問題整理了一下每個都標(biāo)了根因和解決方式希望你能跳過這些坑。5.1 相機(jī)抖動被忽略的四元數(shù)過渡問題我最早實(shí)現(xiàn)姿態(tài)插值的時候直接用線性插值處理 heading/pitch/roll結(jié)果在路徑拐彎處畫面出現(xiàn)了明顯的抖動。排查了很久才意識到是角度跳變問題當(dāng)路徑點(diǎn) A 的 heading 是 170 度、路徑點(diǎn) B 的 heading 是 -170 度時線性插值會讓航向角從 170 一路變到 -170這期間相機(jī)幾乎轉(zhuǎn)了 340 度畫面看起來肯定是在瘋狂旋轉(zhuǎn)。后來的解決辦法就是前面代碼里的normalizedAngleDiff先把角度差歸一化到 [-180, 180]再乘上插值比例。這樣從 170 度到 -170 度的插值結(jié)果會變成從 170 度轉(zhuǎn)到 190 度即 -170 360實(shí)際旋轉(zhuǎn)只有 20 度視覺上完全正常。5.2 加載大場景時的預(yù)加載策略城市級 3D Tiles 模型體量很大漫游過程中不斷加載瓦片會導(dǎo)致幀率劇烈波動。我的處理方式是在漫游路線的每個關(guān)鍵路徑點(diǎn)提前“預(yù)熱”周邊瓦片。代碼層面沒有特別優(yōu)雅的官方 API我是這么做的routePoints.forEach(p { const rectangle Cesium.Rectangle.fromDegrees( p.lon - 0.005, p.lat - 0.005, p.lon 0.005, p.lat 0.005 ); viewer.scene.camera.setView({ destination: rectangle, duration: 0 }); viewer.scene.render(); });這段代碼會讓場景引擎在內(nèi)存中預(yù)加載這些區(qū)域但不會真正顯示在哪因?yàn)殡S即就切回了起點(diǎn)。雖然聽起來有點(diǎn)暴力實(shí)測對降低漫游過程中的卡頓確實(shí)有效。注意每渲染完一個區(qū)域要立即viewer.scene.render()強(qiáng)制觸發(fā)一次渲染否則瓦片調(diào)度可能不會及時執(zhí)行。如果你是加載本地離線地形和影像切片數(shù)據(jù)量更大建議直接限制一下相機(jī)默認(rèn)的屏幕空間誤差viewer.scene.screenSpaceError把默認(rèn)的 2 調(diào)大到 4 或者 8漫游流暢度會有明顯改善遠(yuǎn)看效果也幾乎無感知差異。5.3 與動態(tài)光照、雷達(dá)特效、可視域分析組合時的取舍熱搜詞里提到的 Cesium 動態(tài)光照、雷達(dá)掃描、可視域分析這些特效理論上都可以和自主漫游疊加使用但必須注意 GPU 負(fù)載分配。我這里給出的經(jīng)驗(yàn)是動態(tài)光照enableLighting可以開但不要在漫游過程中頻繁改太陽位置否則會觸發(fā)全局重光照瞬間掉幀。雷達(dá)掃描、雷達(dá)扇形這類特效如果用的是自定義 Primitive 或 PostProcessStage盡量把漫游速度降低一點(diǎn)因?yàn)樘匦в嬎懔坎恍???梢曈蚍治龊吞祀H線分析其實(shí)更適合靜態(tài)視角下做自主漫游過程中開啟這些分析會導(dǎo)致界面信息過載演示效果反而差。如果確實(shí)想同時展示可視域分析和漫游我的建議是先讓漫游飛到目標(biāo)點(diǎn)暫停再啟動分析。這樣一個“飛過去停下來分析”的節(jié)奏在演示時邏輯更清晰觀眾也更容易看明白。另外還有一個容易被忽略的點(diǎn)如果你用了viewer.scene.debugShowFramesPerSecond true或者開了 Cesium 的默認(rèn) FPS 顯示疊加特效后幀率掉到 20 以下千萬不要懷疑是代碼寫錯了先檢查是不是同時開了太多后處理特效。我遇到過最夸張的一次同時開著霧效、動態(tài)光照、雷達(dá)掃描三個 PostProcessStage幀率直接從 60 跌到 12關(guān)掉兩個后立刻恢復(fù)流暢。寫在最后的一點(diǎn)體會自主漫游這個功能真正做到位了就是“外行看著覺得很順滑內(nèi)行知道是插值和狀態(tài)管理做得扎實(shí)”。我再分享幾個自己常用的細(xì)節(jié)習(xí)慣第一路徑點(diǎn)別拍腦袋隨便定。我在項(xiàng)目里會在 Cesium 里先用viewer.entities.add把路徑點(diǎn)用點(diǎn)狀實(shí)體標(biāo)出來再跑一遍漫游確認(rèn)視角和姿態(tài)是否符合預(yù)期。調(diào)姿態(tài)時直接在瀏覽器控制臺里改角度數(shù)值比反復(fù)改源碼重跑快得多。第二漫游結(jié)束時讓相機(jī)有一個自然的減速。我的實(shí)現(xiàn)里是用 smoothstep 對姿態(tài)做平滑位置方面想要減速的話可以在最后兩個路徑點(diǎn)之間把 time 參數(shù)映射成緩動曲線比如easeOutCubic這樣相機(jī)到達(dá)終點(diǎn)時不會突兀地剎停。第三也是我認(rèn)為最重要的自主漫游的代碼一定要做成獨(dú)立的控制器類而不是散落在各個回調(diào)里。你很難預(yù)料甲方后面會不會加“漫游到一半切換到另一個視角”或者“速度調(diào)快 1.5 倍”這種需求控制層和渲染層分離能讓你面對臨時改動時游刃有余。這篇文章里貼的代碼片段就是我項(xiàng)目里跑過的完整邏輯的濃縮版拿回去接上你自己的 Cesium Viewer填入幾個路徑點(diǎn)差不多就能看到效果。后續(xù)如果你想繼續(xù)往深做可以從這幾個方向入手在漫游過程中疊加模型姿態(tài)控制、接入 three.js 共享 WebGL 上下文做更復(fù)雜的特效、或者把漫游路線做成工具化的路徑編輯器。每一塊都是另一個容量不小的實(shí)戰(zhàn)話題了。本文還有配套的精品資源點(diǎn)擊獲取