1. 程式人生 > >cesium原理篇(二)--網格劃分【轉】

cesium原理篇(二)--網格劃分【轉】

更新 vid -- 每一個 下載 三種 pri 一次 adt

轉自:http://www.cnblogs.com/fuckgiser/p/5772077.html

上一篇我們從宏觀上介紹了Cesium的渲染過程,本章延續上一章的內容,詳細介紹一下Cesium網格劃分的一些細節,包括如下幾個方面:

  • 流程
  • Tile四叉樹的構建
  • LOD

流程

首先,通過上篇的類關系描述,我們可以看到,整個調度主要是update和endFrame兩個函數中,前者分工,後者幹活。

另外,QuadtreePrimitive類只要來維護整個地球的四叉樹,而每一個Tile對應一個QuadtreeTile,另外多說一句QuadtreeTile只負責網格的維護,每一個網格對應的數據(地形&影像)則有GlobeSurfaceTile管理。

初始化的時候,當各類Provider準備就緒後,Cesium就開始構建地球,這個過程都發生在QuadtreePrimitive.prototype.update函數中。在update函數中,通過selectTilesForRendering實現網格的劃分和調度。我們先根據時間和狀態的變化來描述一下整個流程。

技術分享圖片

首先判斷是否存在第零層的Tile,也就是整個四叉樹的根節點。createLevelZeroTiles函數負責根節點的創建,參數tilingScheme表示地球網格采用的剖分方式,該參數值和地形的投影是一致的,因此只能是WGS1984的經緯度,也就是我們上一篇提到的剖分方式,第零層有兩個根節點:

技術分享圖片

也就是說無論影像服務采用的是墨卡托的還是經緯度,但Cesium的地球是按照經緯度的剖分方式,和地形數據的規範保持一致。不管怎樣,這樣我們有了兩個QuadtreeTile,一個是L0X0Y0,一個是L0X1Y0。

但此時的Tile只是一個有行列號的空殼,裏面並沒有實際的數據(needsLoading == true),也不能用於渲染(renderable == false)。作為一個新Tile,他被扔進了兩個隊列中,一個是_tileReplacementQueue,這個是一個雙向鏈表,來統計所有加載進來的Tile,不過並沒有發現它的價值,另一個是_tileLoadQueue,下載隊列,該隊列中的Tile,則會在下載函數中完成數據的加載,數據下載的過程會在下一篇詳細介紹,此處一筆帶過。

根據劇情需要,開始下載地形數據和影像數據,這個過程是異步的,當地形數據下載完成後,執行propagateNewLoadedDataToChildren函數。因為要給子節點的地形數據采樣(地形中詳細介紹),所以會創建出該Tile對應的四個子節點,不然還怎麽叫四叉樹,全部數據下載完成後就會更新該Tile的狀態:

QuadtreeTile.renderable == true;

QuadtreeTile.state == QuadtreeTileLoadState.DONE;

技術分享圖片

生命不息,update不止,只是此時,根節點Tile已經整裝待發,來到人生的第二個狀態。這裏判斷一下該Tile在當前狀態下是否可見,如果可見,則加入到traversalQueue隊列,顧名思義,這個隊列就是用來遍歷四叉樹的。一旦traversalQueue不再為空,好戲就開始了。

技術分享圖片

如上,是網格調度中最關鍵的一部分,通過其中的if & else if & else不難看出,一共有三個邏輯:

  • screenSpaceError
    該Tile的精細度是否滿足LOD要求,是否不需要請求更精細的層級。如果滿足精度要求,則該Tile加入到_tilesToRender渲染隊列,如果不滿足精度要求,則該if判斷為false

  • queueChildrenLoadAndDetermineIfChildrenAreAllRenderable
    如果之前的判斷為false,則需要判斷該Tile的四個子節點是否可渲染,如果可以渲染,則將子節點加入到traversalQueue中,如果不可渲染,則加入到_tileLoadQueue來下載,這兩個隊列中的Tile遵循各自的調度流程,此時該else if判斷為false

  • else
    如果進入該else的邏輯,則說明該Tile的精度達不到要求,但其子節點還處於不可渲染狀態,這時候怎麽辦?只要先湊合一下,把當前的Tile先放到tilesToRender隊列吧,寥勝於無

這三個邏輯都比較清楚,我們先不糾結於內部具體的算法實現,以時間順序來描述一個簡化的網格調度場景:

繼續從之前的根節點說起,此時根節點的數據已經加載完畢,該Tile從tileLoadQueue切換到traversalQueue隊列,進入到該while循環:

首先發現該Tile0的精度不夠(此時Level = 0),需要進一步的請求下一層級的切片Tile1(Level = 1的Tile),然後發現Tile的4個Children子節點還不能渲染,則把這四個節點發到tileLoadQueue(這四個節點將經歷和它們父親一樣的經歷,然後進入到traversalQueue隊列,繼續這個while循環),這樣,只好先渲染這個根節點,把Tile0加入到tilesToRender渲染隊列。

While循環就這樣一直判斷,未來的某一段時間,Level = 1的節點們各自完成了數據下載的過程,這是再進入到該while循環,就會發生一些不一樣的事情了:

首先發現該Tile0的精度不夠,需要進一步的請求下一層級的切片Tile1,然後發現Tile的4個Children子節點都可以渲染了,則把可渲染的這四個Tile1加入到traversalQueue,不可渲染的還和以前一樣。這樣,while循環會遍歷traversalQueue隊列中的所有Tile,輪到Tile1了,發現這次精度滿足要求了,則把Tile1加入到渲染隊列tilesToRender,此時,Tile0並沒有進入到渲染隊列,只是起到了新老接替的作用。

