3dTiles 幾何誤差詳解【轉】
3dTiles 幾何誤差詳解
轉載請註明出處。全網@秋意正寒
1. 瓦片的排程
查閱tileset.json
的規範,有一個屬性是refine
,它有兩個值:"ADD"
和"REPLACE"
。
還有另一個屬性,叫geometricError
,是一個數字。
"ADD"
的含義是,當這一級瓦片顯示不夠精細時,渲染下一級瓦片,這一級的瓦片保留繼續顯示(增加下一級的內容)。
"REPLACE"
的含義是,當這一級瓦片顯示不夠精細時,渲染下一級瓦片,這一級的瓦片被銷燬(被下一級“替換”)。
如何衡量這個“不夠精細”?
一個很簡單的思路是利用觀察點(也就是相機)到觀察瓦片的距離來判斷。這個相機與瓦片的距離超過我指定的某個閾值的時候,就要渲染下一級瓦片,而這一級瓦片則根據refine
所謂的 “指定的某個閾值”,在這裡有一個專有名詞:最大螢幕空間誤差(maximumScreenSpaceError)。
這個值是Cesium3DTileset
類中的例項屬性,預設值是16.
暫且不說這個16的具體含義,先回顧剛才的思路:計算相機到瓦片的距離,設為distance,就能與這個值進行比較了嗎?不是的。
1.1 螢幕空間誤差(ScreenSpaceError, sse)
計算當前瓦片的螢幕空間誤差值,才能與maximumScreenSpaceError
進行比較,因為這兩個才是同一種東西嘛。
先說結論:螢幕空間誤差(ScreenSpaceError, sse)由幾何誤差、相機狀態有關的各項引數計算而來。
也就是說,只要 Cesium 在跑,這個 sse 就是一幀一幀實時計算的,每時每刻都在計算。
查閱Cesium3DTile
的原始碼,不難得知它的計算方法被定義在Cesium3DTile
中(可以跳過程式碼不看):
// Cesium3DTile.js >> Cesium3DTile.prototype.getScreenSpaceError() Cesium3DTile.prototype.getScreenSpaceError = function ( frameState, useParentGeometricError, progressiveResolutionHeightFraction ) {var tileset = this._tileset; var heightFraction = defaultValue(progressiveResolutionHeightFraction, 1.0); var parentGeometricError = defined(this.parent) ? this.parent.geometricError : tileset._geometricError; var geometricError = useParentGeometricError ? parentGeometricError : this.geometricError; if (geometricError === 0.0) { // Leaf tiles do not have any error so save the computation return 0.0; } var camera = frameState.camera; var frustum = camera.frustum; var context = frameState.context; var width = context.drawingBufferWidth; var height = context.drawingBufferHeight * heightFraction; var error; if ( frameState.mode === SceneMode.SCENE2D || frustum instanceof OrthographicFrustum ) { if (defined(frustum._offCenterFrustum)) { frustum = frustum._offCenterFrustum; } var pixelSize = Math.max(frustum.top - frustum.bottom, frustum.right - frustum.left) / Math.max(width, height); error = geometricError / pixelSize; } else { // Avoid divide by zero when viewer is inside the tile var distance = Math.max(this._distanceToCamera, CesiumMath.EPSILON7); var sseDenominator = camera.frustum.sseDenominator; error = (geometricError * height) / (distance * sseDenominator); if (tileset.dynamicScreenSpaceError) { var density = tileset._dynamicScreenSpaceErrorComputedDensity; var factor = tileset.dynamicScreenSpaceErrorFactor; var dynamicError = CesiumMath.fog(distance, density) * factor; error -= dynamicError; } } error /= frameState.pixelRatio; return error; };
這麼長,其實在我們關心的三維模式(即 frameState.mode 為 SceneMode.SCENE3D)下,最核心的只有一句程式碼:
error = (geometricError * height) / (distance * sseDenominator);
其中,
error
即計算得到的 sse 螢幕空間誤差geometricError
即當前瓦片設定好的幾何誤差,寫在 tileset.json 中height
即瀏覽器當前執行著 Cesium 的那個 canvas 的畫素高度,如果沒有自己設定progressiveResolutionHeightFraction
值,通常height
值就是canvas 的畫素高度,如果你的 Cesium 佔據了全屏,你的顯示器解析度是 1920 × 1080,那麼這個height
在你瀏覽器全屏時,通常是 936 畫素。distance
是當前狀態下,攝像機的世界座標位置到瓦片中心位置的距離,單位是米sseDenominator
是一個根據當前相機狀態下,根據視錐體的張角(fov)、長寬比引數進行一系列三角計算、四則運算而來的一個引數,具體含義我沒有深究,但是通常狀態下,很少會去修改預設相機的引數,即張角 60 度,寬高比就是 1920÷936(就是canvas的畫素寬高比啦),所以這個值也是固定的,有興趣的讀者可以跟蹤這個引數的計算過程,還要往裡套五六層程式碼才知道計算過程。它翻譯過來就是“sse的分母”。
2. 推演
如果不對相機進行修改,使用預設的,而且你的螢幕是1920×1080,恰好你的 canvas 佔滿了 body,而且你的瀏覽器是最大化的狀態,那麼這個sseDenominator
的值約為 0.5629165124598852
顯而易見,為了遮蔽螢幕解析度差異、瀏覽器是否最大化的差異,這個sseDenominator
的值是會根據瀏覽器視窗狀態、canvas大小以及攝像機的狀態進行變化的。在此,我們假定就是 0.5629165124598852
那麼 上述程式碼改寫成:
sse=geometricError×936distance×0.5629165124598852sse=geometricError×936distance×0.5629165124598852是否還記得一個引數:maximumScreenSpaceError
?它的預設值是16
那麼,這個16就是一個臨界值,當sse<defaultMaximumScreenSpaceError=16sse<defaultMaximumScreenSpaceError=16時,下一級瓦片載入,此瓦片根據refine
進行調整。
假設sse
剛好等於16,那麼得到一個二元方程:
所以,這個等式表達的含義就是,當幾何誤差越大(分子變大),要想等式保持相等,那麼分母:distance(相機到瓦片的距離)也應變大,變大就意味著——根據refine
來控制瓦片的顯隱或增補時,此瓦片觀察距離由此變大。
所以,這個幾何誤差是一個經驗值。
下結論:
在幾何誤差、相機狀態是固定值時,只要觀察距離 > 計算此幾何誤差的經驗距離,就會渲染下一級瓦片,此瓦片的refine
規則若是REPLACE
則消失,若是ADD
則保留。
若渲染下一級瓦片,則當前瓦片的 sse 必定 < maximumScreenSpaceError。
3. 經驗值下的幾何誤差計算
還是以剛好到臨界值,也即預設的 16 時,為例。
設定某瓦片距離相機超過200米時,該瓦片到達臨界狀態。
代入上式,計算得到 geometricError 為:
geometricError=200×0.5629165124598852×16÷936=1.9245008972987geometricError=200×0.5629165124598852×16÷936=1.9245008972987那麼現在這個瓦片的 sse 公式變成了:
sse=1.9245008972987×936distance×0.5629165124598852sse=1.9245008972987×936distance×0.5629165124598852也就是距離越大,sse 越小。當距離超過200米,sse一定小於16,不妨設refine
為REPLACE
。
在檢視中觀察到小於200米時,此瓦片正常顯示,距離一旦大於200米,該瓦片就被REPLACE
了,此時的sse
肯定也小於16,只需調整maximumScreenSpaceError
,在 CesiumLab 中叫顯示精度,調小一些,該瓦片又被顯示了。
不妨假設就按 1080p螢幕 + 全屏canvas + 最大化瀏覽器視窗 + 預設相機引數來算,列舉常見觀察距離的幾何誤差設定:
觀察距離 | 幾何誤差 |
---|---|
100 | 0.96225045 |
200 | 1.92450090 |
300 | 2.88675134 |
400 | 3.84900179 |
500 | 4.81125224 |
1000 | 9.62250447 |
2000 | 19.24500897 |
觀察不難得知,這是一個一次函式:
geometricError=f(distance)=distance×0.5629165124598852×16÷936geometricError=f(distance)=distance×0.5629165124598852×16÷936而後面三個數字,則與相機、瀏覽器等因素有關,只要瀏覽器不變,相機不變,顯示器不變,那麼無論怎麼操作檢視,幾何誤差的計算都只跟經驗上的觀察距離有關。
4. 調參經驗
4.1. 當前視角如果不繼續放大,傾斜攝影的層級比較低(看起來模糊)怎麼辦
方案① 調整最大螢幕空間誤差
調大 maximumScreenSpaceError,因為幾何誤差不變、距離不變,等式左邊放大,勢必等號要變成大於號:
maximumScreenSpaceError>16=geometricError×936distance×0.5629165124598852maximumScreenSpaceError>16=geometricError×936distance×0.5629165124598852根據上述結論,同等條件下最大螢幕空間誤差變大,導致計算結果小於最大螢幕空間誤差,從而引發下一級瓦片渲染,當前瓦片執行 "refine" 策略。
方案② 根治法:調整幾何誤差
不渲染下一級瓦片的原因無非是當前幾何誤差所代表的經驗距離太大了,不小於這個距離就沒法渲染。
解決方法很簡單,把幾何誤差調小即可,參考上文。
4.2. 想不載入這麼快
同 4.1. 的思路,減小maximumScreenSpaceError
或增大幾何誤差。