系轉(zhuǎn)換全解析:從經(jīng)緯高到虛幻世界坐標(biāo))
1. 從經(jīng)緯度到虛幻單位Cesium for UE4 坐標(biāo)系到底在折騰什么如果你剛把 Cesium for UE4 插件拖進(jìn)引擎興沖沖地加載了一塊真實(shí)地形結(jié)果發(fā)現(xiàn)相機(jī)飛到了十萬八千里外或者模型像被揉皺的紙團(tuán)一樣扭曲那大概率不是插件壞了而是坐標(biāo)系在跟你鬧脾氣。Cesium for UE4 的核心價(jià)值在于把真實(shí)地理空間數(shù)據(jù)無縫接入虛幻引擎而它最讓人頭疼也最迷人的地方就是那一套橫跨地理坐標(biāo)系、投影坐標(biāo)系和引擎世界坐標(biāo)的轉(zhuǎn)換體系。這篇文章不打算給你背手冊(cè)而是把我自己在項(xiàng)目里反復(fù)調(diào)試、踩坑、推翻重來的經(jīng)驗(yàn)攤開來講從底層邏輯到實(shí)操細(xì)節(jié)把坐標(biāo)系轉(zhuǎn)換這件事徹底聊透。先說清楚這套東西適合誰看。如果你只是想在 UE4 里擺幾個(gè)靜態(tài)模型那完全用不著 Cesium原生坐標(biāo)就夠了。但只要你涉及真實(shí)地形、傾斜攝影、BIM 模型落位、飛行軌跡模擬、或者任何需要把現(xiàn)實(shí)世界經(jīng)緯度映射到虛幻場(chǎng)景里的活兒坐標(biāo)系轉(zhuǎn)換就是繞不過去的坎。我見過太多項(xiàng)目卡在這一步地形加載出來了但建筑模型飄在半空相機(jī)能飛但飛到特定經(jīng)緯度就抖動(dòng)兩個(gè)不同來源的數(shù)據(jù)疊在一起偏差幾百米。這些問題表面上看是“位置不對(duì)”根子上都是坐標(biāo)系沒對(duì)齊。Cesium for UE4 的坐標(biāo)系體系可以粗暴地分成三層最底層是地理坐標(biāo)經(jīng)度、緯度、高度中間層是地心直角坐標(biāo)ECEF最上層是虛幻引擎的世界坐標(biāo)UE World Coordinates。這三層之間的轉(zhuǎn)換不是簡(jiǎn)單的加減乘除而是涉及橢球體參數(shù)、原點(diǎn)偏移、單位縮放、以及一個(gè)叫“原點(diǎn)重定位”的機(jī)制。很多人第一次接觸會(huì)覺得這套設(shè)計(jì)多此一舉直接給經(jīng)緯度不行嗎不行。因?yàn)樘摶靡娴母↑c(diǎn)精度有限如果把整個(gè)地球的坐標(biāo)直接塞進(jìn)場(chǎng)景離原點(diǎn)稍微遠(yuǎn)一點(diǎn)模型就會(huì)開始抖動(dòng)甚至撕裂。Cesium 的解決方案是動(dòng)態(tài)選擇一個(gè)“原點(diǎn)”把附近的數(shù)據(jù)拉近到引擎原點(diǎn)附近保證精度。這個(gè)原點(diǎn)選在哪里、怎么選、什么時(shí)候切換就是坐標(biāo)系轉(zhuǎn)換的核心戰(zhàn)場(chǎng)。我剛開始用的時(shí)候以為只要調(diào)用一個(gè)LongitudeLatitudeHeightToUnreal函數(shù)就完事了。結(jié)果發(fā)現(xiàn)同一個(gè)經(jīng)緯度在不同時(shí)間、不同相機(jī)位置下轉(zhuǎn)換出來的 UE 坐標(biāo)居然不一樣。當(dāng)時(shí)差點(diǎn)以為是插件有 bug后來才明白這是原點(diǎn)重定位在動(dòng)態(tài)調(diào)整。理解這一點(diǎn)之后很多看似詭異的現(xiàn)象就都能解釋了。接下來的內(nèi)容我會(huì)從最基礎(chǔ)的地理坐標(biāo)講起一步步推到 ECEF再講到 UE 世界坐標(biāo)中間穿插大量實(shí)際調(diào)試中遇到的坑和解決方案。你不需要有測(cè)繪背景但需要有一點(diǎn)耐心因?yàn)檫@套東西一旦理順后面做地理空間項(xiàng)目就會(huì)像呼吸一樣自然。2. 地理坐標(biāo)與 ECEF一切轉(zhuǎn)換的起點(diǎn)2.1 經(jīng)緯高到底是怎么描述一個(gè)點(diǎn)的地理坐標(biāo)系用經(jīng)度、緯度、高度來描述地球表面或附近的點(diǎn)。經(jīng)度范圍是 -180 到 180 度緯度范圍是 -90 到 90 度高度是相對(duì)于參考橢球面的距離。這里有個(gè)容易混淆的地方高度通常指的是橢球高而不是海拔高。橢球高和海拔高之間差了一個(gè)大地水準(zhǔn)面差距這個(gè)差距在不同地區(qū)可能達(dá)到幾十米。Cesium for UE4 默認(rèn)使用的是 WGS84 橢球這也是 GPS 使用的標(biāo)準(zhǔn)。如果你從外部數(shù)據(jù)源拿到的是海拔高直接丟給 Cesium 可能會(huì)產(chǎn)生垂直方向的偏移這一點(diǎn)在精度要求高的項(xiàng)目里必須注意。經(jīng)度和緯度本身是角度不是距離。一度緯度對(duì)應(yīng)的地面距離大約是 111 公里但一度經(jīng)度對(duì)應(yīng)的距離隨緯度變化在赤道附近約 111 公里在兩極附近趨近于零。這個(gè)非線性關(guān)系是很多位置計(jì)算錯(cuò)誤的根源。比如你想讓相機(jī)沿著經(jīng)度方向移動(dòng)固定距離不能簡(jiǎn)單地把經(jīng)度加一個(gè)固定值而要根據(jù)當(dāng)前緯度做換算。Cesium for UE4 提供了一些輔助函數(shù)來處理這類問題但理解背后的數(shù)學(xué)仍然很重要否則你很難判斷結(jié)果對(duì)不對(duì)。在實(shí)際項(xiàng)目中我通常會(huì)把所有外部數(shù)據(jù)先統(tǒng)一成 WGS84 經(jīng)緯高格式然后再交給 Cesium 處理。這樣做的好處是有一個(gè)明確的中間格式方便排查問題。如果數(shù)據(jù)源用的是其他橢球比如 CGCS2000雖然和 WGS84 差異很小但在大范圍場(chǎng)景下仍然可能累積出可觀的偏差。統(tǒng)一格式這一步看起來麻煩但能省掉后面無數(shù)次的“為什么位置不對(duì)”的困惑。2.2 ECEF 才是轉(zhuǎn)換的樞紐地心直角坐標(biāo)系ECEF以地球質(zhì)心為原點(diǎn)X 軸指向本初子午線與赤道的交點(diǎn)Z 軸指向北極Y 軸按右手定則確定。任何一個(gè)地理坐標(biāo)點(diǎn)都可以通過一組數(shù)學(xué)公式轉(zhuǎn)換成 ECEF 坐標(biāo)。這個(gè)轉(zhuǎn)換是確定性的不涉及任何動(dòng)態(tài)調(diào)整所以它是整個(gè)坐標(biāo)系體系里最穩(wěn)定的一環(huán)。Cesium for UE4 內(nèi)部大量使用 ECEF 作為中間表示因?yàn)?ECEF 是笛卡爾坐標(biāo)可以直接做向量運(yùn)算而經(jīng)緯高是角度和距離的混合運(yùn)算起來很別扭。從經(jīng)緯高到 ECEF 的轉(zhuǎn)換公式涉及橢球的長(zhǎng)半軸、短半軸和第一偏心率。具體來說先根據(jù)緯度計(jì)算卯酉圈曲率半徑然后分別計(jì)算 X、Y、Z 分量。這個(gè)公式在教科書上都能查到但實(shí)際寫代碼的時(shí)候要注意單位經(jīng)度緯度要先轉(zhuǎn)成弧度高度單位要和橢球參數(shù)一致。我見過有人把角度和弧度搞混結(jié)果點(diǎn)跑到了地球另一邊。Cesium for UE4 的 API 通常接受角度制但內(nèi)部會(huì)轉(zhuǎn)成弧度所以如果你自己寫轉(zhuǎn)換邏輯一定要確認(rèn)單位。ECEF 坐標(biāo)的數(shù)值范圍很大地球半徑約 6371 公里所以 ECEF 坐標(biāo)的絕對(duì)值在百萬米量級(jí)。這個(gè)量級(jí)直接放進(jìn)虛幻引擎的 float 變量里精度會(huì)嚴(yán)重不足。float 只有大約 7 位有效數(shù)字在百萬量級(jí)下最小分辨率大約是 0.1 米。對(duì)于大多數(shù)場(chǎng)景0.1 米的誤差可能可以接受但對(duì)于高精度應(yīng)用比如建筑構(gòu)件對(duì)齊就不夠了。這就是為什么 Cesium for UE4 不直接把 ECEF 坐標(biāo)給引擎而是要做原點(diǎn)重定位。理解這一點(diǎn)你就能明白為什么插件要設(shè)計(jì)得這么“繞”。2.3 橢球參數(shù)不是擺設(shè)WGS84 橢球的長(zhǎng)半軸是 6378137 米扁率是 1/298.257223563。這兩個(gè)參數(shù)決定了地球的形狀模型。很多人覺得地球就是個(gè)球用球體公式算就行了但在大范圍場(chǎng)景下橢球和球體的差異會(huì)累積到幾十公里。比如在緯度 45 度附近橢球模型和球體模型計(jì)算出的 ECEF 坐標(biāo)差異可能達(dá)到十幾公里。對(duì)于地理空間項(xiàng)目這個(gè)誤差是不可接受的。Cesium for UE4 嚴(yán)格使用橢球模型所以你在做任何自定義轉(zhuǎn)換時(shí)也必須用橢球參數(shù)不能偷懶用球體近似。扁率的影響在極地附近尤其明顯。如果你在極地做項(xiàng)目用球體模型會(huì)導(dǎo)致垂直方向出現(xiàn)顯著偏差。我曾經(jīng)在一個(gè)高緯度地區(qū)的項(xiàng)目里因?yàn)樽远x的某個(gè)轉(zhuǎn)換函數(shù)用了球體近似導(dǎo)致地形和模型之間出現(xiàn)了明顯的錯(cuò)位。排查了很久才發(fā)現(xiàn)是橢球參數(shù)的問題。從那以后我養(yǎng)成了一個(gè)習(xí)慣任何涉及地理坐標(biāo)的代碼第一行先確認(rèn)橢球參數(shù)是否正確。這個(gè)習(xí)慣幫我省了很多時(shí)間。除了長(zhǎng)半軸和扁率有時(shí)候還會(huì)用到第二偏心率、卯酉圈曲率半徑等派生參數(shù)。這些參數(shù)不需要你手動(dòng)計(jì)算Cesium for UE4 的 API 會(huì)處理。但如果你要自己實(shí)現(xiàn)轉(zhuǎn)換最好把這些公式封裝成函數(shù)避免每次手寫。我通常會(huì)把地理坐標(biāo)轉(zhuǎn)換相關(guān)的函數(shù)集中放在一個(gè)工具類里加上詳細(xì)的注釋和單位說明這樣團(tuán)隊(duì)里其他人用的時(shí)候不容易出錯(cuò)。3. 原點(diǎn)重定位Cesium for UE4 的精度魔法3.1 為什么需要原點(diǎn)重定位虛幻引擎的世界坐標(biāo)使用 float 存儲(chǔ)float 的精度隨著數(shù)值增大而降低。在原點(diǎn)附近float 可以精確到微米級(jí)但在 100 公里外精度可能只有幾厘米在 1000 公里外精度可能達(dá)到米級(jí)。對(duì)于地理空間場(chǎng)景如果直接把 ECEF 坐標(biāo)減去一個(gè)固定偏移比如地球半徑得到的坐標(biāo)仍然在百萬米量級(jí)精度依然不夠。原點(diǎn)重定位的核心思想是動(dòng)態(tài)選擇一個(gè)靠近當(dāng)前關(guān)注區(qū)域的原點(diǎn)把該區(qū)域附近的坐標(biāo)拉回到引擎原點(diǎn)附近從而保證 float 精度。這個(gè)原點(diǎn)不是固定的而是隨著相機(jī)或玩家位置移動(dòng)而調(diào)整。Cesium for UE4 會(huì)根據(jù)當(dāng)前視圖中心或指定的原點(diǎn)位置計(jì)算一個(gè)局部的東-北-天ENU坐標(biāo)系然后把 ECEF 坐標(biāo)轉(zhuǎn)換到這個(gè) ENU 坐標(biāo)系下再映射到 UE 的世界坐標(biāo)。ENU 坐標(biāo)系以原點(diǎn)為參考東方向?yàn)?X 軸北方向?yàn)?Y 軸天方向?yàn)?Z 軸。這個(gè)映射關(guān)系是線性的所以轉(zhuǎn)換效率很高。但要注意ENU 坐標(biāo)系是局部的只在小范圍內(nèi)近似平面大范圍下仍然有曲率影響。原點(diǎn)重定位帶來的一個(gè)直接后果是同一個(gè)地理坐標(biāo)在不同原點(diǎn)下轉(zhuǎn)換出的 UE 坐標(biāo)是不同的。這解釋了為什么你移動(dòng)相機(jī)后某些物體的 UE 坐標(biāo)會(huì)變化。如果你在代碼里緩存了 UE 坐標(biāo)然后移動(dòng)了原點(diǎn)緩存就會(huì)失效。我踩過這個(gè)坑在一個(gè)飛行模擬項(xiàng)目里我緩存了航路點(diǎn)的 UE 坐標(biāo)結(jié)果飛機(jī)飛到一半原點(diǎn)切換了飛機(jī)突然跳了一下。后來改成每次需要時(shí)重新轉(zhuǎn)換或者監(jiān)聽原點(diǎn)變化事件來更新緩存問題才解決。3.2 原點(diǎn)是怎么選出來的Cesium for UE4 默認(rèn)會(huì)根據(jù)相機(jī)的視點(diǎn)來自動(dòng)選擇原點(diǎn)。具體策略是取相機(jī)前方一定距離處的點(diǎn)或者取場(chǎng)景中某個(gè)關(guān)鍵對(duì)象的經(jīng)緯度作為原點(diǎn)。這個(gè)策略在大多數(shù)情況下工作良好但在某些場(chǎng)景下會(huì)出問題。比如相機(jī)快速移動(dòng)時(shí)原點(diǎn)頻繁切換導(dǎo)致物體抖動(dòng)。或者相機(jī)看向天空時(shí)前方?jīng)]有地面點(diǎn)原點(diǎn)選擇可能不穩(wěn)定。在這些情況下手動(dòng)指定原點(diǎn)會(huì)更可靠。手動(dòng)指定原點(diǎn)的方法是通過CesiumGeoreference組件設(shè)置 Origin Longitude、Origin Latitude 和 Origin Height。這三個(gè)值決定了 ENU 坐標(biāo)系的原點(diǎn)位置。設(shè)置好之后所有坐標(biāo)轉(zhuǎn)換都基于這個(gè)固定原點(diǎn)不再動(dòng)態(tài)調(diào)整。這樣做的好處是穩(wěn)定壞處是如果場(chǎng)景范圍很大遠(yuǎn)離原點(diǎn)的區(qū)域精度會(huì)下降。所以手動(dòng)原點(diǎn)適合場(chǎng)景范圍有限、精度要求高的項(xiàng)目比如單個(gè)建筑群或小范圍地形。我通常的做法是先讓自動(dòng)原點(diǎn)跑起來觀察場(chǎng)景中是否有抖動(dòng)或跳變。如果有再切換到手動(dòng)原點(diǎn)把原點(diǎn)設(shè)在場(chǎng)景中心或主要關(guān)注區(qū)域。設(shè)置原點(diǎn)時(shí)高度不要設(shè)為零最好設(shè)在實(shí)際地形高度附近這樣可以減少垂直方向的偏移。另外原點(diǎn)一旦設(shè)定就不要頻繁修改因?yàn)樾薷脑c(diǎn)會(huì)導(dǎo)致所有已轉(zhuǎn)換的坐標(biāo)失效。如果確實(shí)需要修改要確保所有依賴坐標(biāo)的組件都能正確響應(yīng)。3.3 原點(diǎn)切換時(shí)的平滑處理即使使用自動(dòng)原點(diǎn)Cesium for UE4 也會(huì)盡量平滑地切換原點(diǎn)避免突兀的跳變。它通過一個(gè)過渡機(jī)制在舊原點(diǎn)和新原點(diǎn)之間做插值讓物體看起來是連續(xù)移動(dòng)的。但這個(gè)平滑處理不是萬能的如果原點(diǎn)切換幅度太大或者物體本身在快速移動(dòng)仍然可能出現(xiàn)視覺上的異常。我在調(diào)試一個(gè)地面車輛模擬時(shí)發(fā)現(xiàn)車輛在原點(diǎn)切換時(shí)會(huì)輕微下沉后來發(fā)現(xiàn)是因?yàn)檐囕v的高度計(jì)算依賴于原點(diǎn)高度原點(diǎn)切換時(shí)高度參考變了。要減少這類問題可以在原點(diǎn)切換時(shí)暫停物理模擬或者對(duì)關(guān)鍵物體的坐標(biāo)做額外補(bǔ)償。Cesium for UE4 提供了一些事件和委托可以監(jiān)聽原點(diǎn)變化。我通常會(huì)在原點(diǎn)變化時(shí)重新計(jì)算所有動(dòng)態(tài)物體的位置而不是依賴引擎的自動(dòng)更新。這樣做雖然多寫一些代碼但能保證行為可控。另外如果場(chǎng)景中有多個(gè)相機(jī)或視口原點(diǎn)選擇可能會(huì)沖突這時(shí)候最好統(tǒng)一使用一個(gè)主相機(jī)來決定原點(diǎn)其他相機(jī)通過相對(duì)坐標(biāo)來定位。還有一個(gè)細(xì)節(jié)原點(diǎn)重定位不僅影響位置還影響旋轉(zhuǎn)。ENU 坐標(biāo)系的軸向和 UE 世界坐標(biāo)系的軸向不是簡(jiǎn)單對(duì)齊的需要根據(jù)原點(diǎn)的經(jīng)緯度計(jì)算旋轉(zhuǎn)矩陣。這個(gè)旋轉(zhuǎn)矩陣把東-北-天方向映射到 UE 的 X-Y-Z 方向。如果你手動(dòng)設(shè)置物體的旋轉(zhuǎn)要注意這個(gè)映射關(guān)系。我見過有人直接把地理方向的角度賦給 UE 的旋轉(zhuǎn)結(jié)果方向完全不對(duì)。正確的做法是先計(jì)算 ENU 坐標(biāo)系下的方向向量再通過旋轉(zhuǎn)矩陣轉(zhuǎn)換到 UE 世界坐標(biāo)。4. 從 ENU 到 UE 世界坐標(biāo)最后的落地4.1 ENU 坐標(biāo)系的定義與軸向ENU 坐標(biāo)系以原點(diǎn)為基準(zhǔn)東方向?yàn)?X 軸北方向?yàn)?Y 軸天方向?yàn)?Z 軸。這個(gè)定義和 UE 的世界坐標(biāo)系有一個(gè)根本差異UE 的 X 軸是前Y 軸是右Z 軸是上。所以從 ENU 到 UE 的轉(zhuǎn)換不是簡(jiǎn)單的軸交換而是涉及一個(gè)旋轉(zhuǎn)。具體來說ENU 的東方向?qū)?yīng) UE 的哪個(gè)方向取決于原點(diǎn)的經(jīng)緯度。在赤道附近東方向大致對(duì)應(yīng) UE 的 Y 軸北方向大致對(duì)應(yīng) UE 的 X 軸。但在其他緯度這個(gè)對(duì)應(yīng)關(guān)系會(huì)旋轉(zhuǎn)。Cesium for UE4 內(nèi)部使用一個(gè)旋轉(zhuǎn)矩陣來處理這個(gè)轉(zhuǎn)換。這個(gè)矩陣由原點(diǎn)的經(jīng)度和緯度決定。經(jīng)度決定了東方向相對(duì)于 UE 坐標(biāo)系的旋轉(zhuǎn)緯度決定了天方向相對(duì)于 UE 坐標(biāo)系的傾斜。如果你需要自己實(shí)現(xiàn)轉(zhuǎn)換可以按照以下步驟先把 ECEF 坐標(biāo)減去原點(diǎn) ECEF 坐標(biāo)得到相對(duì)向量然后把這個(gè)向量投影到 ENU 三個(gè)軸上得到 ENU 坐標(biāo)最后通過旋轉(zhuǎn)矩陣把 ENU 坐標(biāo)轉(zhuǎn)換成 UE 坐標(biāo)。這個(gè)過程中單位始終是米不需要額外縮放因?yàn)?UE 默認(rèn)單位也是厘米但 Cesium 的 API 通常會(huì)處理好單位轉(zhuǎn)換。我建議在實(shí)際項(xiàng)目中盡量使用 Cesium for UE4 提供的轉(zhuǎn)換函數(shù)而不是自己實(shí)現(xiàn)。因?yàn)樽约簩?shí)現(xiàn)容易忽略一些細(xì)節(jié)比如橢球參數(shù)、單位轉(zhuǎn)換、旋轉(zhuǎn)矩陣的符號(hào)等。但理解這些細(xì)節(jié)仍然很重要因?yàn)楫?dāng)轉(zhuǎn)換結(jié)果不符合預(yù)期時(shí)你需要知道從哪里入手排查。我通常會(huì)在關(guān)鍵位置打印出經(jīng)緯高、ECEF、ENU 和 UE 坐標(biāo)對(duì)比每一步的結(jié)果這樣能快速定位問題出在哪一層。4.2 實(shí)際轉(zhuǎn)換中的單位與縮放虛幻引擎的世界坐標(biāo)默認(rèn)單位是厘米而 Cesium 的坐標(biāo)單位是米。這意味著從 Cesium 轉(zhuǎn)換到 UE 時(shí)需要乘以 100。這個(gè)縮放看起來簡(jiǎn)單但容易出錯(cuò)。比如你在計(jì)算速度或加速度時(shí)如果忘記縮放結(jié)果會(huì)差兩個(gè)數(shù)量級(jí)。我在一個(gè)飛行軌跡項(xiàng)目中就因?yàn)樗俣葐挝粵]轉(zhuǎn)換飛機(jī)飛得像蝸牛一樣慢。后來檢查代碼發(fā)現(xiàn)速度是從 Cesium 的米每秒直接賦給 UE 的厘米每秒少了 100 倍。除了長(zhǎng)度單位角度單位也要注意。UE 的旋轉(zhuǎn)通常用度而 Cesium 的 API 可能用弧度。轉(zhuǎn)換時(shí)要注意。另外UE 的旋轉(zhuǎn)順序和 Cesium 的旋轉(zhuǎn)表示也可能不同。Cesium 常用四元數(shù)表示旋轉(zhuǎn)UE 也支持四元數(shù)但兩者的坐標(biāo)系定義不同所以不能直接復(fù)制。我通常會(huì)把 Cesium 的四元數(shù)轉(zhuǎn)換成旋轉(zhuǎn)矩陣再根據(jù) ENU 到 UE 的旋轉(zhuǎn)矩陣做復(fù)合最后再轉(zhuǎn)成 UE 的四元數(shù)。這個(gè)過程比較繁瑣但能保證正確性。還有一個(gè)容易忽略的點(diǎn)UE 的坐標(biāo)系是左手系而 ECEF 和 ENU 通常是右手系。這個(gè)手性差異會(huì)導(dǎo)致旋轉(zhuǎn)方向相反。比如在 ENU 中逆時(shí)針旋轉(zhuǎn)在 UE 中可能變成順時(shí)針。處理這個(gè)問題的方法是在轉(zhuǎn)換矩陣中引入一個(gè)負(fù)號(hào)或者調(diào)整旋轉(zhuǎn)順序。我剛開始的時(shí)候因?yàn)闆]注意手性導(dǎo)致相機(jī)的朝向總是反的。后來在旋轉(zhuǎn)矩陣?yán)锛恿艘粋€(gè)軸翻轉(zhuǎn)問題才解決。這個(gè)坑很隱蔽因?yàn)槲恢棉D(zhuǎn)換通常不受手性影響只有旋轉(zhuǎn)才會(huì)暴露。4.3 轉(zhuǎn)換函數(shù)的封裝與調(diào)試技巧在實(shí)際項(xiàng)目中我會(huì)把坐標(biāo)系轉(zhuǎn)換相關(guān)的邏輯封裝成幾個(gè)函數(shù)GeographicToECEF、ECEFToENU、ENUToUnreal以及它們的逆函數(shù)。每個(gè)函數(shù)都加上詳細(xì)的注釋說明輸入輸出的單位、坐標(biāo)系定義和參考橢球。這樣團(tuán)隊(duì)里其他人用的時(shí)候不容易搞混。我還會(huì)寫一個(gè)簡(jiǎn)單的測(cè)試用例用幾個(gè)已知點(diǎn)驗(yàn)證轉(zhuǎn)換的正確性。比如取一個(gè)經(jīng)緯度手動(dòng)計(jì)算 ECEF再和函數(shù)結(jié)果對(duì)比。這個(gè)測(cè)試用例在每次修改轉(zhuǎn)換邏輯后都跑一遍能快速發(fā)現(xiàn)回歸問題。調(diào)試坐標(biāo)系問題時(shí)可視化是關(guān)鍵。我會(huì)在場(chǎng)景中放置一些標(biāo)記點(diǎn)用不同的顏色表示不同坐標(biāo)系的轉(zhuǎn)換結(jié)果。比如紅色表示直接使用經(jīng)緯度轉(zhuǎn)換的結(jié)果綠色表示經(jīng)過 ECEF 轉(zhuǎn)換的結(jié)果藍(lán)色表示最終 UE 坐標(biāo)。如果紅色和綠色重合說明地理到 ECEF 的轉(zhuǎn)換正確如果綠色和藍(lán)色重合說明 ECEF 到 UE 的轉(zhuǎn)換正確。這樣一步步縮小范圍能快速定位問題。另外我會(huì)在屏幕上打印出關(guān)鍵數(shù)值比如原點(diǎn)的經(jīng)緯高、當(dāng)前相機(jī)的經(jīng)緯高、某個(gè)物體的 UE 坐標(biāo)等。這些數(shù)值在排查時(shí)非常有用。還有一個(gè)技巧使用 Cesium for UE4 的調(diào)試工具。插件提供了一些調(diào)試選項(xiàng)比如顯示原點(diǎn)位置、顯示坐標(biāo)軸等。打開這些選項(xiàng)能直觀地看到原點(diǎn)在哪里、坐標(biāo)軸朝向如何。我通常會(huì)在項(xiàng)目初期就打開這些調(diào)試選項(xiàng)直到確認(rèn)坐標(biāo)系轉(zhuǎn)換沒問題后再關(guān)閉。這樣能避免很多“想當(dāng)然”的錯(cuò)誤。比如我曾經(jīng)以為原點(diǎn)在場(chǎng)景中心結(jié)果調(diào)試顯示原點(diǎn)在相機(jī)前方幾公里處導(dǎo)致我之前的計(jì)算全錯(cuò)了。從那以后我養(yǎng)成了先看調(diào)試信息再動(dòng)手的習(xí)慣。5. 常見坐標(biāo)系轉(zhuǎn)換問題與排查鏈路5.1 模型位置偏移幾百米從數(shù)據(jù)源開始查模型位置偏移是坐標(biāo)系轉(zhuǎn)換中最常見的問題。表現(xiàn)是模型加載后位置和預(yù)期不符可能偏移幾十米到幾公里。排查這類問題我通常從數(shù)據(jù)源開始。先確認(rèn)數(shù)據(jù)源的坐標(biāo)系是什么是 WGS84 還是其他橢球是經(jīng)緯高還是投影坐標(biāo)。如果數(shù)據(jù)源是投影坐標(biāo)比如 UTM需要先轉(zhuǎn)換成經(jīng)緯高再交給 Cesium。這一步經(jīng)常被忽略因?yàn)橛行?shù)據(jù)看起來是經(jīng)緯度但實(shí)際上是投影坐標(biāo)的某種變體。確認(rèn)數(shù)據(jù)源坐標(biāo)系后檢查轉(zhuǎn)換參數(shù)。比如如果數(shù)據(jù)源用的是 CGCS2000雖然和 WGS84 差異很小但在大范圍場(chǎng)景下仍然可能累積出偏差。我遇到過一個(gè)案例數(shù)據(jù)源是 CGCS2000直接當(dāng) WGS84 用結(jié)果在場(chǎng)景邊緣出現(xiàn)了幾十米的偏移。后來加上了橢球轉(zhuǎn)換參數(shù)問題才解決。另外如果數(shù)據(jù)源有高程異常也要處理。高程異常是橢球高和海拔高的差異不同地區(qū)數(shù)值不同。如果忽略這個(gè)差異垂直方向會(huì)有偏差。排查時(shí)我會(huì)先用一個(gè)已知點(diǎn)驗(yàn)證。比如取數(shù)據(jù)源中的一個(gè)特征點(diǎn)手動(dòng)計(jì)算它的 UE 坐標(biāo)然后和實(shí)際加載后的位置對(duì)比。如果偏差很大說明轉(zhuǎn)換鏈路有問題。然后逐步檢查每一步經(jīng)緯高轉(zhuǎn) ECEF 是否正確ECEF 轉(zhuǎn) ENU 是否正確ENU 轉(zhuǎn) UE 是否正確。我通常會(huì)在每一步打印出坐標(biāo)值和手動(dòng)計(jì)算的結(jié)果對(duì)比。這樣能快速定位是哪一步出了問題。如果每一步都正確但最終結(jié)果還是偏那可能是原點(diǎn)設(shè)置的問題。檢查原點(diǎn)的經(jīng)緯高是否和數(shù)據(jù)源匹配如果不匹配調(diào)整原點(diǎn)。5.2 相機(jī)飛行時(shí)抖動(dòng)原點(diǎn)切換的副作用相機(jī)飛行時(shí)抖動(dòng)尤其是在快速移動(dòng)或跨越較大范圍時(shí)通常是原點(diǎn)切換引起的。原點(diǎn)切換時(shí)所有物體的 UE 坐標(biāo)都會(huì)重新計(jì)算如果處理不當(dāng)就會(huì)出現(xiàn)視覺上的跳變。Cesium for UE4 雖然做了平滑處理但在某些情況下仍然不夠。比如相機(jī)快速移動(dòng)時(shí)原點(diǎn)可能頻繁切換導(dǎo)致物體持續(xù)抖動(dòng)?;蛘呦鄼C(jī)看向天空時(shí)原點(diǎn)選擇不穩(wěn)定導(dǎo)致物體位置漂移。解決這個(gè)問題的方法有幾種。一是手動(dòng)指定原點(diǎn)避免自動(dòng)切換。如果場(chǎng)景范圍有限手動(dòng)原點(diǎn)是最穩(wěn)定的方案。二是調(diào)整原點(diǎn)切換的閾值讓切換不那么頻繁。Cesium for UE4 提供了一些參數(shù)來控制原點(diǎn)切換的行為比如切換距離閾值。增大閾值可以減少切換頻率但會(huì)犧牲遠(yuǎn)離原點(diǎn)區(qū)域的精度。三是使用相對(duì)坐標(biāo)把動(dòng)態(tài)物體掛在一個(gè)相對(duì)原點(diǎn)的父節(jié)點(diǎn)下這樣原點(diǎn)切換時(shí)相對(duì)坐標(biāo)不變物體不會(huì)跳變。我通常會(huì)把相機(jī)和主要?jiǎng)討B(tài)物體放在一個(gè)相對(duì)原點(diǎn)的坐標(biāo)系下這樣即使原點(diǎn)切換它們的位置也是連續(xù)的。還有一個(gè)技巧在原點(diǎn)切換時(shí)暫停物理模擬或動(dòng)畫更新等切換完成后再恢復(fù)。這樣可以避免切換過程中的計(jì)算錯(cuò)誤。Cesium for UE4 提供了原點(diǎn)變化的事件可以監(jiān)聽這個(gè)事件在事件觸發(fā)時(shí)暫停和恢復(fù)。我試過這個(gè)方法效果不錯(cuò)但要注意暫停時(shí)間不能太長(zhǎng)否則會(huì)影響用戶體驗(yàn)。通常原點(diǎn)切換很快暫停幾毫秒就夠了。5.3 旋轉(zhuǎn)方向反了手性差異的坑旋轉(zhuǎn)方向反了是坐標(biāo)系轉(zhuǎn)換中比較隱蔽的問題。表現(xiàn)是物體的朝向和預(yù)期相反或者相機(jī)的旋轉(zhuǎn)方向不對(duì)。這個(gè)問題的根源是 ECEF 和 ENU 通常是右手系而 UE 是左手系。手性差異導(dǎo)致旋轉(zhuǎn)矩陣的行列式為負(fù)從而翻轉(zhuǎn)了旋轉(zhuǎn)方向。解決方法是調(diào)整旋轉(zhuǎn)矩陣或者在轉(zhuǎn)換時(shí)對(duì)某個(gè)軸取反。我遇到這個(gè)問題時(shí)一開始以為是旋轉(zhuǎn)順序的問題試了各種旋轉(zhuǎn)順序都不對(duì)。后來才想到手性差異。具體來說ENU 的東-北-天是右手系UE 的 X-Y-Z 是左手系。如果直接把 ENU 的旋轉(zhuǎn)矩陣賦給 UE旋轉(zhuǎn)方向就會(huì)反。正確的做法是在旋轉(zhuǎn)矩陣中引入一個(gè)軸翻轉(zhuǎn)比如把 Y 軸取反或者調(diào)整旋轉(zhuǎn)順序。我通常會(huì)把 ENU 的旋轉(zhuǎn)矩陣轉(zhuǎn)換成 UE 的旋轉(zhuǎn)矩陣時(shí)對(duì) Y 軸取反這樣手性就一致了。除了手性還要注意旋轉(zhuǎn)的參考系。Cesium 的旋轉(zhuǎn)通常是相對(duì)于 ECEF 或 ENU 的而 UE 的旋轉(zhuǎn)是相對(duì)于世界坐標(biāo)的。所以不能直接把 Cesium 的旋轉(zhuǎn)賦給 UE需要先轉(zhuǎn)換到 UE 的世界坐標(biāo)。我通常會(huì)把 Cesium 的四元數(shù)轉(zhuǎn)換成旋轉(zhuǎn)矩陣然后乘以 ENU 到 UE 的旋轉(zhuǎn)矩陣再轉(zhuǎn)成 UE 的四元數(shù)。這個(gè)過程雖然繁瑣但能保證旋轉(zhuǎn)正確。如果旋轉(zhuǎn)仍然不對(duì)檢查旋轉(zhuǎn)矩陣的乘法順序矩陣乘法不滿足交換律順序錯(cuò)了結(jié)果就錯(cuò)了。5.4 高度不對(duì)橢球高與海拔高的混淆高度不對(duì)是另一個(gè)常見問題。表現(xiàn)是模型在地面之上或之下或者地形和模型之間的垂直關(guān)系不對(duì)。這個(gè)問題的根源通常是橢球高和海拔高的混淆。Cesium for UE4 使用的是橢球高而很多數(shù)據(jù)源提供的是海拔高。兩者之間差了一個(gè)大地水準(zhǔn)面差距這個(gè)差距在不同地區(qū)可能達(dá)到幾十米。如果忽略這個(gè)差異垂直方向就會(huì)有偏差。解決方法是把海拔高轉(zhuǎn)換成橢球高。這需要一個(gè)大地水準(zhǔn)面模型比如 EGM96。Cesium for UE4 可能沒有內(nèi)置這個(gè)轉(zhuǎn)換需要自己處理。我通常會(huì)在數(shù)據(jù)預(yù)處理階段把海拔高轉(zhuǎn)換成橢球高然后再交給 Cesium。轉(zhuǎn)換的方法是用大地水準(zhǔn)面模型計(jì)算差距然后加到海拔高上。這個(gè)差距是經(jīng)緯度的函數(shù)所以需要查表或使用近似公式。如果精度要求不高可以用一個(gè)平均差距值但這樣在不同地區(qū)會(huì)有誤差。排查高度問題時(shí)我會(huì)先確認(rèn)數(shù)據(jù)源的高度類型。如果是海拔高就做轉(zhuǎn)換如果是橢球高就直接用。然后檢查原點(diǎn)的原點(diǎn)高度設(shè)置。原點(diǎn)高度應(yīng)該是橢球高而不是海拔高。如果原點(diǎn)高度設(shè)錯(cuò)了所有物體的垂直位置都會(huì)偏。我通常會(huì)把原點(diǎn)高度設(shè)在實(shí)際地形高度附近這樣垂直方向的偏移最小。另外如果場(chǎng)景中有多個(gè)數(shù)據(jù)源要確保它們的高度類型一致否則會(huì)出現(xiàn)有的模型在地面上有的在地下。6. 把坐標(biāo)系轉(zhuǎn)換用進(jìn)實(shí)際項(xiàng)目幾個(gè)經(jīng)驗(yàn)之談6.1 項(xiàng)目初期就統(tǒng)一坐標(biāo)規(guī)范我在多個(gè)地理空間項(xiàng)目里最大的體會(huì)是項(xiàng)目初期就要統(tǒng)一坐標(biāo)規(guī)范。具體來說確定一個(gè)標(biāo)準(zhǔn)的地理坐標(biāo)系通常是 WGS84確定一個(gè)標(biāo)準(zhǔn)的高度類型橢球高或海拔高確定一個(gè)標(biāo)準(zhǔn)的單位米或厘米然后所有數(shù)據(jù)都往這個(gè)標(biāo)準(zhǔn)上靠。這個(gè)工作看起來簡(jiǎn)單但能省掉后期無數(shù)次的轉(zhuǎn)換和排查。我見過一個(gè)項(xiàng)目因?yàn)椴煌瑘F(tuán)隊(duì)用了不同的坐標(biāo)系最后整合時(shí)花了大量時(shí)間做坐標(biāo)對(duì)齊還出現(xiàn)了無法消除的偏差。統(tǒng)一坐標(biāo)規(guī)范還包括確定原點(diǎn)的策略。是使用自動(dòng)原點(diǎn)還是手動(dòng)原點(diǎn)原點(diǎn)的經(jīng)緯高怎么定這些都要在項(xiàng)目初期確定。如果項(xiàng)目范圍小手動(dòng)原點(diǎn)更穩(wěn)定如果范圍大自動(dòng)原點(diǎn)更靈活。我通常會(huì)在項(xiàng)目初期做一個(gè)測(cè)試場(chǎng)景加載一些已知數(shù)據(jù)觀察原點(diǎn)切換的行為然后決定用哪種策略。這個(gè)測(cè)試場(chǎng)景不需要很復(fù)雜幾個(gè)標(biāo)記點(diǎn)加一個(gè)相機(jī)就夠了。另外文檔化也很重要。把坐標(biāo)規(guī)范寫進(jìn)項(xiàng)目文檔包括坐標(biāo)系定義、單位、原點(diǎn)策略、轉(zhuǎn)換函數(shù)的使用方法等。這樣新加入的團(tuán)隊(duì)成員能快速上手不會(huì)因?yàn)樽鴺?biāo)系問題浪費(fèi)時(shí)間。我通常會(huì)在代碼倉(cāng)庫(kù)里放一個(gè)CoordinateSystem.md文件詳細(xì)說明這些內(nèi)容。這個(gè)文件在項(xiàng)目后期排查問題時(shí)也非常有用因?yàn)榭梢钥焖倩仡櫘?dāng)初的決策。6.2 轉(zhuǎn)換函數(shù)的性能優(yōu)化坐標(biāo)系轉(zhuǎn)換在實(shí)時(shí)應(yīng)用中可能被頻繁調(diào)用比如每幀更新相機(jī)位置或物體位置。如果轉(zhuǎn)換函數(shù)效率低會(huì)影響幀率。Cesium for UE4 的轉(zhuǎn)換函數(shù)已經(jīng)做了優(yōu)化但如果自己實(shí)現(xiàn)要注意避免重復(fù)計(jì)算。比如原點(diǎn)的 ECEF 坐標(biāo)和旋轉(zhuǎn)矩陣可以緩存起來不需要每次轉(zhuǎn)換都重新計(jì)算。我通常會(huì)把原點(diǎn)相關(guān)的數(shù)據(jù)緩存在一個(gè)結(jié)構(gòu)體里轉(zhuǎn)換時(shí)直接使用。另外如果需要對(duì)大量點(diǎn)做轉(zhuǎn)換可以考慮批量處理。比如把多個(gè)經(jīng)緯度點(diǎn)一次性轉(zhuǎn)換成 UE 坐標(biāo)而不是逐個(gè)轉(zhuǎn)換。批量處理可以減少函數(shù)調(diào)用開銷也能利用 SIMD 指令加速。Cesium for UE4 可能沒有提供批量轉(zhuǎn)換的 API但可以自己實(shí)現(xiàn)。我通常會(huì)把點(diǎn)集組織成數(shù)組然后在一個(gè)循環(huán)里轉(zhuǎn)換同時(shí)緩存中間結(jié)果。如果點(diǎn)集很大還可以考慮多線程處理但要注意線程安全。還有一個(gè)優(yōu)化點(diǎn)是減少不必要的轉(zhuǎn)換。比如如果物體的位置在短時(shí)間內(nèi)不變就不需要每幀重新轉(zhuǎn)換。可以緩存轉(zhuǎn)換結(jié)果只在原點(diǎn)變化或物體移動(dòng)時(shí)更新。我通常會(huì)給每個(gè)需要轉(zhuǎn)換的物體加一個(gè)臟標(biāo)記只有標(biāo)記為臟時(shí)才重新轉(zhuǎn)換。這樣能顯著減少轉(zhuǎn)換次數(shù)尤其是在場(chǎng)景中有大量靜態(tài)物體時(shí)。6.3 與其他系統(tǒng)的坐標(biāo)對(duì)接地理空間項(xiàng)目往往需要和其他系統(tǒng)對(duì)接比如 GIS 系統(tǒng)、BIM 系統(tǒng)、仿真系統(tǒng)等。這些系統(tǒng)可能使用不同的坐標(biāo)系對(duì)接時(shí)要做轉(zhuǎn)換。我通常會(huì)在對(duì)接層做一個(gè)坐標(biāo)適配器把外部系統(tǒng)的坐標(biāo)轉(zhuǎn)換成項(xiàng)目?jī)?nèi)部的標(biāo)準(zhǔn)坐標(biāo)。這個(gè)適配器要處理坐標(biāo)系差異、單位差異、高度類型差異等。適配器的接口要簡(jiǎn)單明了輸入輸出都明確標(biāo)注坐標(biāo)系和單位。對(duì)接時(shí)最容易出問題的是旋轉(zhuǎn)和朝向。不同系統(tǒng)對(duì)旋轉(zhuǎn)的定義可能不同比如有的用歐拉角有的用四元數(shù)有的用旋轉(zhuǎn)矩陣。而且旋轉(zhuǎn)的參考系也可能不同。我通常會(huì)把所有旋轉(zhuǎn)統(tǒng)一成四元數(shù)然后根據(jù)參考系做轉(zhuǎn)換。如果外部系統(tǒng)用的是歐拉角要先轉(zhuǎn)成四元數(shù)再轉(zhuǎn)成項(xiàng)目?jī)?nèi)部的表示。這個(gè)過程中要注意旋轉(zhuǎn)順序和手性。另外時(shí)間同步也很重要。如果外部系統(tǒng)是動(dòng)態(tài)的比如實(shí)時(shí)仿真坐標(biāo)轉(zhuǎn)換要考慮時(shí)間戳。不同系統(tǒng)的時(shí)鐘可能不同步導(dǎo)致坐標(biāo)在時(shí)間上不對(duì)齊。我通常會(huì)在對(duì)接層加一個(gè)時(shí)間同步機(jī)制確保坐標(biāo)轉(zhuǎn)換基于同一時(shí)刻。這個(gè)機(jī)制可以簡(jiǎn)單到用一個(gè)共享的時(shí)間戳也可以復(fù)雜到用網(wǎng)絡(luò)時(shí)間協(xié)議。具體取決于項(xiàng)目需求。6.4 調(diào)試工具與可視化的重要性最后再?gòu)?qiáng)調(diào)一下調(diào)試工具和可視化的重要性。坐標(biāo)系轉(zhuǎn)換的問題往往很隱蔽光看代碼很難發(fā)現(xiàn)。我通常會(huì)在場(chǎng)景中放置坐標(biāo)軸標(biāo)記顯示原點(diǎn)的位置和朝向。還會(huì)用不同顏色的線連接不同坐標(biāo)系的轉(zhuǎn)換結(jié)果直觀地看到偏差。Cesium for UE4 提供了一些調(diào)試選項(xiàng)比如顯示原點(diǎn)、顯示坐標(biāo)軸這些都要充分利用。另外我會(huì)寫一個(gè)簡(jiǎn)單的坐標(biāo)轉(zhuǎn)換測(cè)試工具輸入經(jīng)緯高輸出 ECEF、ENU 和 UE 坐標(biāo)并和手動(dòng)計(jì)算的結(jié)果對(duì)比。這個(gè)工具在開發(fā)過程中隨時(shí)可用能快速驗(yàn)證轉(zhuǎn)換邏輯。我還會(huì)把常見的轉(zhuǎn)換問題整理成檢查清單比如“高度類型是否正確”“原點(diǎn)是否匹配”“手性是否處理”等排查時(shí)逐項(xiàng)檢查。這個(gè)清單在團(tuán)隊(duì)里共享能提高排查效率??梢暬矫嫖疫€會(huì)用一些第三方工具比如把 UE 坐標(biāo)導(dǎo)出成 CSV然后在外部工具里繪制和原始地理數(shù)據(jù)對(duì)比。這樣能從宏觀上看到偏差的分布和趨勢(shì)。如果偏差是系統(tǒng)性的比如所有點(diǎn)都偏同一個(gè)方向那可能是原點(diǎn)或旋轉(zhuǎn)矩陣的問題如果偏差是隨機(jī)的那可能是數(shù)據(jù)源或精度的問題。通過可視化能快速判斷問題的性質(zhì)從而選擇正確的排查方向。