1. 程式人生 > >回想四叉樹LOD地形(上)

回想四叉樹LOD地形(上)

難點 過程 即使 mod 消失 發現 結點之間 有時 結束

唉。~事實上這是在差點兒相同一年前實現的東西,但當時沒作好記錄。放了那麽久了,假設不做點總結的話,好像有點對不起自己,於是·········還是做點什麽吧。

我腦洞比較小,僅僅能大量參考“潘李亮”大神的四叉樹LOD地形論文和一個叫做“曾濤”大神的代碼實現,小弟在此先向諸位致敬了。以下分享本人實踐之後的一點心得。
為什麽要用LOD地形? 或許看完了傳說中的“龍書”有關地形那章,你就知道了一個簡單的地形系統是怎麽實現的,無非就是依據高度圖提供的高度信息以及每一個高度像素在高度圖中所在的位置計算出地形頂點在3D空間中的世界坐標。然後依據各個地形頂點的拓撲關系計算出每一個地形三角形面須要由哪三個頂點組成。這樣就行讓渲染API以繪制三角面的方式拼接出三維地形了。

可是,我們應該註意到,若地形邊長為n,那麽組成地形的三角形個數就會是2*n^2。非常明顯,若直接將計算出來的三角形圖元扔給渲染API,那麽算法的復雜度就會是O(n^2),且渲染這些三角形的工作都必須在一幀的時間內完畢。

這樣一來。不管是CPU或者是GPU的開銷都非常大,地形略微變大,機器要做的工作就要多非常多,實時程序隨時變成播放幻燈片。

況且,假設地形比較大,那麽在遠處的地形三角形被投影到屏幕上時或許已經變成了幾個像素點。甚至不可見了。

既然遠處的地形對我們的視覺貢獻不大。那麽為什麽還要辛辛苦苦地讓硬件做那麽多的無用功呢?

有沒有辦法不渲染那些遠到看不見的地形三角形呢?答案是有的。

那就是LOD地形。LOD的全稱是“Level Of Detail”(細節層級),主要的要求是:在近處的地形渲染得盡可能具體,在遠處的地形渲染得盡可能簡單。

通常。LOD地形除了“遠粗近細”的功能外。一般還加上了“視錐體可見性剔除”功能,即僅僅考慮在視野範圍內的地形三角形是否渲染,而絕不渲染在視野外的。這種“簡化”+“剔除”機制能夠大幅度減少機器的開銷。聽起來是挺有吸引力的,可是到底怎麽實現呢?


實現LOD的難點: 依據我的經驗,我覺的實現LOD有例如以下兩個難點:1、以何種方式對地形進行簡化;2、怎樣解決地形簡化之後出現的T型裂縫;3、在視點移動時,原本遠處被簡化的地形區域在靠近視點後突然變得具體起來,或者原本在近處具體顯示的地形區域遠離視點之後突然被簡化了,這樣就可能造成某些地形細節的突然出現或者突然消失,在有光照的情況下,這樣的現象尤為明顯,怎樣解決問題?
對於第三個問題。我當時因為時間原因來不及考慮,可是發現將地形進行平滑處理後能夠減輕這樣的“突隱突現”現象,並且還能降低三角形面數的顯示。於是將計就計了。怎樣對地形進行平滑?你能夠在PS中事先對高度圖進行模糊處理。又或者能夠在程序中使用濾波算法對高度圖進行濾波,方法應該非常多,我就不糾結了。前兩個問題是我回想的重點。


地形的表示: 想要對地形進行簡化,首先要知道地形是怎樣表示的。 我們先不理會地形在內存中的數據組成,我們先來理解下地形在邏輯上的表示。

地形是由 (2^n+1) X (2^n+1) 個頂點組成,當中n>0,n到底要多大?我認為跟n有關系的計算都不溢出就好

左下圖就是個9X9的地形樣例。實際上的地形不會那麽小。幾百X幾百甚至成千上萬也不奇怪,這裏僅僅是為了舉例說明而已。另外。組成地形的基本單元也不再像龍書中那樣使用一個個的獨立三角形。而是使用三角形扇為地形基本單位,這裏的三角形扇事實上也是由8個三角形組成,在渲染API中使用9個頂點和10個索引表示。

我們最好還是把這樣組成的三角形扇稱為一個地形結點。如右下圖表示:

技術分享 技術分享
由三角形扇組成的地形 一個地形結點

