在開發 Cesium 程序時,需要快速確定場景中的對象什么時候不可見,從而判斷它不需要渲染。
一種方法是使用視錐體平視剔除,但是還有另一種重要的剔除方法是地平線剔除。

上圖中,綠色點是 viewer 內可見的。紅色點是不可見的,因為它們在視錐體外面(視錐體用粗白線畫出)。藍色點雖然在視錐體內,但是它在地球背面,所以它也是不可見的。換句話說,它在地平線下。
地平線剔除的思路很簡單,即不需要渲染 viewer 視野中地平線之下的東西。聽起來簡單,但是細節挺多的,尤其是要考慮到性能問題(要快速剔除)。Cesium 每次渲染時,為了檢測這些地形瓦片的可見性,就要測試上百次,不過這很重要。
相對於球體的地平線下點剔除
可為所有靜態對象(例如瓦片)計算其范圍球體(boundingSphere)。假設這個范圍球體很小以至於與地球比起來,像一個點,如果這個點在地平線下,那么我們也能說這個瓦片就在地平線下。
當前提出的新算法僅限於對橢球體計算單個點的情況。現在不妨設“遮擋點”已經被計算出來了。
為了說明簡便,先進行正球體的地平線剔除,然后再推廣到橢球體的地平線剔除。
考慮下面這張圖片:

上圖中,藍色的圓是一個單位球面,從 Viewer 向外延申並和單位球面相切的這兩條細黑線表示地平線。
垂直的這根粗黑線表示地平線交單位球面的所有地平點,是一個圓。
從 Viewer 到這個圓上的所有點的向量,就構成了一個圓錐體(包括陰影部分)。
譯者注
想象一下一個漏斗套一個乒乓球,大概就是這個情況
圖中陰影部分表示地平線以下的區域,Viewer 看不到這些區域。換句話說,如果一個點在這個陰影區域,那么這個點就在地平線下。
計算某點位於平面的哪一側
首先,做一個簡單的計算,來算出這個點在垂直黑直線那個圓的圓面的哪一邊:

- V:Viewer的位置
- C:單位球面的中心
- H:地平線切單位球面的點
- T:待計算的目標點
- P:H點投影到VC向量的點
- Q:T點投影到VC向量的點
由勾股定理:
由單位球,易得 \(\vec{HC}\) 向量的長度是1:
易證 \(△VCH\) 和 \(△HCP\) 相似,所以有:
代入 \(||\vec{HC}|| =1\),整理得
所以,Viewer 到平面(下文均用平面簡稱,即地平線與球面相切的所有點的集合構成的圓周代表的面,即圖上垂直黑色粗線)的距離:
如果,\(\vec{VT}\) 在 \(\vec{VC}\) 的投影 \(\vec{VQ}\) 長度小於 \(\vec{VP}\),那么點就在平面內(視錐內)。
換句話說,如果 \(||\vec{VQ}||>||\vec{VP}||\),那么點就在地平線下:
左右均乘以 \(||\vec{VC}||\),即
結論
若想知道目標點位於平面的前面還是后面,只需取 Viewer 到目標點的向量 \(\vec{VT}\)、Viewer 到單位球心的向量 \(\vec{VC}\),求其內積,判斷結果與 Viewer 到單位球心距離與1的差的大小即可。
若大於,則點在地平線下(平面后),反之則在平面前(地平線上)
判斷目標點與圓錐體的關系
仍舊是考慮原來的圖,這次考慮兩個角 \(∠HVC\)(記為α)、\(∠TVC\)(記為β)

當角 β < α 時,目標點 T 就位於圓錐內了。
在 \([0, π]\) 區間上,對於任意的 β > α,有
角 α 是 \(Rt△VCH\) 的一個角,所以:
由余弦的定義,cos(β) 可寫為
即
兩邊同時乘上 \(||\vec{VC}||\) 並同時平方,則
根據上一節的計算結果 \(||\vec{VH}||^2=||\vec{VC}||^2-1\)
最終,得到的不等式關系是:
\(\vec{VT}\) 和 \(\vec{VC}\) 都很容易計算,若上式不等號成立,則說明目標點在視錐內,否則在視錐外。
推廣到橢球體的情況
上述均為單位球的情況,現在推廣到橢球體上。
單位球的方程是:
橢球體的方程是:
其中,a、b、c是三個軸的半長(軸半徑)。
利用縮放矩陣,可以將橢球體上的所有點歸為單位球上的計算:
代碼
作者認為數學推導過程很重要,但是可以歸結成一些簡單的代碼。每當攝像機位置改變時,都要執行
// 橢球的三個軸半徑 此處使用 WGS84橢球體
var rX = 6378137.0;
var rY = 6378137.0;
var rZ = 6356752.3142451793;
// 向量CV,縮放到單位球空間(除以各軸半徑),方便計算
var cvX = cameraPosition.x / rX;
var cvY = cameraPosition.y / rY;
var cvZ = cameraPosition.z / rZ;
// 向量VH長度的平方
var vhMagnitudeSquared = cvX * cvX + cvY * cvY + cvZ * cvZ - 1.0;
然后對於每個點,要進行測試遮擋剔除算法:
// 目標點T,縮放到單位球空間(除以各軸半徑),方便計算
var tX = position.x / rX;
var tY = position.y / rY;
var tZ = position.z / rZ;
// 向量VT
var vtX = tX - cvX;
var vtY = tY - cvY;
var vtZ = tZ - cvZ;
// 向量VT長度的平方
var vtMagnitudeSquared = vtX * vtX + vtY * vtY + vtZ * vtZ;
// VT點乘VC 和 VT點乘CV的相反數是一樣的
var vtDotVc = -(vtX * cvX + vtY * cvY + vtZ * cvZ);
// bool值,前者是判斷是否在平面內,后者判斷是否在錐體內
var isOccluded = vtDotVc > vhMagnitudeSquared && vtDotVc * vtDotVc / vtMagnitudeSquared > vhMagnitudeSquared;
在 Cesium 中,預先進行了單位球空間的縮放,而不是每次測試都縮放。
EllipsoidalOccluder.prototype.isPointVisible = function (occludee) {
var ellipsoid = this._ellipsoid;
var occludeeScaledSpacePosition = ellipsoid.transformPositionToScaledSpace(
occludee,
scratchCartesian
);
return isScaledSpacePointVisible(
occludeeScaledSpacePosition,
this._cameraPositionInScaledSpace,
this._distanceToLimbInScaledSpaceSquared
);
};
展望
與之前使用最小范圍球進行剔除的方法相比,使用這個技術減少大約 15% 的瓦片繪制。
其他就不翻譯了
實際應用
在 Cesium 的私有類 EllipsoidalOccluder (位於Core目錄下)中,就使用了這個算法進行剔除計算。
