| 趙 剛 引言 在三維遊戲等建立的虛擬世界中要求虛擬情境具有很高的逼真度,其中的三維地形逼真度是關鍵之一。然而三維地形的產生和繪製需要巨大的計算量,實景地形的產生還需要地形資料庫的支援,在運算能力非常有限的PC機中即時產生逼真的實景三維地形一直是業界的一個難題。三維地形的產生方法經過了多年的探索,現已形成一系列優秀的演算法,本文介紹的演算法是一種入門的演算法,學習該演算法可為系統學習三維地形產生演算法打下基礎,該演算法複雜度較低,運算快速,產生的地形可以滿足小規模地形可視化的要求。 一、演算法實現過程 Direct3D立即模式中,三維繪圖使用DrawPrimitive方法,DrawPrimitive方法將3D 模型分解為基本的點,線,和三角形面三種,本三維地形產生法中將地形全部分解成三角形,使用DrawPrimitive方法繪製一系列三角形,從而繪製出整個地形。DrawPrimitive繪製三角形的方法有三種,第一種是繪製離散的三角形,每個三角形分別指定三個頂點,這種方法適合繪製零散的三角形,對於繪製成片的三角形效率較低。第二種是繪製三角形序列,第一個三角形指定三個頂點,其餘的三角形只需要指定一個頂點,另外兩個取前一個三角形的最後兩個頂點,這樣繪製的三角形全部串連在一塊兒,適合於繪製成片的三角形。第三種是繪製三角形組成的扇形,以第一個頂點作為所有三角形個公用頂點,為成一個扇形,該方法只適合繪製扇形類的面。因此本地形產生方法採用第二種方法,但將地形中所有的三角形的頂點座標按首尾相接方式排列起來以適合於第二種繪製三角形方法很難,幸運的是Direct3D立即模式提供了頂點索引的繪圖方法,只要將待繪製頂點的編號傳遞給DrawPrimitive的姊妹函數DrawIndexedPrimitive,就可以完成和DrawPrimitive一樣的功能。頂點在記憶體中採用數組的方式儲存,因此頂點編號是順序編號。因此只要提供一個使三角形的頂點按首尾相接的索引序列,就可以完成高效的繪圖。 本三維地形產生方法產生地形的過程如下: 第一步:初始化一個正方形網路(1),網格數為64×64(規模太小,產生的地形不逼真,規模太大,PC機難以處理,因此經過多次實驗,採用64×64格最為合適)每個正方形網格用兩個三角形表示。網格的邊長由可視區內的地面大小決定,比如網格邊長為50米,可見的地面大小為50×64=3200米。(網格的邊長小,產生的地面就小,網格的邊長大,產生的地面就大,但地面的逼真度降低,實驗表明採用50米比較合適)。 圖1 正方形網格(4×3) 頂點包含的資訊有: 1. 位置座標 x、y、z(x表示左右方向,y表示垂直方向,z表示縱深方向) 2. 法線座標 nx、ny、nz(表示該點周圍地面陡峭情況) 3. 顏色 diffuse(表示該點對光線的反射性質) 4. 紋理座標 tu、tv(tu表示紋理橫座標,tv表示紋理縱座標) 定義一個結構儲存這些資訊: struct VERTEX { D3DVALUE x,y,z; D3DVALUE nx,ny,nz; D3DCOLOR diffuse; D3DVALUE tu,tv; }; 在64×64的網格中,頂點的數量為(64+1)×(64+1)=4225,因此可聲明一個長度為4225的數組儲存頂點資訊:VERTEX m_Vertex[4225]; 頂點的編號規則為從左至右,從上到下順序編號,這樣第一行第一列的頂點為0號,第一行第二列的頂點為1號,第二行第一列的頂點為65號,依次類推……,因此64×64的正方形網的頂點可初始化如下: for(j=0;j<65;j++) { for(i=0;i<65;i++) { m_Vertex[j*65+i].x=-m_Length*0.5f+m_Block*i; m_Vertex[j*65+I].y=0.0f; m_Vertex[j*65+i].z= m_Length*0.5f-m_Block*j; m_Vertex[j*65+i].nx=0.0f; m_Vertex[j*65+i].ny=1.0f; m_Vertex[j*65+i].nz=0.0f; m_Vertex[j*65+I].diffuse=0xffffffff; } } 其中 m_Length 表示可見地面長度(3200米)m_Block 表示網格長度(50米) 按這種方式初始化完的正方形網是一張平面(座標y均為0),它表示的地面將是一片平地,不含高程資料,高程資料的加入將在稍後介紹。 為了使用DrawPrimitive第二種繪製三角形的方式繪圖,必須製作一頂點索引序列,使頂點按三角形首尾相接的順序排列。該頂點索引序列可如下賦值: WORD m_Index[64*64*6]; for(j=0;j<64;j++) { for(i=0;i<64;i++) { m_Index[j*64*6+i*6+0]=(WORD)(j*65+i); m_Index[j*64*6+i*6+1]=(WORD)(j*65+i+65); m_Index[j*64*6+i*6+2]=(WORD)(j*65+i+65+1); m_Index[j*64*6+i*6+3]=(WORD)(j*65+i); m_Index[j*64*6+i*6+4]=(WORD)(j*65+i+1); m_Index[j*64*6+i*6+5]=(WORD)(j*65+i+65+1); } } 地形的高程資料存貯在一個512×512像素的256色BMP檔案裡,該圖形檔案按實際地形高程測繪,使用圖形中的紅色分量表示地面的海拔高度,紅色分量從0~255共有256級,這樣地面的高度也只有256級,這樣的精度對於三維地形模擬已經足夠。本方法中,為進一步提高地形的真實度,還在地形中融入了水面的效果,只要將高程圖的紅色分量指定為0,將產生水面而非地面,要指定為陸地,紅色分量範圍為1~255。高程圖中的綠色分量用來指定是否有樹林,綠色分量越大,表示樹林越密,同時這片土地將呈現為草地效果,但綠色分量不可濫用,因為繪製樹木很費時間,樹木過多將降低程式地即時性,一般將可見地樹木數量限制在500棵以內。典型地高程圖 2。 圖2 典型高程圖 圖中為了直觀,將紅色成分為零的地區繪製成藍色,可以明顯的看到水域部分構成了一條河流和兩個湖泊。 高程圖的解析度為512像素×512像素,地形網格的每一個頂點對應一個像素,而頂點間隔為50米,這樣這幅高程圖表示的地形範圍為邊長512×50=25600米(25.6公裡)的正方形地區(655.36平方公裡)這對一般的三維情境已經夠用,如果仍不夠用,可增大高程圖的解析度,或採用多幅高程圖拼接,但會消耗更多的記憶體。 要使地面逼真,地面還應該貼上一層紋理圖,紋理圖可以是典型地面的照片,或用影像處理軟體製作而成。為了正確貼上紋理圖,應該該頂點指定紋理座標,因顯示儲存空間是很有限的,所以紋理圖不可能很大,相對於巨大的地面來說,紋理圖顯得非常不足,因此紋理圖要重複使用,就好像貼地磚一樣,貼在地面上,這樣帶來的問題是地面上的紋理呈現周期性,就好像真的是地磚鋪的一樣,而且紋理圖的拼接處會出現難看的裂紋,要減少這些現象,首先紋理圖不能太小(本方法中採用1024×1024像素的位元影像)其次,紋理圖的邊緣要做特殊處理,使紋理拼接的時候不出現裂縫(這種圖叫做可拼接圖,廣泛用於網頁的底紋)本方法中紋理圖的邊緣不用做特殊處理,也不會出現裂縫,因為本法在貼紋理的時候讓拼接處的兩個紋理圖成鏡像關係,因此邊緣的圖形一致,不會錯位,但缺點是可以見到很多對稱的花紋,用小紋理拼接地表的方法有待改進,典型的地面紋理圖3。 圖3 土質地面紋理圖 為了兼顧紋理的細膩度和低周期性,本法讓每個網格擁有的紋理大小為64×64像素,這樣一張1024×1024像素的紋理圖可覆蓋16×16個網格(也就是800米×800米的面積)紋理座標的演算法如下: long p,q,a,b; float x,y; x= m_CenterPos[0]*m_ReciBlock-32; y=-m_CenterPos[2]*m_ReciBlock-32; for(j=0;j<65;j++) { for(i=0;i<65;i++) { p=(((DWORD)x+150+m_TexWidth/2+i)*64)%m_TexWidth; q=(((DWORD)y+150+m_TexHeight/2+j)*64)%m_TexHeight; a=(((DWORD)x+150+m_TexWidth/2+i)*64)/m_TexWidth; b=(((DWORD)y+150+m_TexHeight/2+j)*64)/m_TexHeight; if(a%2) m_Vertex[j*65+i].tu=(float)p/m_TexWidth; else m_Vertex[j*65+i].tu=(float)(m_TexWidth-p)/m_TexWidth; if(b%2) m_Vertex[j*65+i].tv=(float)q/m_TexHeight; else m_Vertex[j*65+i].tv=(float)(m_TexHeight-q)/m_TexHeight; } } 其中 m_CenterPos[0],m_CenterPos[2]是可見地面中心的水平座標 m_TexWidth,m_TexHeight 是紋理的寬度和高度 m_ReciBlock 是網格邊長的倒數(1/50) 變數 m_CenterPos[0],m_CenterPos[2]用於確定繪製地面的中心座標,在該座標周圍的指定距離內的地面將被繪製,當三維情境中的觀察點移動時,地面中心座標也要做相應的移動,否則觀察點移動一定位置後,將會看到地面的邊界,甚至看不到地面。一般來說,三維情境中可見地區是一個錐形,觀察點位於錐頂,錐底垂直於觀察方向,並向觀察方向延伸。對於這樣可視區,使地面的中心座標和觀察點的水平座標一致是不可取的,因為在觀察點後面有和觀察點前面同樣大小的一塊地面被繪製了,但是卻看不到,白白浪費時間,因此應將地面的中心座標置於觀察點正前方一定的距離處,對於邊長為3200米的地面,這個距離取1200米是比較合適的。因此程式要負責把觀測點正前方,距離觀察點1200米的那個點的座標算出來,用這個座標給m_CenterPos[0],m_CenterPos[2]賦值,4。這樣仍有一半左右的地面位於可視區之外,採用其他方法避免非可視區內地面的繪製。 圖4 地面中心在可視區內的選擇 到現在為止地面還是一張平面,還沒有將高程資料寫到頂點座標裡。下面將介紹如何將高程資料寫入頂點座標裡。高程資料順序存貯在512×512像素的影像檔裡。我們認為映像的中心為座標原點,往上和往右是座標增長的方向,這樣可以算得,映像的左上端點像素(即第一個像素)儲存著座標為(-256×50,256×50)即(-12800,12800)地面的高程資料,第二個像素存貯著座標為(-12750,12800)地面的高程資料一次類推,因此可以根據地面的水平座標去高程映像中定址,擷取高程資料,高程資料加入網格中後5所示。 圖5 高程資料加入地形網格中
為了提高繪製速度,本法對地形網格中每一個網格都進行一次可見度判斷,如果網格位於可視區內,就繪製該網格,否則不繪製。本地形中共有64×64=4096個網格,要進行4096次判斷,但判斷可見度的函數本身運行較慢,過多的判斷反而適得其反,因此,本法中對網格可見度的判斷以四個為一組,共判斷1024次,四個中只要有一個位於可視區內就認為四個都可見,否則不可見。這樣雖然繪製效率有所降低,但卻大大節省了可見度判斷時間。實驗證明以四個一組的分法總體運行速度最高。 本法又聲明了另外一個頂點索引數組 WORD m_IndexTemp[4225],對於可見度判斷成功的頂點,與其相關的頂點索引將存入m_IndexTemp,否則不存。這樣m_IndexTemp裡只含有在可視區內的頂點索引,使DrawPrimitive繪製最少的三角形,最大程度提高速度。 對於高程圖中紅色分量為零的地區,通過以下方法將地表描述為水面: 1. 地表的顏色為淺藍色,半透明。 2. 地表的紋理座標周期性移動,使水面具有流動感。 處理如下: for(j=0;j<65;j++) { for(i=0;i<65;i++) {…… if(altitude==0) //水域地帶 { m_Vertex[j*65+i].diffuse=0x600030ff; //淺藍色,半透明 if(Count/16%2) //處理水的流動感 { p=(((DWORD)x+150+m_TexWidth/2+i)*64+Count%16)%m_TexWidth; q=(((DWORD)y+150+m_TexHeight/2+j)*64+Count%16)%m_TexHeight; a=(((DWORD)x+150+m_TexWidth/2+i)*64+Count%16)/m_TexWidth; b=(((DWORD)y+150+m_TexHeight/2+j)*64+Count%16)/m_TexHeight; } else { p=(((DWORD)x+150+m_TexWidth/2+i)*64+16-Count%16)%m_TexWidth; q=(((DWORD)y+150+m_TexHeight/2+j)*64+16-Count%16)%m_TexHeight; a=(((DWORD)x+150+m_TexWidth/2+i)*64+16-Count%16)/m_TexWidth; b=(((DWORD)y+150+m_TexHeight/2+j)*64+16-Count%16)/m_TexHeight; } } if(a%2) m_Vertex[j*65+i].tu=(float)p/m_TexWidth; else m_Vertex[j*65+i].tu=(float)(m_TexWidth-p)/m_TexWidth; I f(b%2) m_Vertex[j*65+i].tv=(float)q/m_TexHeight; else m_Vertex[j*65+i].tv=(float)(m_TexHeight-q)/m_TexHeight; } } 其中 Count是計數器,每0.1秒數值增加1。 對於高程圖中綠色不為零的地區為樹林,通過以下方法實現 1. 地面的顏色為暗綠色,表示草地 2. 隨機產生樹木座標,在該樹木座標上用公用板技術顯示樹木。 實施如下: if(m_ShowTree) //建立樹木參數 { long r,s; if(green>0&&m_TreeNum { r=abs((long)((m_Vertex[j*m_Row+i].tu +m_Vertex[j*m_Row+i].tv*10)*10000)%1000); for(s=0;s { vect.x=m_Vertex[j*m_Row+i].x+m_RandPos[s+r][0]; vect.z=m_Vertex[j*m_Row+i].z+m_RandPos[s+r][2]; vect.y=m_Vertex[j*m_Row+i].y; D3DMath_VectorMatrixMultiply(vect,vect,m_Matrix); vect.y=GetHeight(vect.x*0.01f,vect.z*0.01f,FALSE)*100.0f; m_TreePos[m_TreeNum][0]=vect.x; m_TreePos[m_TreeNum][1]=vect.y; m_TreePos[m_TreeNum][2]=vect.z; m_TreePos[m_TreeNum][3]=(float)((long)(green+s+r)%5); m_TreeNum++; } } } 其中m_ShowTree 是邏輯量,為真時才繪製樹木(可以不繪製樹木以提高速度) m_TreeMax 用來控制樹木數量, 當m_TreeNum等於m_TreeMax時不再增加樹木。 GetHight()是一個函數,用來計算地面高度。 最後繪製地形調用DrawIndexedPrimitive如下: m_pd3dDevice->SetTexture(0,m_pTexture); m_pd3dDevice->SetTransform(D3DTRANSFORMSTATE_WORLD,&m_Matrix); m_pd3dDevice->SetRenderState(D3DRENDERSTATE_CULLMODE,D3DCULL_NONE); m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,D3DFVF_VERTEX, m_Vertex, m_VertexNum,m_IndexTemp,m_IndexNum,NULL); 圖6 最終效果樣本 二、結束語 本文介紹的三維地形快速產生演算法為典型的基於高度圖的均勻網格地形產生演算法,具有學習三維地形產生演算法的入門指導作用。實驗證明,在配置為PIII 667MHz,128MB記憶體,Matrix G400顯示卡的電腦中以解析度為1024×768運行,速度達到35幀/秒以上。該演算法的程式碼已經使用在某大型工程中。 |