為什麽地形大小被限制為 (2^n+1) X (2^n+1) 個頂點呢?這是由於用四叉樹來簡化地形就必須使用這樣的的尺寸。這樣在每一次切割地形結點時才幹剛好劃分成四個等分。大家能夠把每一個交點都當成頂點,自己比劃一下左上方那幅圖。從上面也能夠看出,使用三角形扇來組成地形時。能夠簡單地用一個三角形扇來取代幾個小的三角形扇組成的大塊地形,這也是簡化的關鍵所在之中的一個。當然,這塊地形要被評估為比較平坦的表面才幹夠這樣替換。
簡化流程預覽: 技術分享 地形結點由低分辨率到高分辨率的過程
上面就是某個大地形塊被細分為多個小地形塊的簡單過程。大地形的簡化實際上是從“粗糙”到“精細”的一個過程,而不是真正地由繁到簡。 在實際的細化過程中。地形是按分辨率級別來處理的。地形四個角落的頂點以及四條邊上的中點加上地形中間的那個頂點,就構成了最大的地形結點,這個結點算作第0級分辨率結點,之後每次劃分得到的四個結點的分辨率就遞增1個級別,最小的不可再分的地形結點就是最高級分辨率結點。這樣劃分分辨率級別有什麽優點?這樣方便我們在代碼實現時使用分辨率級別高速確定地形結點的中心頂點在頂點緩沖區中的索引值。

說起來非常抽象。後面看代碼就清楚了。

簡化流程開始,首先評估0級結點是否須要被切割。通常來說都會被分成四個子節點。然後分別評估四個子結點是否須要繼續劃分。若地形結點須要切割,則把它再分為四個結點,然後對四個結點分別再推斷是否須要繼續切割,如此遞歸下去。直到切割到最小地形結點或結點不可分為止。最小的地形結點無法再進行切割,僅僅有送到渲染API進行渲染。若地形結點不須要被切割。也是直接送入渲染API進行渲染。當然,這些結點前提是要在我們的視野範圍內才會進行上述的切割推斷,否則,我們就直接忽略它們,不做不論什麽處理。 在細化的過程中。若某地形結點被評估為不可分結點時,後面的細化步驟就被中斷了,這時我們能夠簡單理解為該不可分結點的細節已經達到要求,能夠取代一系列小的地形結點,我們就僅僅顯示該不可分結點,而不顯示興許一系列小地形結點,這樣我們就達到了簡化目的。

因為有些結點須要切割有些不須要。所以最後得到的結果不會像上面三幅圖那麽規則,或許像這樣:


技術分享
地形簡化後可能的樣子,這裏如果視點在左上角
由上面的簡化流程能夠看出。簡化過程實際上是個遞歸的過程。在實時性要求比較高的大地形系統中使用遞歸算法,實在不能算是一個比較好的解決方式。於是,潘大神提出“使用雙隊列來化解遞歸”的想法。


技術分享
上面的圖片展示了雙隊列化解遞歸的過程。格子代表地形結點,數字代表結點的級別。

須要切割的結點將切割後得到的子結點壓入第二條隊列中,自己彈出第一條隊列。不須要切割的結點直接彈出第一條隊列並送入渲染API。

在第一條隊列中全部結點都被處理完成後,兩條隊列交換身份,第二條隊列變成第一條,然後反復之前處理步驟。直到兩條隊列中都沒有結點為止。



結點評價系統: 上面說到了評估,對的。我們須要一個評價系統來確定一個地形結點是否須要被切割。那麽這個評價系統究竟是怎麽樣的呢? 技術分享
技術分享
技術分享
引用潘大神的圖片

首先,我們的LOD須要實現地形“遠簡近繁”的效果。遠和近靠什麽來度量?潘大神使用“視點”到“地形結點中心”的距離。可是僅靠距離來衡量一個地形結點是否須要被切割是不夠的。

由於有可能一個結點大得離譜但卻遠在天邊。若該結點不切割又會造成地形細節大量損失;又或者一個結點小得可憐卻近在眼前,本來已經能夠表示足夠的地形細節了但卻依舊被切割了。所以。為了處理好這兩種情況,我們還要考慮結點的大小。把兩個因素放在一起考慮,由此得出潘大神的第一條評價公式:

技術分享
技術分享

在上面的公式中,L代表視點到地形結點中心的距離,D代表地形結點的邊長,C是一個調節因子。

