基于四叉樹空間劃分的地形實時渲染方法(轉(zhuǎn)載)自從我發(fā)布作品3D地形渲染以來,有很多朋友表示了對我所使用的地形LOD算法的興趣。于是我決定寫一篇文章具體地介紹我所使用的地形LOD以及表面紋理映射方法,供學習交流之用。這篇文章首先講解了基本基于高程數(shù)據(jù)的多邊形表示方法,然后詳細地講述了地形的質(zhì)量分級和視見體剔除方法。最后還介紹了如何為大型的地形創(chuàng)建穩(wěn)定的光照效果和細膩的紋理貼圖。聲明:本文前半部分講述的地形四叉樹算法最初來自《基于LOD的大規(guī)模真實感室外場景實時渲染技術(shù)的初步研究》一文,作者潘李亮。本人對算法進行了部分的更改,并提出了自己的觀點。文章后面所講述的“紋理索引貼圖”方法則是個人想出的方法。如要轉(zhuǎn)載本文,請聲明作者及出處。
地形是計算機圖形的一個重要組成部分,而它又具有特殊的形態(tài)。地形往往覆蓋面積極廣,且精度要求很高,使得我們必須用許多多邊形來描述。這樣的特點使得我們不能像對待其他普通模型那樣對待地形。要想實時地渲染地形,我們需要一些特殊的方法。 地形渲染一直以來都是計算機圖形學中一個重要的研究領(lǐng)域。并且在這一方面已經(jīng)誕生了許多優(yōu)秀的算法。其中包括基于體素的渲染方法,也有基于多邊形的渲染方法。早期的游戲,如三角洲特種部隊就是采用體素渲染法的成功例子。體素法類似光線追蹤渲染,它從屏幕空間出發(fā),找到地形與屏幕像素發(fā)出的射線交點,然后確定該像素的顏色。這種方法不依賴具體的圖形硬件,整個渲染過程完全使用CPU處理,因此它不能使用現(xiàn)代硬件來加速,并且對于一個場景來說,往往不只是地形,還有其他使用多邊形描述的物體,體素法渲染的圖像很難與硬件渲染的多邊形進行混合,因此這種方法現(xiàn)在用得極少。而多邊形渲染方法則成為一種主流。選擇多邊形來描述和渲染地形有很多的理由和優(yōu)點。最重要的是它能夠很好地使用硬件加速,并且能夠和其他多邊形對象一起統(tǒng)一管理。 已有大量優(yōu)秀的基于多邊形的地形渲染算法。比較經(jīng)典的算法有M. Duchaineau等人提出ROAM算法。這個算法采用一棵三角二叉樹來描述整個地形。一個地形在最初的層次上由兩個較大的等腰直角三角形組成,這兩個等腰直角三角形可以被不斷地細分來展現(xiàn)地形的更多細節(jié)。每一次細分過程都向直角三角形的斜邊的中點處增加一個由高程數(shù)據(jù)所描述的頂點,該點將所在的直角三角形一分為二,同時該算法也定義了一些規(guī)則來保證地形中不會因相鄰兩個三角形細節(jié)層次的不同而出現(xiàn)裂縫。這個算法已被許多游戲所采用。還有一類算法,通過將地形在X-Z投影面上不斷地規(guī)則細分來得到不同的細節(jié),這就是本文要介紹的四叉樹空間劃分算法。另外,最新提出的一個地形算法也不得不提,Hugues Hoppe在2004年提出的幾何裁剪圖方法(Geometry Clipmaps),算法使用了最新硬件所支持的頂點紋理來定義地形的外觀,并且對于距離攝影機不同遠近的地方采用不同的紋理層,最大限度地使用硬件加速了地形渲染的過程。這個方法聽起來非常美妙,但它目前只被較少的硬件支持。因為頂點紋理是Shader Model 3.0才支持的功能,也就是說只有DirectX 9.0c級別的顯卡才能支持這種算法。這對于某些有普及性要求的圖形應(yīng)用程序,尤其是對游戲來講不是一件好的事情。因此大多數(shù)人現(xiàn)在還在使用經(jīng)典的地形渲染方法。 首先,基于四叉樹的地形渲染方法使用高程數(shù)據(jù)作為數(shù)據(jù)源。且算法要求高程數(shù)據(jù)的大小必須為2n+1的正方形。所謂高程數(shù)據(jù),即色彩范圍在0-255的灰度圖片,不同的灰度代表了不同的高度值。如果某高程數(shù)據(jù)指出這個高程數(shù)據(jù)最高處的Y坐標值是4000,那么在高程數(shù)據(jù)中一個值為255的像素點就表示這個點所代表的地形區(qū)域的高度是4000,同理如果該像素值是127那么就表示這個點所代表的地形區(qū)域的高度是4000×(127/255)=2000。高程數(shù)據(jù)的每個像素都對應(yīng)所渲染網(wǎng)格中的一個頂點。另外還有一個參數(shù)描述頂點與頂點之間的水平距離,以及一個描述最大高度的參數(shù)。因此地形的基本數(shù)據(jù)結(jié)構(gòu)如下: struct Terrain 其中,各變量的具體意義如下圖所示: 有了這些參數(shù),我們可以很容易地由高程數(shù)據(jù)的參數(shù)值得到它所表述的多邊形網(wǎng)格。得到這個網(wǎng)格之后,可以簡單地把它放入頂點數(shù)組,并為之建立一個頂點索引,就可以傳入硬件進行渲染了。然而,事情并不是這么簡單。對于較小尺寸的高程數(shù)據(jù)(如129×129),這樣做確實可行,但隨著高程數(shù)據(jù)規(guī)模的增大,所需的頂點數(shù)和描述網(wǎng)格的三角形數(shù)會急劇膨脹。這個數(shù)值很快就會大到最新的顯卡也無法接受。比如一個1025×1025的高程數(shù)據(jù),我們需要1025×1025=1050625個頂點,以及1050625×2=2101250個三角形。就算你的顯卡每秒能夠渲染1000萬個三角形,你也只能得到不到5fps的渲染速度,況且你的場景可能還不只包括地形。因此我們必須想辦法在不影響視覺效果的情況下縮減所渲染的三角形數(shù)量,另外還應(yīng)該注意一次性將最多的數(shù)據(jù)預先傳給硬件以節(jié)約帶寬。 這里要講解的算法,目的就是在不影響或在視覺可以接受的范圍內(nèi)縮減所渲染三角形的數(shù)量,以達到實時渲染的要求。根據(jù)測試,本算法在漫游大小為1025*1025的地形時速度穩(wěn)定在150fps以上(在nVidia Geforce 6200 + P4 1.6GHz的硬件上得到)。 由于地形覆蓋范圍廣,但它的投影在XZ平面上均勻分布(以下采用OpenGL中的右手坐標系,Y軸為豎直向上的坐標軸),因此我們有必要考慮對地形進行空間劃分。正是由于這樣的均勻分布,給我們的劃分過程帶來了便利。我們不需要具體地去分割某個三角形,只要選擇那些過頂點且和X或Z軸垂直的平面作為劃分面即可。例如對于一個高程數(shù)據(jù),我們可以以坐標原點作為地形的中心點,然后沿著X軸和Z軸依次展開來分布各個頂點。如下如所示。 首先,我們可以選擇X=0和Z=0這兩個平面,將地形劃分為等大的四個區(qū)域,然后對劃分出來的四個子區(qū)域進行遞歸劃分,每次劃分都選擇交于區(qū)域中心點并且互相垂直的兩個平面作為劃分面,直到每個子區(qū)域都只包含一個地形單元塊(即兩個三角形)而不能再劃分為止。例如對于上圖所示9*9大小的地形塊,經(jīng)過劃分之后如下圖所示: 由圖可知,只有高程數(shù)據(jù)滿足大小2n+1的正方形這個條件,我們才可能對地形進行均勻劃分。我們可以把劃分結(jié)果用一棵樹來表述,由于每次劃分之后產(chǎn)生四個子節(jié)點,因此這棵樹叫四叉樹。那么,這棵樹中應(yīng)該存儲那些信息呢?首先對于每個節(jié)點,應(yīng)該指定這個節(jié)點所代表的地形的區(qū)域范圍。并不是把地形網(wǎng)格中實際的頂點放入樹中,而是要在樹中說明這個節(jié)點覆蓋了地形的那些區(qū)域。比如一個子節(jié)點應(yīng)該有一個Center(X,Y)變量,指定這個節(jié)點的中心點所對應(yīng)的頂點索引,或編號。為了方便起見,可以把地形中心點編號為(0,0)然后沿著坐標軸遞增。此外還要有個變量指定這個節(jié)點到底覆蓋了地形的多少個頂點。如下圖所示。 我們目前的四叉樹的數(shù)據(jù)結(jié)構(gòu)如下: struct QuadTreeNode 有了四叉樹之后,如何利用它的優(yōu)勢呢?首先我們考慮簡單的視見體裁剪(View Frustum Culling,以下簡稱VFC)。相信很多接觸過基本圖形優(yōu)化的人都應(yīng)該熟悉VFC,VFC的作用既是對那些明顯位于可見平截頭體之外的多邊形在把它們傳給顯卡之前剔除掉。這個過程由CPU來完成。雖然簡單,但它卻非常有效。VFC過程如下: 1.為每個節(jié)點計算包圍球。包圍球可以簡單的以中心頂點為球心,最大坐標值點(節(jié)點所覆蓋的所有頂點的最大X、Y、Z值作為此點的坐標值)到球心的距離為半徑。 2.根據(jù)當前的投影和變換矩陣計算此時可視平截頭體的六個平面方程。這一步可以參考Azure的Blog上的一篇文章,這篇文章給出了VFC的具體代碼。單擊這里。 3.從樹的根結(jié)點以深度優(yōu)先的順序遍歷樹。每次訪問節(jié)點時,測試該節(jié)點包圍球與視見體的相交情況。在下面的情況下,包圍球與視見體相交: 1) 球心在六個平面所包圍的凸狀區(qū)域內(nèi)部。 4.如果相交測試顯示包圍球和視見體存在交集,繼續(xù)遞歸遍歷此節(jié)點的4個子節(jié)點,如果此節(jié)點已經(jīng)是葉節(jié)點,則這個節(jié)點應(yīng)被繪制。如果不存在交集,放棄這個節(jié)點,對于這個節(jié)點的所有子節(jié)點不再遞歸檢查。因為如果一個節(jié)點不可見,那么其子節(jié)點一定不可見。 這樣,我們剔除了那些不在視見體內(nèi)的地形區(qū)域,節(jié)約了一些資源。但這還不夠。在某些情況下,VFC可能還會指出整個地形都可見,在這種情況下,將這么多三角形都畫出顯然是不可取的。 因此還要考慮地形的細節(jié)層次(LOD)。我們應(yīng)該考慮到,地形不可能所有部分都一樣平坦或陡峭。對于平坦的部分,我們用過多的三角形去描述是沒有意義的。而對于起伏程度較大的區(qū)域,只有較多的三角形數(shù)量才不讓人感到尖銳的棱角。再者,無論地形起伏程度如何,那些距離視點很遠的區(qū)域,也沒有必要花費太多的資源去渲染,畢竟它們投影到屏幕上的面積很小,對其進行簡化也是必要的。 既然我們要對起伏程度不同的區(qū)域采用不同的細節(jié)級別,我們首先必須找到一種描述地形起伏程度的量。與其說起伏程度,不如說是地形的某個頂點因為被簡化后而產(chǎn)生的誤差。要計算這個誤差,我們先要了解地形是如何被簡化的。 考慮下圖所示的地形塊,它的渲染結(jié)果如下圖右圖所示。 現(xiàn)在如果要對所需渲染的三角形進行簡化,我們可以考慮這個地形塊每條邊中間的頂點(下圖左側(cè)紅色點): 如果將這些紅色的頂點剔除,我們可以得到上圖右邊所示的簡化后的網(wǎng)格。誤差就在這一步產(chǎn)生。由于紅色的頂點被剔除后,原本由紅色頂點所表示的地形高度現(xiàn)在變成了兩側(cè)黑色頂點插值后的高度。這個高度就是誤差。如下圖。 因此,對于每個節(jié)點,我們先計算這個節(jié)點所有邊中點被刪除后所造成的誤差,分別記為ΔH1, ΔH2, ΔH3, ΔH4。如果這個節(jié)點包含子節(jié)點,遞歸計算子節(jié)點的誤差,并把四個子節(jié)點的誤差記為ΔHs1, ΔHs2, ΔHs3, ΔHs4。這個節(jié)點的誤差就是這八個誤差值中的最大值。由于這是一個遞歸的過程,因此應(yīng)該把這個過程加到四叉樹的生成過程中,并向四叉樹的數(shù)據(jù)結(jié)構(gòu)中加入一個誤差變量。如下。 struct QuadTreeNode 下面來看一下地形的具體渲染過程。 首先,我們位于四叉樹的根結(jié)點。我們此時考慮根結(jié)點的誤差,如果這個誤差小于一個閾值,直接使用根結(jié)點的中心點以及此節(jié)點的四個邊角點作為頂點渲染一個三角扇形,這個三角扇形就是渲染出來的地形。但是更經(jīng)常的情況下,根結(jié)點的誤差值是很大的,因此算法認為要對根結(jié)點進行細分,以展現(xiàn)更多細節(jié)。于是對于根結(jié)點的每個子節(jié)點,重復這個步驟,即檢查它的誤差值是否大于閾值,如果大于,直接渲染這個節(jié)點,如果小于,遞歸細分節(jié)點。目前我們的算法偽代碼如下。 procedure DrawTerrain(QuadTreeNode *node) 這個偽代碼在一個較高的層次上表述了算法的基本思想。然而我們還有許多問題要考慮。其一是目前我們僅僅考慮了地形的細節(jié)層次和地形表面起伏程度的關(guān)系,但還應(yīng)該考慮地形塊距離視點遠近跟地形細節(jié)層次的關(guān)系。解決這個問題很簡單,我們只需在偽代碼的條件中加入距離這一因素即可。即把 if (node->DeltaH > k) 改為: if (node->DeltaH / d > k) 其中d為節(jié)點中心點與視點之間的距離。而事實上,當細節(jié)程度與距離的平方成反比時,能夠減少更多的三角形,而且視覺效果更好,只要閾值k設(shè)置得當,根本感覺不出地形因為視點的移動而發(fā)生幾何形變。因此,我們最終的條件式為: node->DeltaH / d2 > k 還有一個很重要的問題,就是這個算法所產(chǎn)生的地形會因為節(jié)點之間細節(jié)層次的不同而產(chǎn)生裂縫。下圖說明了裂縫的產(chǎn)生原因。 有兩個方法可以解決這個問題,一個方法是刪除左側(cè)節(jié)點中產(chǎn)生裂縫的頂點,使兩條邊能夠重合。另一種方法是人為地在右側(cè)地形塊中插入一條邊,這條邊連接中心點和造成裂縫的頂點,從而消除裂縫。在渲染地形時,可以采取下面的辦法避免裂縫的產(chǎn)生: 1.在預處理階段,為所有頂點創(chuàng)建一個標記數(shù)組,標記以該頂點為中心點的節(jié)點在某一幀是否被細分。如果被細分則標記為1,否則標記0。 2.從根節(jié)點開始,以廣度優(yōu)先的順序遍歷四叉樹,使用之前提出的條件式判斷節(jié)點是否需要分割。如果公式表明需要分割,并且與節(jié)點相鄰的四個節(jié)點的中心點都被標記為1,那么把這個節(jié)點及其四個子節(jié)點的標記設(shè)為1,并遞歸細分這個節(jié)點。否則,將這個節(jié)點的標記設(shè)為1,把這個節(jié)點的四個子節(jié)點的標記設(shè)為0,然后采用下面的方法繪制這個地形塊: 1)將節(jié)點的中心頂點和四個邊角點添加到即將繪制的三角扇形列表中。 我們最終的偽代碼如下。
另外,一個重要的優(yōu)化是利用硬件的緩沖區(qū)或頂點數(shù)組(對于不支持頂點緩沖的硬件而言)。因為地形無論怎樣簡化,頂點數(shù)據(jù)總是固定不變的。我們在每一幀動態(tài)產(chǎn)生的僅僅是頂點索引,因此我們有必要實現(xiàn)將地形的所有頂點數(shù)據(jù)輸入到頂點緩沖中,然后在渲染時一次性將所有的索引傳給顯卡,以提高速度。實驗表明,使用頂點緩沖比直接使用glBegin/glEnd繪制圖形要快5倍以上。 以上講述了如何做到實時地渲染大型地形。主要應(yīng)用了LOD和VFC兩種手段來精簡三角形數(shù)量。然而VFC只能剔除不在視見體內(nèi)的圖形,而對于在視見體內(nèi)但被其他更近的物體遮擋的情況卻無能為力。如果要實現(xiàn)地形的自遮擋剔除,地平線算法是一個好的選擇。然而當你的場景不僅僅是包含地形時,地平線算法也只能處理地形的自遮擋情況。因為地平線算法只對2.5D的地圖(即在XZ平面上無重合投影的場景)有效。對于完全3D場景,地平線并不能很好的工作。所以當你在引擎中使用地形時,可以考慮將地形分塊后放入場景的管理樹中,如BSP或Octree等。然后根據(jù)引擎的性質(zhì)使用入口(Portal)、PVS或者遮擋測試(Occlusion Culling)等方法進行遮擋剔除。值得強調(diào)的是,遮擋測試是一個非常靈活的實時的剔除算法,且無需任何預計算過程。但要想有效的實現(xiàn)它并不是一件容易的事。我曾將地形分塊后使用遮擋剔除來完成地形的自遮擋,但是渲染速度不但沒有提升,反而有輕微的下降。因此如果要使用遮擋剔除的話必須和引擎結(jié)合起來統(tǒng)一進行遮擋測試,才有可能提高效率。 現(xiàn)在你應(yīng)該了解了基本的地形實時渲染方法。要想讓地形的外觀更加真實,我們還需要更多的工作。我們需要為地形加上紋理貼圖和光照。首先考慮地形的光照。由于地形的多邊形網(wǎng)格是實時產(chǎn)生的,它會隨著視點的移動而變化,因此如果你直接使用OpenGL內(nèi)置的頂點光照,你會得到極度不穩(wěn)定的光照效果。你會看到地形表面會因為你的移動而不斷跳動。因此我們必須使用其他的光照方法來避免這個問題。我們想到了光照貼圖。光照貼圖是一個游戲中常用的光照技術(shù)。它是一個覆蓋了場景中所有多邊形的貼圖。通過給貼圖賦值,我們可以得到多邊形表面復雜的光照效果。使用好的算法計算出來的光照貼圖可以模擬極度逼真的光影效果。它給我們帶來的視覺享受遠遠地超過了OpenGL的內(nèi)置光照。有關(guān)光照貼圖的計算可以參考我翻譯的一篇文章:輻射度算法(Radiosity) 你可以簡單地為地形覆蓋上單一的紋理,這看起來些許增加了地形的真實性: 在上圖中,我們創(chuàng)建了一個地形,并運用了一個重復的紋理。這個過程讓地形的無論哪一個區(qū)域看起來都是一樣的(例如都是草地)。這顯然不太真實,也過于乏味?;蛟S你會創(chuàng)建了一幅超大的圖片,以拉伸覆蓋的方式映射到地形表面。這樣做的后果是內(nèi)存開銷過于龐大,這樣做也很會受到硬件的限制。因此我們應(yīng)該使用一種更好的紋理貼圖方式,紋理索引貼圖。 紋理索引貼圖對三個可重復的紋理進行索引貼圖。所謂索引貼圖,就是對三個可重復紋理進行索引,以決定地形的哪些區(qū)域需要使用哪些紋理的混合來貼圖。因為對于任意的貼圖,都由一組包含3個顏色通道(即R、G、B)的像素組成。用于索引的貼圖的像素并不表示地形的某個區(qū)域的具體顏色,而是表示地形的某個區(qū)域用何種具體的紋理貼圖。因為具體的紋理細節(jié)存儲在這三個可重復的紋理中,因此索引貼圖的貼圖方式也為拉伸到地形表面,但它的分辨率可以大大降低。 紋理索引貼圖的工作方式如下:對于地形投影到屏幕上的像素,查找該像素所映射到索引貼圖上的像素。然后根據(jù)這一像素R、G、B分量的不同,決定R、G、B分量所代表的具體紋理貼圖的混合因子。根據(jù)這個混合因子混合三個可重復貼圖后,將混合得到的最終顏色值輸出到屏幕上。 例如,令索引貼圖的R分量代表沙灘的紋理,G分量代表草地,B分量代表巖石。如果索引貼圖上一個像素的值是(0,255,0),即綠色,則這個像素所對應(yīng)的地形區(qū)域的具體紋理就為草地。如果該像素顏色值是(127,127,0),即黃色,則該像素所對應(yīng)的地形區(qū)域的紋理為草地和沙灘的混合,看起來既有草,又有沙。又如下圖顯示了一個樣本索引貼圖,以及使用該貼圖索引紋理之后的渲染效果。
原理很簡單,下面講解一下具體的實現(xiàn)過程。首先,我們準備4個紋理,其中1個紋理索引貼圖,它將被拉伸覆蓋整個地形,然后3張細節(jié)貼圖,并將它們綁定到相應(yīng)的紋理通道上。然后使用Vertex Shader為每個頂點自動計算索引貼圖的紋理坐標,在Fragment Shader里,對索引貼圖進行紋理查找,使用查找得到的顏色值的RGB顏色信息混合3張細節(jié)貼圖,得到當前像素的顏色。最后還應(yīng)該把這個顏色和光照貼圖中的值相乘,得到最終的結(jié)果。下面是相關(guān)的Shader代碼,使用GLSL編寫。
最后,如果你對本文有不解之處,歡迎和我共同討論。 posted on 2008-05-03 14:24 狂爛球 閱讀(1861) 評論(0) 編輯 收藏 引用 基于四叉樹空間劃分的地形實時渲染方法(轉(zhuǎn)載)自從我發(fā)布作品3D地形渲染以來,有很多朋友表示了對我所使用的地形LOD算法的興趣。于是我決定寫一篇文章具體地介紹我所使用的地形LOD以及表面紋理映射方法,供學習交流之用。這篇文章首先講解了基本基于高程數(shù)據(jù)的多邊形表示方法,然后詳細地講述了地形的質(zhì)量分級和視見體剔除方法。最后還介紹了如何為大型的地形創(chuàng)建穩(wěn)定的光照效果和細膩的紋理貼圖。聲明:本文前半部分講述的地形四叉樹算法最初來自《基于LOD的大規(guī)模真實感室外場景實時渲染技術(shù)的初步研究》一文,作者潘李亮。本人對算法進行了部分的更改,并提出了自己的觀點。文章后面所講述的“紋理索引貼圖”方法則是個人想出的方法。如要轉(zhuǎn)載本文,請聲明作者及出處。
地形是計算機圖形的一個重要組成部分,而它又具有特殊的形態(tài)。地形往往覆蓋面積極廣,且精度要求很高,使得我們必須用許多多邊形來描述。這樣的特點使得我們不能像對待其他普通模型那樣對待地形。要想實時地渲染地形,我們需要一些特殊的方法。 地形渲染一直以來都是計算機圖形學中一個重要的研究領(lǐng)域。并且在這一方面已經(jīng)誕生了許多優(yōu)秀的算法。其中包括基于體素的渲染方法,也有基于多邊形的渲染方法。早期的游戲,如三角洲特種部隊就是采用體素渲染法的成功例子。體素法類似光線追蹤渲染,它從屏幕空間出發(fā),找到地形與屏幕像素發(fā)出的射線交點,然后確定該像素的顏色。這種方法不依賴具體的圖形硬件,整個渲染過程完全使用CPU處理,因此它不能使用現(xiàn)代硬件來加速,并且對于一個場景來說,往往不只是地形,還有其他使用多邊形描述的物體,體素法渲染的圖像很難與硬件渲染的多邊形進行混合,因此這種方法現(xiàn)在用得極少。而多邊形渲染方法則成為一種主流。選擇多邊形來描述和渲染地形有很多的理由和優(yōu)點。最重要的是它能夠很好地使用硬件加速,并且能夠和其他多邊形對象一起統(tǒng)一管理。 已有大量優(yōu)秀的基于多邊形的地形渲染算法。比較經(jīng)典的算法有M. Duchaineau等人提出ROAM算法。這個算法采用一棵三角二叉樹來描述整個地形。一個地形在最初的層次上由兩個較大的等腰直角三角形組成,這兩個等腰直角三角形可以被不斷地細分來展現(xiàn)地形的更多細節(jié)。每一次細分過程都向直角三角形的斜邊的中點處增加一個由高程數(shù)據(jù)所描述的頂點,該點將所在的直角三角形一分為二,同時該算法也定義了一些規(guī)則來保證地形中不會因相鄰兩個三角形細節(jié)層次的不同而出現(xiàn)裂縫。這個算法已被許多游戲所采用。還有一類算法,通過將地形在X-Z投影面上不斷地規(guī)則細分來得到不同的細節(jié),這就是本文要介紹的四叉樹空間劃分算法。另外,最新提出的一個地形算法也不得不提,Hugues Hoppe在2004年提出的幾何裁剪圖方法(Geometry Clipmaps),算法使用了最新硬件所支持的頂點紋理來定義地形的外觀,并且對于距離攝影機不同遠近的地方采用不同的紋理層,最大限度地使用硬件加速了地形渲染的過程。這個方法聽起來非常美妙,但它目前只被較少的硬件支持。因為頂點紋理是Shader Model 3.0才支持的功能,也就是說只有DirectX 9.0c級別的顯卡才能支持這種算法。這對于某些有普及性要求的圖形應(yīng)用程序,尤其是對游戲來講不是一件好的事情。因此大多數(shù)人現(xiàn)在還在使用經(jīng)典的地形渲染方法。 首先,基于四叉樹的地形渲染方法使用高程數(shù)據(jù)作為數(shù)據(jù)源。且算法要求高程數(shù)據(jù)的大小必須為2n+1的正方形。所謂高程數(shù)據(jù),即色彩范圍在0-255的灰度圖片,不同的灰度代表了不同的高度值。如果某高程數(shù)據(jù)指出這個高程數(shù)據(jù)最高處的Y坐標值是4000,那么在高程數(shù)據(jù)中一個值為255的像素點就表示這個點所代表的地形區(qū)域的高度是4000,同理如果該像素值是127那么就表示這個點所代表的地形區(qū)域的高度是4000×(127/255)=2000。高程數(shù)據(jù)的每個像素都對應(yīng)所渲染網(wǎng)格中的一個頂點。另外還有一個參數(shù)描述頂點與頂點之間的水平距離,以及一個描述最大高度的參數(shù)。因此地形的基本數(shù)據(jù)結(jié)構(gòu)如下: struct Terrain 其中,各變量的具體意義如下圖所示: 有了這些參數(shù),我們可以很容易地由高程數(shù)據(jù)的參數(shù)值得到它所表述的多邊形網(wǎng)格。得到這個網(wǎng)格之后,可以簡單地把它放入頂點數(shù)組,并為之建立一個頂點索引,就可以傳入硬件進行渲染了。然而,事情并不是這么簡單。對于較小尺寸的高程數(shù)據(jù)(如129×129),這樣做確實可行,但隨著高程數(shù)據(jù)規(guī)模的增大,所需的頂點數(shù)和描述網(wǎng)格的三角形數(shù)會急劇膨脹。這個數(shù)值很快就會大到最新的顯卡也無法接受。比如一個1025×1025的高程數(shù)據(jù),我們需要1025×1025=1050625個頂點,以及1050625×2=2101250個三角形。就算你的顯卡每秒能夠渲染1000萬個三角形,你也只能得到不到5fps的渲染速度,況且你的場景可能還不只包括地形。因此我們必須想辦法在不影響視覺效果的情況下縮減所渲染的三角形數(shù)量,另外還應(yīng)該注意一次性將最多的數(shù)據(jù)預先傳給硬件以節(jié)約帶寬。 這里要講解的算法,目的就是在不影響或在視覺可以接受的范圍內(nèi)縮減所渲染三角形的數(shù)量,以達到實時渲染的要求。根據(jù)測試,本算法在漫游大小為1025*1025的地形時速度穩(wěn)定在150fps以上(在nVidia Geforce 6200 + P4 1.6GHz的硬件上得到)。 由于地形覆蓋范圍廣,但它的投影在XZ平面上均勻分布(以下采用OpenGL中的右手坐標系,Y軸為豎直向上的坐標軸),因此我們有必要考慮對地形進行空間劃分。正是由于這樣的均勻分布,給我們的劃分過程帶來了便利。我們不需要具體地去分割某個三角形,只要選擇那些過頂點且和X或Z軸垂直的平面作為劃分面即可。例如對于一個高程數(shù)據(jù),我們可以以坐標原點作為地形的中心點,然后沿著X軸和Z軸依次展開來分布各個頂點。如下如所示。 首先,我們可以選擇X=0和Z=0這兩個平面,將地形劃分為等大的四個區(qū)域,然后對劃分出來的四個子區(qū)域進行遞歸劃分,每次劃分都選擇交于區(qū)域中心點并且互相垂直的兩個平面作為劃分面,直到每個子區(qū)域都只包含一個地形單元塊(即兩個三角形)而不能再劃分為止。例如對于上圖所示9*9大小的地形塊,經(jīng)過劃分之后如下圖所示: 由圖可知,只有高程數(shù)據(jù)滿足大小2n+1的正方形這個條件,我們才可能對地形進行均勻劃分。我們可以把劃分結(jié)果用一棵樹來表述,由于每次劃分之后產(chǎn)生四個子節(jié)點,因此這棵樹叫四叉樹。那么,這棵樹中應(yīng)該存儲那些信息呢?首先對于每個節(jié)點,應(yīng)該指定這個節(jié)點所代表的地形的區(qū)域范圍。并不是把地形網(wǎng)格中實際的頂點放入樹中,而是要在樹中說明這個節(jié)點覆蓋了地形的那些區(qū)域。比如一個子節(jié)點應(yīng)該有一個Center(X,Y)變量,指定這個節(jié)點的中心點所對應(yīng)的頂點索引,或編號。為了方便起見,可以把地形中心點編號為(0,0)然后沿著坐標軸遞增。此外還要有個變量指定這個節(jié)點到底覆蓋了地形的多少個頂點。如下圖所示。 我們目前的四叉樹的數(shù)據(jù)結(jié)構(gòu)如下: struct QuadTreeNode 有了四叉樹之后,如何利用它的優(yōu)勢呢?首先我們考慮簡單的視見體裁剪(View Frustum Culling,以下簡稱VFC)。相信很多接觸過基本圖形優(yōu)化的人都應(yīng)該熟悉VFC,VFC的作用既是對那些明顯位于可見平截頭體之外的多邊形在把它們傳給顯卡之前剔除掉。這個過程由CPU來完成。雖然簡單,但它卻非常有效。VFC過程如下: 1.為每個節(jié)點計算包圍球。包圍球可以簡單的以中心頂點為球心,最大坐標值點(節(jié)點所覆蓋的所有頂點的最大X、Y、Z值作為此點的坐標值)到球心的距離為半徑。 2.根據(jù)當前的投影和變換矩陣計算此時可視平截頭體的六個平面方程。這一步可以參考Azure的Blog上的一篇文章,這篇文章給出了VFC的具體代碼。單擊這里。 3.從樹的根結(jié)點以深度優(yōu)先的順序遍歷樹。每次訪問節(jié)點時,測試該節(jié)點包圍球與視見體的相交情況。在下面的情況下,包圍球與視見體相交: 1) 球心在六個平面所包圍的凸狀區(qū)域內(nèi)部。 4.如果相交測試顯示包圍球和視見體存在交集,繼續(xù)遞歸遍歷此節(jié)點的4個子節(jié)點,如果此節(jié)點已經(jīng)是葉節(jié)點,則這個節(jié)點應(yīng)被繪制。如果不存在交集,放棄這個節(jié)點,對于這個節(jié)點的所有子節(jié)點不再遞歸檢查。因為如果一個節(jié)點不可見,那么其子節(jié)點一定不可見。 這樣,我們剔除了那些不在視見體內(nèi)的地形區(qū)域,節(jié)約了一些資源。但這還不夠。在某些情況下,VFC可能還會指出整個地形都可見,在這種情況下,將這么多三角形都畫出顯然是不可取的。 因此還要考慮地形的細節(jié)層次(LOD)。我們應(yīng)該考慮到,地形不可能所有部分都一樣平坦或陡峭。對于平坦的部分,我們用過多的三角形去描述是沒有意義的。而對于起伏程度較大的區(qū)域,只有較多的三角形數(shù)量才不讓人感到尖銳的棱角。再者,無論地形起伏程度如何,那些距離視點很遠的區(qū)域,也沒有必要花費太多的資源去渲染,畢竟它們投影到屏幕上的面積很小,對其進行簡化也是必要的。 既然我們要對起伏程度不同的區(qū)域采用不同的細節(jié)級別,我們首先必須找到一種描述地形起伏程度的量。與其說起伏程度,不如說是地形的某個頂點因為被簡化后而產(chǎn)生的誤差。要計算這個誤差,我們先要了解地形是如何被簡化的。 考慮下圖所示的地形塊,它的渲染結(jié)果如下圖右圖所示。 現(xiàn)在如果要對所需渲染的三角形進行簡化,我們可以考慮這個地形塊每條邊中間的頂點(下圖左側(cè)紅色點): 如果將這些紅色的頂點剔除,我們可以得到上圖右邊所示的簡化后的網(wǎng)格。誤差就在這一步產(chǎn)生。由于紅色的頂點被剔除后,原本由紅色頂點所表示的地形高度現(xiàn)在變成了兩側(cè)黑色頂點插值后的高度。這個高度就是誤差。如下圖。 因此,對于每個節(jié)點,我們先計算這個節(jié)點所有邊中點被刪除后所造成的誤差,分別記為ΔH1, ΔH2, ΔH3, ΔH4。如果這個節(jié)點包含子節(jié)點,遞歸計算子節(jié)點的誤差,并把四個子節(jié)點的誤差記為ΔHs1, ΔHs2, ΔHs3, ΔHs4。這個節(jié)點的誤差就是這八個誤差值中的最大值。由于這是一個遞歸的過程,因此應(yīng)該把這個過程加到四叉樹的生成過程中,并向四叉樹的數(shù)據(jù)結(jié)構(gòu)中加入一個誤差變量。如下。 struct QuadTreeNode 下面來看一下地形的具體渲染過程。 首先,我們位于四叉樹的根結(jié)點。我們此時考慮根結(jié)點的誤差,如果這個誤差小于一個閾值,直接使用根結(jié)點的中心點以及此節(jié)點的四個邊角點作為頂點渲染一個三角扇形,這個三角扇形就是渲染出來的地形。但是更經(jīng)常的情況下,根結(jié)點的誤差值是很大的,因此算法認為要對根結(jié)點進行細分,以展現(xiàn)更多細節(jié)。于是對于根結(jié)點的每個子節(jié)點,重復這個步驟,即檢查它的誤差值是否大于閾值,如果大于,直接渲染這個節(jié)點,如果小于,遞歸細分節(jié)點。目前我們的算法偽代碼如下。 procedure DrawTerrain(QuadTreeNode *node) 這個偽代碼在一個較高的層次上表述了算法的基本思想。然而我們還有許多問題要考慮。其一是目前我們僅僅考慮了地形的細節(jié)層次和地形表面起伏程度的關(guān)系,但還應(yīng)該考慮地形塊距離視點遠近跟地形細節(jié)層次的關(guān)系。解決這個問題很簡單,我們只需在偽代碼的條件中加入距離這一因素即可。即把 if (node->DeltaH > k) 改為: if (node->DeltaH / d > k) 其中d為節(jié)點中心點與視點之間的距離。而事實上,當細節(jié)程度與距離的平方成反比時,能夠減少更多的三角形,而且視覺效果更好,只要閾值k設(shè)置得當,根本感覺不出地形因為視點的移動而發(fā)生幾何形變。因此,我們最終的條件式為: node->DeltaH / d2 > k 還有一個很重要的問題,就是這個算法所產(chǎn)生的地形會因為節(jié)點之間細節(jié)層次的不同而產(chǎn)生裂縫。下圖說明了裂縫的產(chǎn)生原因。 有兩個方法可以解決這個問題,一個方法是刪除左側(cè)節(jié)點中產(chǎn)生裂縫的頂點,使兩條邊能夠重合。另一種方法是人為地在右側(cè)地形塊中插入一條邊,這條邊連接中心點和造成裂縫的頂點,從而消除裂縫。在渲染地形時,可以采取下面的辦法避免裂縫的產(chǎn)生: 1.在預處理階段,為所有頂點創(chuàng)建一個標記數(shù)組,標記以該頂點為中心點的節(jié)點在某一幀是否被細分。如果被細分則標記為1,否則標記0。 2.從根節(jié)點開始,以廣度優(yōu)先的順序遍歷四叉樹,使用之前提出的條件式判斷節(jié)點是否需要分割。如果公式表明需要分割,并且與節(jié)點相鄰的四個節(jié)點的中心點都被標記為1,那么把這個節(jié)點及其四個子節(jié)點的標記設(shè)為1,并遞歸細分這個節(jié)點。否則,將這個節(jié)點的標記設(shè)為1,把這個節(jié)點的四個子節(jié)點的標記設(shè)為0,然后采用下面的方法繪制這個地形塊: 1)將節(jié)點的中心頂點和四個邊角點添加到即將繪制的三角扇形列表中。 我們最終的偽代碼如下。
另外,一個重要的優(yōu)化是利用硬件的緩沖區(qū)或頂點數(shù)組(對于不支持頂點緩沖的硬件而言)。因為地形無論怎樣簡化,頂點數(shù)據(jù)總是固定不變的。我們在每一幀動態(tài)產(chǎn)生的僅僅是頂點索引,因此我們有必要實現(xiàn)將地形的所有頂點數(shù)據(jù)輸入到頂點緩沖中,然后在渲染時一次性將所有的索引傳給顯卡,以提高速度。實驗表明,使用頂點緩沖比直接使用glBegin/glEnd繪制圖形要快5倍以上。 以上講述了如何做到實時地渲染大型地形。主要應(yīng)用了LOD和VFC兩種手段來精簡三角形數(shù)量。然而VFC只能剔除不在視見體內(nèi)的圖形,而對于在視見體內(nèi)但被其他更近的物體遮擋的情況卻無能為力。如果要實現(xiàn)地形的自遮擋剔除,地平線算法是一個好的選擇。然而當你的場景不僅僅是包含地形時,地平線算法也只能處理地形的自遮擋情況。因為地平線算法只對2.5D的地圖(即在XZ平面上無重合投影的場景)有效。對于完全3D場景,地平線并不能很好的工作。所以當你在引擎中使用地形時,可以考慮將地形分塊后放入場景的管理樹中,如BSP或Octree等。然后根據(jù)引擎的性質(zhì)使用入口(Portal)、PVS或者遮擋測試(Occlusion Culling)等方法進行遮擋剔除。值得強調(diào)的是,遮擋測試是一個非常靈活的實時的剔除算法,且無需任何預計算過程。但要想有效的實現(xiàn)它并不是一件容易的事。我曾將地形分塊后使用遮擋剔除來完成地形的自遮擋,但是渲染速度不但沒有提升,反而有輕微的下降。因此如果要使用遮擋剔除的話必須和引擎結(jié)合起來統(tǒng)一進行遮擋測試,才有可能提高效率。 現(xiàn)在你應(yīng)該了解了基本的地形實時渲染方法。要想讓地形的外觀更加真實,我們還需要更多的工作。我們需要為地形加上紋理貼圖和光照。首先考慮地形的光照。由于地形的多邊形網(wǎng)格是實時產(chǎn)生的,它會隨著視點的移動而變化,因此如果你直接使用OpenGL內(nèi)置的頂點光照,你會得到極度不穩(wěn)定的光照效果。你會看到地形表面會因為你的移動而不斷跳動。因此我們必須使用其他的光照方法來避免這個問題。我們想到了光照貼圖。光照貼圖是一個游戲中常用的光照技術(shù)。它是一個覆蓋了場景中所有多邊形的貼圖。通過給貼圖賦值,我們可以得到多邊形表面復雜的光照效果。使用好的算法計算出來的光照貼圖可以模擬極度逼真的光影效果。它給我們帶來的視覺享受遠遠地超過了OpenGL的內(nèi)置光照。有關(guān)光照貼圖的計算可以參考我翻譯的一篇文章:輻射度算法(Radiosity) 你可以簡單地為地形覆蓋上單一的紋理,這看起來些許增加了地形的真實性: 在上圖中,我們創(chuàng)建了一個地形,并運用了一個重復的紋理。這個過程讓地形的無論哪一個區(qū)域看起來都是一樣的(例如都是草地)。這顯然不太真實,也過于乏味。或許你會創(chuàng)建了一幅超大的圖片,以拉伸覆蓋的方式映射到地形表面。這樣做的后果是內(nèi)存開銷過于龐大,這樣做也很會受到硬件的限制。因此我們應(yīng)該使用一種更好的紋理貼圖方式,紋理索引貼圖。 紋理索引貼圖對三個可重復的紋理進行索引貼圖。所謂索引貼圖,就是對三個可重復紋理進行索引,以決定地形的哪些區(qū)域需要使用哪些紋理的混合來貼圖。因為對于任意的貼圖,都由一組包含3個顏色通道(即R、G、B)的像素組成。用于索引的貼圖的像素并不表示地形的某個區(qū)域的具體顏色,而是表示地形的某個區(qū)域用何種具體的紋理貼圖。因為具體的紋理細節(jié)存儲在這三個可重復的紋理中,因此索引貼圖的貼圖方式也為拉伸到地形表面,但它的分辨率可以大大降低。 紋理索引貼圖的工作方式如下:對于地形投影到屏幕上的像素,查找該像素所映射到索引貼圖上的像素。然后根據(jù)這一像素R、G、B分量的不同,決定R、G、B分量所代表的具體紋理貼圖的混合因子。根據(jù)這個混合因子混合三個可重復貼圖后,將混合得到的最終顏色值輸出到屏幕上。 例如,令索引貼圖的R分量代表沙灘的紋理,G分量代表草地,B分量代表巖石。如果索引貼圖上一個像素的值是(0,255,0),即綠色,則這個像素所對應(yīng)的地形區(qū)域的具體紋理就為草地。如果該像素顏色值是(127,127,0),即黃色,則該像素所對應(yīng)的地形區(qū)域的紋理為草地和沙灘的混合,看起來既有草,又有沙。又如下圖顯示了一個樣本索引貼圖,以及使用該貼圖索引紋理之后的渲染效果。
原理很簡單,下面講解一下具體的實現(xiàn)過程。首先,我們準備4個紋理,其中1個紋理索引貼圖,它將被拉伸覆蓋整個地形,然后3張細節(jié)貼圖,并將它們綁定到相應(yīng)的紋理通道上。然后使用Vertex Shader為每個頂點自動計算索引貼圖的紋理坐標,在Fragment Shader里,對索引貼圖進行紋理查找,使用查找得到的顏色值的RGB顏色信息混合3張細節(jié)貼圖,得到當前像素的顏色。最后還應(yīng)該把這個顏色和光照貼圖中的值相乘,得到最終的結(jié)果。下面是相關(guān)的Shader代碼,使用GLSL編寫。
最后,如果你對本文有不解之處,歡迎和我共同討論。 |
|