轉載請注明出處。全網@秋意正寒
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);
其中,
-
即計算得到的 sse 螢幕空間誤差error
-
即目前瓦片設定好的幾何誤差,寫在 tileset.json 中geometricError
-
即浏覽器目前運作着 Cesium 的那個 canvas 的像素高度,如果沒有自己設定height
值,通常progressiveResolutionHeightFraction
值就是canvas 的像素高度,如果你的 Cesium 占據了全屏,你的顯示器分辨率是 1920 × 1080,那麼這個height
在你浏覽器全屏時,通常是 936 像素。height
-
是目前狀态下,錄影機的世界坐标位置到瓦片中心位置的距離,機關是米distance
-
是一個根據目前相機狀态下,根據視錐體的張角(fov)、長寬比參數進行一系列三角計算、四則運算而來的一個參數,具體含義我沒有深究,但是通常狀态下,很少會去修改預設相機的參數,即張角 60 度,寬高比就是 1920÷936(就是canvas的像素寬高比啦),是以這個值也是固定的,有興趣的讀者可以跟蹤這個參數的計算過程,還要往裡套五六層代碼才知道計算過程。它翻譯過來就是“sse的分母”。sseDenominator
2. 推演
如果不對相機進行修改,使用預設的,而且你的螢幕是1920×1080,恰好你的 canvas 占滿了 body,而且你的浏覽器是最大化的狀态,那麼這個
sseDenominator
的值約為 0.5629165124598852
顯而易見,為了屏蔽螢幕分辨率差異、浏覽器是否最大化的差異,這個
sseDenominator
的值是會根據浏覽器視窗狀态、canvas大小以及錄影機的狀态進行變化的。在此,我們假定就是 0.5629165124598852
那麼 上述代碼改寫成:
\[sse = \frac{geometricError×936}{distance × 0.5629165124598852}
\]
是否還記得一個參數:
maximumScreenSpaceError
?它的預設值是16
那麼,這個16就是一個臨界值,當 \(sse < defaultMaximumScreenSpaceError = 16\) 時,下一級瓦片加載,此瓦片根據
refine
進行調整。
假設
sse
剛好等于16,那麼得到一個二進制方程:
\[16 = \frac{geometricError×936}{distance × 0.5629165124598852}
是以,這個等式表達的含義就是,當幾何誤差越大(分子變大),要想等式保持相等,那麼分母:distance(相機到瓦片的距離)也應變大,變大就意味着——根據
refine
來控制瓦片的顯隐或增補時,此瓦片觀察距離由此變大。
是以,這個幾何誤差是一個經驗值。
下結論:
在幾何誤差、相機狀态是固定值時,隻要觀察距離 > 計算此幾何誤差的經驗距離,就會渲染下一級瓦片,此瓦片的
refine
規則若是
REPLACE
則消失,若是
ADD
則保留。
若渲染下一級瓦片,則目前瓦片的 sse 必定 < maximumScreenSpaceError。
3. 經驗值下的幾何誤差計算
還是以剛好到臨界值,也即預設的 16 時,為例。
設定某瓦片距離相機超過200米時,該瓦片到達臨界狀态。
代入上式,計算得到 geometricError 為:
\[geometricError = 200 × 0.5629165124598852×16÷936=1.9245008972987
那麼現在這個瓦片的 sse 公式變成了:
\[sse = \frac{1.9245008972987×936}{distance×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÷936
而後面三個數字,則與相機、浏覽器等因素有關,隻要浏覽器不變,相機不變,顯示器不變,那麼無論怎麼操作視圖,幾何誤差的計算都隻跟經驗上的觀察距離有關。
4. 調參經驗
4.1. 目前視角如果不繼續放大,傾斜攝影的層級比較低(看起來模糊)怎麼辦
方案① 調整最大螢幕空間誤差
調大 maximumScreenSpaceError,因為幾何誤差不變、距離不變,等式左邊放大,勢必等号要變成大于号:
\[maximumScreenSpaceError > 16 = \frac{geometricError×936}{distance × 0.5629165124598852}
根據上述結論,同等條件下最大螢幕空間誤差變大,導緻計算結果 小于 最大螢幕空間誤差,進而引發下一級瓦片渲染,目前瓦片執行 "refine" 政策。
方案② 根治法:調整幾何誤差
不渲染下一級瓦片的原因無非是目前幾何誤差所代表的經驗距離太大了,不小于這個距離就沒法渲染。
解決方法很簡單,把幾何誤差調小 即可,參考上文。
4.2. 想不加載這麼快
同 4.1. 的思路,減小
maximumScreenSpaceError
或 增大 幾何誤差。