這個公式表示,當結點離視點越來越遠,且地形結點越來越小時。兩者的比值超過C。地形結點就不須要切割。這個調節因子是要依據地形顯示的結果來確定的。也就是要在地形系統實現後,在測試階段進行該因子的調整。

嗯。看似評價系統就是這樣了,但細想一下。僅有上面這個條件就夠了嗎?假如有些地形塊凹凸起伏非常大。不管這些大地形塊結點離視點的距離多遠。我們都要求清楚地看到這塊地形凹凸不平的特征,若僅靠上面的公式來對結點進行評判,距離一拉遠,凹凸不平的地形就被一個平坦的地形結點取代了。這樣一來地形就失真了。所以。我們還須要考慮地形結點的粗糙度。 怎樣定義地形結點的粗糙度?潘大神這樣做: 技術分享 技術分享
若一個地形結點被簡化後才顯示是和實際地形結點有誤差的。

比方左上圖中。底下的平面代表簡化後的地形結點,上面籠罩著的是實際的地形結點。

直觀上,我們會發現有5個誤差,在圖中顯示為結點邊上的dh1、dh2、dh3、dh4以及中心處的dh0。為保證誤差測量結果盡量準確。我們還要考慮大地形結點被切割成4個子地形結點後,每一個子節點的粗糙度。在右上圖中表示為dh5、dh6、dh7、dh8。

那子節點的粗糙程度怎麽算?我們僅僅須要反復地像計算大結點粗糙度那樣即可了,非常easy發現這是個遞歸的過程。那假設一個地形結點已經切割到沒有子節點時怎麽辦?那就忽略dh5~8,僅僅考慮前5個誤差僅僅即可了。

上面的粗糙度計算盡管是個遞歸過程,可是全然能夠放在預處理階段計算。將結果存放在一個與頂點緩沖區一樣大的數組中即可了,評價結點時直接取值計算,不影響實時效率。 找出了這幾個誤差值之後還沒完工,一個地形結點的粗糙程度 R 應該這樣算:
R=Max(dh0 ,······ , dh8) / D
地形結點的粗糙度應該等於最大誤差值除以地形結點的邊長。那麽。一個地形結點越粗糙就越不應該被簡化。由此得出潘大神的第二條評價公式: 技術分享 技術分享
上面公式中的C2是粗糙調節因子。也是像第一條評價公式中的C那樣,須要在地形系統實現後測試調整。那麽,將兩條評價公式一合並就變成了這樣: 技術分享
技術分享
能夠意識到,若C和C2越大,地形的細節就會越多,簡化程度就越低。顯示的三角形就會越多,機器的開銷就會越大。前面我還說過。若將地形平滑處理,“突隱突現”現象就會減小,且三角形的渲染數也降低了,從上面的公式中也非常easy就知道這是怎麽回事。

這是由於將地形進行平滑處理之後。地形的粗糙度R降低了。直觀上看。簡化顯示和實際顯示的誤差不會太大。於是“突”現象就不那麽明顯了。從公式上看,R值小,地形結點被簡化的幾率增高了。於是相對顯示的三角形就少了。


修補T型裂縫: 知道了地形怎麽表示。大致的簡化流程,以及怎樣評價一個結點是否須要切割,那麽LOD是否就完畢了呢?算是吧······可是還不夠完美。

細想一下。有些結點須要切割,有些結點不須要切割,那麽假設不加以控制的話,切割程度相差非常大的結點接壤在一起時就會形成T型裂縫。例如以下圖所看到的: 技術分享 技術分享
我相信上面的圖片已經能非常好地告訴讀者T型裂縫是怎麽產生的,以及在實際程序中裂縫是什麽樣子的。那怎樣修補T型裂縫呢? 首先,我們必須先控制好接壤的地形結點之間的級別差不能超過1。左上角圖片那種就是典型的符合要求的接壤類型,而例如以下類型由於接壤結點之間的級別差已經超過1。所以不符合要求: 技術分享 技術分享
那該怎樣實現這種控制呢?我們必須為地形增設一個標誌數組。該數組的元素個數等於地形頂點數,元素類型不用太浪費,用char就好。僅僅需表示0和1。能夠臨時將0理解為相應結點不存在,將1理解為相應結點存在。

