這一篇,接着上一篇,內容集中在高度圖方式構建地球網格的細節方面。
此時,Globe對每一個切片(GlobeSurfaceTile)創建對應的TileTerrain類,用來維護地形切片的相關邏輯;接着,在requestTileGeometry中,TileTerrain會請求對應該切片的地形數據。如果讀者對這部分有疑問的話,可以閱讀《Cesium原理篇:1最長的一幀之渲染調度》;最后,如果你是采用的高度圖的地形服務,地形數據對應的是HeightmapTerrainData類,最終,該TerrainData形成了一個TerrainMesh網格。下面,我們就詳細的介紹一下最后一步的相關內容。
高度圖
首先,怎么理解高度圖?通常一個Tile都會對應一個256*256的影像切片,代表該Tile對應的XYZ范圍下對應的影像內容,高度圖也是一樣的思路,只是此時,前者每一個像素代表當前位置對應的顏色,而后者代表當前位置對應的高度
一般情況下,高度圖是一個縮略圖,比如在Cesium中,在沒有真實地形數據下,高度圖的寬高是16*16大小,每個點對應的值都是0,在有真實地形數據下,高度圖是65*65的大小。可見,高度圖是有抽稀的,而不是一一對應,一來沒必要,不然就是點雲了,二來構網的計算量很大,也是效果和效率的一個折中。
Workers線程
Cesium的調度是基於狀態的變化,看似簡單,但個人認為非常精髓的。相比基於事件驅動的策略,基於狀態可以更好的實時的處理大數據,邏輯上也簡單清晰,當然這是題外話,我們繼續回到地形本身。
有了數據,TileTerrain的狀態由RECEIVING變為RECEIVED,自然也就進入了下一環節transform:將原始的地形數據(HeightmapTerrainData)轉換為格網(TerrainMesh)的過程。
這個過程涉及到不小的計算量,因此,Cesium采用Promise + Workers技術,把計算量放到線程中,這樣保證界面操作的流暢。對Workers感興趣的可以參考《Cesium原理篇:4Web Workers剖析》。
HeightmapTerrainData.prototype.createMesh方法提供了構建格網的方法,內部正是采用Workers線程的方式,下面我們進入主題,詳細介紹高度圖構網的細節。
HeightmapTessellator
如上是一個流程示意圖,橫線以上的是主線程,調用createMesh,創建線程,把buffer(高度值數組),寬高(width&height),tile的范圍(rectangle)和中心點(center)等作為createVerticesFromHeightmap函數的參數,這樣,每一個Tile都會創建一個Worker線程,並在線程中實現網格的構建。
Paremeters
網格構建的算法則封裝在HeightmapTessellator.computeVertices函數中,我們先詳細了解里面的參數:
- Heightmap
用於構建格網的高度圖點串 - Width&height
高度圖的像素寬高 - skirtHeight
俗稱裙邊,每一個Tile四周會圍成一個柵欄,指定該柵欄的高度,保證和相鄰的Tile拼接時沒有間隙 - nativeRectangle
該Tile的范圍,如果是WGS坐標系,單位是度,如果是墨卡托,單位是米 - exaggeration
地形高度的縮放系數,通常為1,現實真實的地形高度 - rectangle
該Tile對應的地理范圍,單位是弧度,rectangle和nativeRectangle至少要有一個,如果兩個參數都有,則互相是匹配的 - isGeographic
true則為WGS坐標,false為墨卡托 - relativetoCenter
該Tile對應的中心點,單位是基於球心的笛卡爾坐標,單位為米 - ellipsoid
橢球體類,提供一些計算和換算方法 - structure
高度圖數據結構,后續再說,感覺有點雞肋
以上就是必須傳入的參數(也有一些不准確,如果沒有設置這些參數,則會采用默認值),當然還有一些可選參數。然后就正式開始構網了。構網的過程主要分為四個部分:
- 構建網格
- 計算BoundingSphere
- 計算HorizonCulling
- Encode
網格節點
構建網格的代碼很長,但仔細讀一下其實不難理解,結合下面一張圖,先跟大家解釋一下思路。
這里先把這個網格想象成正方形的(就像把地球儀平鋪成一幅地圖),這就是一個Tile對應的格網。通過之前傳入的參數,我們已知Tile的長寬(rectangle),行列的格子數(width&height),不難計算出每一個節點的位置(經緯度),說白了,就是兩個for循環嘛,偽代碼如下:
當然這是一段偽代碼,如果這樣寫確實也很短,但Cesium認為構建網格的計算量大,也很頻繁,所以在此處進行了優化,是一個簡化版的cartographicToCartesian函數,這一塊說復雜也復雜,需要你對橢球體能有所了解,說簡單也可以,因為即使不了解,你也可以直接套公式。大致的圖示和公式如下:
其中,B是緯度,L是經度,N是長半軸,這里是地球半徑6378137米,而N(1-e^2)是橢圓的短半軸,這里取值6356752.3142451793米。另外,網格中節點數和高度圖的寬高是一致的,這樣每一個節點都對應高度圖中的一個高度heightSample,這樣,套用上面的公式,對應實現代碼和注釋如下:
這樣,對應格網中每一個節點,我們可以計算出positions和heights這兩個數組,同理,再次把這個Tile網格想象成平面的,每一個Tile也對應一張影像切片,假設把這兩張半透明的紙疊在一起,下面那張的是網格,上面那張是影像圖,就是如下的這個效果:
通常影像切片是256*256像素大小,我們把[0,256]的像素范圍映射到[0,1]的比例中,這樣,也能夠計算出每一個節點對應[0,1]的比值,也就是通常說的uv(OpenGL里面的紋理坐標,渲染紋理時需要用到該參數)。Cesium中實現uv的代碼如下:
此外,在計算網格節點時,還計算了每個節點距離該Tile中心點relativetoCenter的距離,這個在下面計算boundingsphere時會需要。 這需要掌握圖形學和矩陣方面的一些數學基礎:
如上,通過兩個遍歷,我們得到了和網格節點一一對應的三個數組:positions,heights,uvs以及距離中心點的最大值maximum和最小值minimum。
Cull裁剪
如果只是單純的網格構建,工作已經完成,但實際中還遠遠不夠,最直接的一個問題是你不知道是否需要顯示這個Tile。
如果做過渲染優化的人有會有這樣一個共鳴吧,渲染一個物體最快的方式就是不去渲染它。看上去很正確,但做起來其實是一個很嚴肅的問題。把這句話轉譯成程序員的語言就是,判斷這個物體的范圍是否在當前可視范圍內。在結合我們正在討論的Cesium地形,考慮到大規模,頻繁的渲染環境下,在相機的視錐體下,如何快速,簡單的判斷當前地形格網是否可見,這是一個嚴肅的問題。而Cesium在這個問題上,做到了極致,讓我深為嘆服。
首先,Cesium主要采用了兩種裁剪方式:
- Frustum Cull
- Horizon Cull
因為里面涉及到很多算法,坦白說,每一個單獨的細節,展開講都很有學問,所以下面主要是思路和個人的理解,我也盡量把條理說的清楚一些,讓大家能夠一個完整的認識。由於篇幅過長,本篇主要介紹錐體裁剪部分,水平面裁剪在TIN地形的時候在涉及。
Frustum Cull
首先,當一個物體不在視錐體范圍內,自然就不需要顯示了。視錐體的大小是清楚的,所以,剩下的就是如何計算該物體的Bounds。同時,從世紀角度來看,這個判斷的過程一定不能超過渲染該物體的時間,否則也是沒有意義的。因此,構建這個Bounds的關鍵就在於快速和有效之間的平衡。即能夠較快的構建出一個近似准確的Bounds,同時這個Bounds也能高效的較為准確的判斷是否可見。
BoundingSphere
Cesium最先提供的是BoundingSphere,如下圖,就是一個模型的BoundingSphere,也就是一個物體的外接圓。
現在,我們理解BoundingSphere的概念,那么我們有一堆點串,如何實現BoundingSphere.fromPoints這個函數呢?在閱讀下面的內容前,希望大家也琢磨一下這個問題。
BoundingSphere就是一個球,所以這個問題就是獲取球的球心和半徑。之前我遇到過類似的問題,有一堆點串,已知中心點的情況下,計算其半徑。我的思路是遍歷所有點,計算每個點和中心點距離,取最大值作為半徑。
現在,這個中心點是未知的,所以我們需要先遍歷所有點,找到XYZ三個方向的Max和Min,即X(Min,Max),Y(Min,Max),Z(Min,Max),然后計算Min和Max的均值,作為中心點,即:P = (Min+Max) / 2。這樣有了中心點,前面也給出了計算半徑的思路,我們就實現了BoundingSphere.fromPoints。
這個算法不難理解,也是最簡單最快速的方式,在Cesium中,稱這個算法為Naïve Method,看到Naïve,不知道有幾個人會會脫口而出“圖樣圖森破”?但這個算法也有一個缺點,這個球通常都不是最優的,就像你穿了一件大一號的衣服,略微不太優雅。
接着,Cesium對比了Jack Ritter算法。這個算法和Naïve算法相似,也是需要遍歷兩遍,第一次遍歷后,估算出一個初始的球,然后再次遍歷,如果點在這個球內則什么也不做,如果點在球外,則調整中心點和半徑,確保該點在球內。調整算法如下:
Naïve和Jack Ritter相比,第一次遍歷過程基本一致,但第二次遍歷時,Naïve只修改半徑,而后者對中心點和半徑都會調整。Jack Ritter自己測試,兩者在計算量上相當,但后者要多5%的准確性。但Cesium自己測試發現,19%的情況下,效果會比前者差,而11%的情況下,效果會比前者好,說明第一次估算的球和添加點的順序也會影響Jack Ritter算法的結果。
如上是一個測試數據對比(參考),最后在Cesium里面會同時執行Naïve和Ritter算法,以半徑最大的值作為最后的結果,這個思路是可取的,兩次遍歷的計算內容99%都一樣,就像拼車一樣,舉手之勞,受益良多。有了BoundingSphere,如何判斷是否在視錐體范圍內呢,在《Cesium原理篇:2最長的一幀之網格划分》里面有詳細解釋,這里就不贅述。
OrientedBoundingBox
就這樣相安無事了不久,人們對性能的追求始終沒有停止。之前的BoundingSphere發現還是不夠精確,你也看到了,這個球里面有不少的空白區域,造成了過度的渲染,那有沒有更精准的Bound呢,這個就是OrientedBoundingBox了。
BoundingBox是指包圍盒,再加上Oriented,顧名思義就是有朝向的包圍盒了。上一個對比效果圖,可以看到這個其實就是在本地坐標系下的一個包圍盒,如下是一個OrientedBoundingBox和對比圖,可見,確實范圍要小很多。
同樣,我們要考慮兩個問題,獲取這個Bounds的成本,以及判斷Bounds是否可見的成本。
首先,對應一個地形Tile,總會有一個中心點,也就是參數relativetoCenter,該點對應球面的切面+法線,構成了這個local coordination(NEU:north east up)的XYZ軸,這樣一個相對NEU坐標的正交geometry,相對於球心的笛卡爾坐標系就是一個斜geometry了,這就不像BoundingSphere那樣具有更好的對稱性,可以很直白的用參數化的方式來構造了。OrientedBoundingBox默認是一個2*2*2的正方體,center是該包圍盒的中心點,而還有一個矩陣halfAxes用來記錄包圍盒按照中心點旋轉和縮放信息。
下面簡單介紹一下如何獲取一個地形Tile對應的OrientedBoundingBox,也就是Cesium.OrientedBoundingBox.fromRectangle函數。
首先,我們知道該Tile對應的relativetoCenter,然后構造出EllipsoidTangentPlane對象,也就是該點對應的橢球切面,這個過程其實就是從笛卡爾坐標轉為NEU坐標的過程,進而獲取該點對應橢球的法線方向,點+法線 = 切面。如下圖,紅線是一個二維橢圓切線示意圖,對應三維橢球就是一個切面:
此時,我們只是獲取了OrientedBoundingBox的XYZ的三個朝向,並在XYZ三個方向上無限延伸,但無法確定具體的范圍。如何計算這個范圍呢?
先舉個簡單的例子,上面在一個黑暗的屋子里,你手拿這個足球正對着一面牆,在球心放一盞燈,。假設黑色的部分是透光的,而白色的不透光,這樣牆上會有黑色切面的影子。我們把球不斷的靠近這面牆,直到剛剛好貼在牆面上,這時每一個切面對應的影子是最小的。
很顯然,這個切面是一個弧面,而牆是一個平面,剛才的過程其實就是把XYZ的三維體投影到XY平面的過程。Cesium也是用同樣的思路,通過EllipsoidTangentPlane.prototype.projectPointsToNearestOnPlane方法計算每一個Tile地形在XY上的范圍(此時並不是計算所有點,而是類似九宮格,計算Tile對應左中右上中下這九個點來配准),而Z的范圍可以簡單的理解為當前地形數據對應的最高點和最低點,從而得到該Tile對應的minX, maxX, minY, maxY, minZ, maxZ(該值是以relativetoCenter為原點,米單位)。
最后通過如上獲取該包圍盒准確的中心點以及相對偏移量和縮放比,結合NEU矩陣最終構造出halfAxes矩陣。
這樣,我們就找到了地形Tile對應的OrientedBoundingBox,坦白說,理解上要比BoundingSphere復雜很多,但在計算量上,因為只針對特征點來計算,其實性能要好,當然,個人沒有測試,只是推測。
如何判斷包圍盒和視錐體的位置關系,這個和BoundingSphere的算法非常相似。那這種方式效果是否有改進,還得看療效,如下是Cesium自己提供的對比效果(參考):
可見,改善效果還是很不錯的,而判斷是否相交的性能上差別不大:
實際上,兩種Bounds方式在Cesium中都在使用,而且,計算格網positions和兩個bounds(BoundingSphere,OrientedBoundingBox)中,有一些重復計算的部分,所以還是有一定的優化空間。但這主要是在編程技巧上的對比,從邏輯和算法上,Cesium已經非常專業了,我個人覺得在研究源碼時,在方方面面,都受益匪淺。
如上,我們計算了positions,heights,uvs以及bounds后,我們基本完成了HeightmapTerrainData.CreateMesh的過程,也是地形中最關鍵的環節,下一步,就是開始加載到顯卡中,通過shader渲染了,我們在后續會介紹。同時,由於篇幅的問題,臨時決定把水平裁剪和Encode的部分取消,后面找個合適的機會在介紹。