這是一個最簡化的過程,但無論調度有多復雜,都符合如上的一個邏輯,萬變不離其宗。從根本來說,在渲染和update的過程中,Cesium並不面向過程來推動整個流程,而是面向對象(狀態),每一個不同狀態的Tile賦予一個不同的職能,各司其職,盡其所能。這樣,一個Tile從下載隊列到遍歷隊列,最後到渲染隊列,然後交出接力棒,退出渲染隊列。所以這個渲染過程本質就是一個狀態維護的過程,這個思想來貫徹這個Cesium的各個模塊。

Tile四叉樹的構建

四叉樹,簡單而言,就是在當前的Tile上畫一個田字格,這樣就形成了四個子Tile,level++,整個過程就結束了。 
  • 1

Cesium裏面四叉樹的構建很簡單,一目了然。但難能可貴的是,通過接口設計,並不需要可以的來進行這個過程,而是通過屬性接口的設計方,在第一次需要getChildren的時候來構建,我覺得很巧妙。

這樣避免了過多的邏輯判斷,在需要Children的時候先要判斷有沒有,當這個鏈表層級多了,這個判斷就很難維護了。集中在一起維護,提高代碼重用,方便管理。

實現代碼很簡單,這裏只給出代碼片段,實現了一個子節點的構建,記住田字格,相信大家都能夠一目了然:

技術分享圖片

LOD

下面涉及到LOD時關鍵的兩個算法,第一是判斷當前Tile是否滿足精度的要求,如果不能滿足,則需要繼續請求下一級的Tile,滿足渲染效果,第二是在Tile從根節點的遍歷中,判斷當前Tile是否可見。

  • screenSpaceError

    Cesium就是通過該函數來判斷是否適可而止,停止網格的進一步剖分。算法很簡單,只有下面一句話:

技術分享圖片

我們把公式分解一下,先看看height/sseDenominator的涵義:1 height是整個屏幕像素高,而sseDenominator是相機fovy角度的tan值的2倍,如下圖所示:

技術分享圖片

如上圖所示,根據三角函數可知:tan(fov/2) == (height/2) / far;而height/sseDenominator == (height/2) / tan(fov/2) == far;也就是相機距離屏幕中心的像素距離。

而maxGeometricError是地球赤道的周長/像素數,也就是分辨率,可以認為是在Tile不拉伸的情況下(比如一個256的Tile就是按照256的像素顯示,而不是被拉伸成300的像素)一個像素代表多少米。(maxGeometricError * height) / sseDenominator == maxGeometricError * (height/sseDenominator),也就是理想情況下,相機距離屏幕中心的米單位距離,我們記作L。

而distance是當前狀態下,相機距離該Tile的真實距離,於是L / distance就是一個粗略的拉伸比,如果distance值小於L,說明當前觀看的位置distance比真實的位置L要近,則需要更精細的層級效果,而distance是分母,分母越小,該值就越大。換句話說,就是該值越小,說明當前的拉伸越小。

  • GlobeSurfaceTileProvider.prototype.computeTileVisibility

這個函數主要是判斷當前Tile是否可見,裏面主要有兩個算法:

computeDistanceToTile

判斷相機距離Tile的距離。一個Tile在球面上是一個弧面,在TileBoundingBox中分別記錄了四個邊的法向量和四個頂點的位置,這樣,根據每一個邊的法向量和相機距離頂點的向量,點乘的結果就是相機的垂直距離,分別計算出四個邊的距離,各取東西方向(a),南北方向中的一個(b),再加上相機到地球表面的距離c,sqrt(a^2 + b^2 + c^2),則是相機距離Tile的distance。

  • computeVisibility

判斷該Tile是否在裁剪面的內部,參考上圖,可以看到裁剪面有六個面,則在三維中,一個Tile和其中一個面則有三種關系:內部,外部,相交。

技術分享圖片

如上是一個簡易圖,我已經在mspaint中盡力了。紅色的是其中的一個平面,而箭頭是它法線的方向,這裏有五個圓,對應了五種BoundingSphere的情況(Cesium裏面抽象了Tile,把他們簡化為一個原點+半徑的圓球,方便計算,這也是三維中常見的一種思路。)

首先,還是點乘,法向量和綠色直線的點乘,就是該原點距離該plane的垂直距離,這裏還有一個方向的問題,也就是當角度>90時,cos值小於0.

結合上圖,在x軸以上的圓心,角度小於90,點乘的結果肯定大於0,肯定在plane平面內,而在x軸以下的,角度大於90,點乘結果為負數。這時計算圓心和平面的距離,和該圓的半徑比較,則可以輕松的計算出兩者的關系。代碼如下:

技術分享圖片

本篇內容大致如上考慮的篇幅,並沒有在這裏和大家介紹horizon occlusion這個技術,也是用於Tile裁剪的,因為只是在STK地形時采用這個方式,所以會在地形這一篇中和大家交流,簡單說,效果如下(前者不采用,後者采用):

技術分享圖片

你會發現采用這個方式的,地球背面,不需要顯示的Tile會過濾點,以往這需要計算Tile的法線和相機的角度,比較繁瑣,主要是處於性能的考慮,但通過這個方式,可以快速過濾,但在數據層面需要提前準備好,這也是為什麽采用STK地形的時候開啟該功能的原因,不多解釋,下回分解。

cesium原理篇(二)--網格劃分【轉】