這句話不理解沒關系,先記住。該標誌數組的作用是:在當前結點進行切割時提供四周結點的存在信息。若四周的結點都存在則當前結點能夠切割,否則,僅僅要四周隨意一個結點不存在。當前結點就不能切割。這一步推斷是在前面的評價系統之後才運行的,也就是說,評價系統的推斷優先於這一步推斷,僅僅有兩個階段的推斷都通過了,結點才幹算真正地能夠被切割。

那麽標誌數組中的存在信息是怎樣獲得的呢?這就須要我們在結點的切割操作中更新該標誌數組。

怎樣更新?當一個結點能夠切割時。將自身相應的標誌位設為1,同一時候將四個子結點相應的標誌位也設為1。當一個結點不能切割時,將自身相應的標誌位也設為1,但同一時候將四個子結點相應的標誌位設為0。

我想大家一定會對“結點相應的標誌位”這個說法非常不解,事實上在代碼實現的時候。一個結點僅僅須要用中心頂點的索引以及所屬級別來表示即可了,僅僅是送入渲染API時才轉換成10個索引表示的三角形扇,到時看代碼就知道了。非常抽象是吧?以下我來用9X9的樣例演示一下。

初始標誌數組全0,現如果視點在左上角。

一開始地形塊太大,如果被評估為須要切割,那麽標誌數組修改就像以下左中兩幅圖:

技術分享 技術分享
那麽4個子結點在第二輪循環中被處理。逐一推斷是否須要被切割。如果僅僅有離視點比較近的左上角結點才幹被切割,其它的都不能被切割,那麽標誌數組又會變成右上圖的樣子。 註意,右上圖中左上角的那個結點可以被切割不是純粹靠評價系統來評估的。還須要看看四周同等級結點的標誌位是否都為1。大家應該能發現左上角結點的正右方結點和正下方結點的標誌位確實為1,但是正左方和正上方的結點去哪找?這時我們僅僅要簡單地忽略不考慮就好了(PS:大地形中那些遠離邊緣的結點是要考慮四個方向的)。

話說回來,中間那幅圖的大地形結點在切割時需不須要考慮四周的標誌位?當然要,但是去哪找?道理同前。如今看看推斷四周標誌後不可分的情況。

技術分享
技術分享
上圖右方的結點的父結點是先被推斷為不可分結點的,如今導致了右方結點實際不存在。

所以,即使左方結點後來通過了評價系統的切割評估,但在查看四周結點標誌時。發現右方結點不存在,所以左方結點切割失敗。分析完成。


到眼下為止。接壤結點之間的級別差已經控制在1級之內,能夠進行T型裂縫消除了。有了標誌數組的幫助,消除T型裂縫實際上很easy。

前面提到過。在地形進行切割時,地形結點能夠用結點中心頂點所在的索引和結點所屬的級別表示,在送到渲染API時再轉換成10個頂點索引表示的三角形扇。嘿!我們就是要在結點轉換成10個索引的過程中消除裂縫。

怎麽做?請看下圖:

技術分享 技術分享
看到了左圖。我想大家應該更加理解標誌數組中的1和0所表示的存在與不存在詳細是什麽意思了吧!實際上我也不知道怎麽解釋得更通俗易懂了。我直接說中間那幅圖了。依據地形結點的中心頂點的索引和結點所屬級別,我們能夠非常快地找到表示三角形扇的9個頂點的索引。中間那幅圖圖就是所謂的用9個頂點、10個索引表示的三角形扇。

我們從左圖能夠發現,高分辨率級別的結點(即小結點)的正右方的標誌位是0,代表正右方同等級別的結點不存在,依據這一信息。我們就能夠把中間那幅圖中的三角形扇的4號結點給忽略,這就導致0、3、5結點能夠組成一個大點的三角面,而不再須要分別顯示0、3、4和0、4、5組成的三角形。這個操作就消除掉了由4號結點引起的高度差,T型裂縫就是這樣被消除掉了。如右上圖。也就是說,我們能夠依據一個結點四周相鄰同等級別的結點相應的標誌位是否為1來確定中間節點四條邊中點的頂點是否參與渲染,從而消除裂縫。

修補T型裂縫到此結束。
LOD終於簡化流程: 技術分享
唉!

~四叉樹LOD的回想就差點兒相同這樣了。感覺有必要做個重要代碼回想,還是改天有時間寫個下篇吧。本文不作為個人技術的創新展示,僅僅是學習心得的分享,全部知識都來自大神們的研究成果。

關於潘李亮的論文和曾濤的代碼實現,改天再補上鏈接,如今得歇息············


回想四叉樹LOD地形(上)