【Visual C++】遊戲開發四十八 淺墨DirectX教程十六 三維地形系統的實現


本系列文章由zhmxy555(毛星雲)編寫,轉載請註明出處。
作者:毛星雲(淺墨)    郵箱: [email protected]

上個星期淺墨寫的介紹三維攝像機的文章和示例程序放出以後,大家似乎都表現出了很高漲的熱情,不少朋友評論或者給淺墨發郵件問什麼時候講地形和天空頂。本來淺墨是準備這個星期就開始講可編程渲染流水線的,看大家這麼強烈的要求,淺墨決定乾脆把準備在後面講的地形天空一氣呵成,跟在攝像機後面一起講了得了。所以,這篇文章就誕生了。

先上一張配套示例程序的截圖:

1363566478_4817.png

修改頂點間距和縮放比例可以得到更廣闊,更陡峭的山峯:

1363595075_8916.png

想創造出極具真實感的三維遊戲世界,三維地形的模擬是必不可少,至關重要的。

三維地形模擬其實是一個很廣闊的課題,它其實不僅僅侷限於我們的遊戲開發領域,在三維仿真,虛擬現實等領域都涉及。說起三維地形模擬,似乎有那麼一絲神祕,其實,只要瞭解其實現原理了這所謂的地形系統模擬也就是紙老虎一隻。這篇文章裏我們就來揭開三維地形模擬的面紗,看看到底怎樣利用一個C++類的書寫,實現我們專屬的三維地形系統,然後就只需幾句代碼,兩幅圖片,一個“活生生”的三維地形就躍然紙上了。

一、三維地形繪製思路分析


關於地形繪製的大體思路,其實非常簡單,讓我們先來看三幅圖。


1363567036_2999.png


1363567054_8987.jpg


1363567071_7331.jpg


我們可以發現,以上的三幅圖就概括了三維地形模擬的大體走向與思路。

首先是第一幅圖,我們在圖中可以看到,圖中描繪的就是在同一平面上的三角形網格組成的一個大的矩形區域。在這裏我們把他看做是一張大的均勻的同一平面上的“漁網”,顯然它是一個二維的平面。圖中的每一個頂點都可以用一個二維的座標(x,y)來唯一表示。

然後第二幅圖,我們就像“揠苗助長”一樣,拉着第一幅圖中的“漁網”的某些頂點往上提(或者往下壓)。這裏往上提一點,那裏提一點,這樣,我們就爲每一個頂點都賦予了一個高度(就算有的頂點沒有移動,它的高度就爲0),第一幅圖中的漁網就變形了,成了三維圖形了。每個頂點就都有了一個高度值。用z座標來表示這個高度值的話,那麼現在三維空間中這個變形的“漁網”中的每個頂點都可以用(x,y,z)來唯一表示。

最後第三幅圖,在第二幅圖中的三維“漁網”的表面我們“鍍上”紋理不盡相同的“薄膜”,也就是進行了一個紋理包裝的過程。這樣奇蹟就發生了,逼真的雪原山川,奇峯怪石展現在了我們眼前。

所以,繪製三維地形的玄機,就被這三幅圖聯手一語道破了。

其中,第二幅圖中的那個“揠苗助長”的過程可謂三維地形繪製的一招“妙棋”。

這招“妙棋”我們常常是藉助高度圖來完成。下面我們就來講一講什麼是高度圖。


二、關於高度圖


高度圖在三維地形模擬中扮演着非常重要的角色。下面讓我們來一起探討一下高度圖的方方面面。

1.高度圖的概念

高度圖說白了其實就是一組連續的數組,這個數組中的元素與地形網格中的頂點一一對應,且每一個元素都指定了地形網格的某個頂點的高度值。當然,高度圖至少還有一種實現方案,就是用數值中的每一個元素來指定每個三角形柵格的高度值,而不是頂點的高度值。

高度圖有多種可能的圖形表示,其中最常用的一種是灰度圖(grayscale map)。地形中某一點的海拔越高的話,相應地該點對應的灰度圖中的亮度就越大。下面就是一幅灰度圖:

1363567097_1117.png



我們通常只爲每一個元素分配一個字節的存儲空間,這樣高度也就只能在0~255之間取值。

因此,地形中最低點將用0表示,而最高點使用255表示(當然,這樣做可能會 出現一些問題,比如地形中大部分區域的高度差別都不大,但是有少數地方高度差特別大時,不過大多數情況下這個系統都能運行的很好)

這個範圍大體上來反應地形中的高度變化完全沒問題,但是在實際運用中,爲了匹配3D世界的尺寸,可能需要對高度值進行比例變換,然而一進行比例變換,往往就可能超出上面的0~255這個區間。所以我們把高度數據加載到應用程序中時,我們重新分配一個整型或者浮點型的數組來存儲這些高度值,這樣我們就不必拘泥於0~255這個範圍,這樣就可以隨心所欲地構建出我們心儀的三維世界了。

對於灰度圖中的每個像素來說,同樣使用0~~255之間的值來表示一個灰度。這樣,我們就能把不同的灰度映射爲高度,並且用像素索引表示不同網格。

要從高度圖創建一個地形,我們需要創建一個與高度圖相同大小的頂點網格,並使用高度圖上每個像素的高度值作爲頂點的高度。例如,我們可以使用一張6×6像素分辨率的高度圖生成一個6×6大小的頂點網格。

網格上的頂點不僅包含位置,還包含諸如法線和紋理座標的信息。下圖就是一個在XZ平面中的6×6大小的頂點網格,其中每個頂點的高度對應在Y座標上。


1363567181_9793.png

另外我們在設計三維地形模擬系統的時候,會指定一下相鄰頂點的距離(水平距離和垂直距離一樣)。這個距離在上圖中用“Block Scale”表示。這個距離如果取小一點的話,會使頂點間的高度過渡平滑,但是會減少網格也就是三維地形的整體大小;反之,相鄰間頂點的距離取大一點的話,頂點間的過渡會變得陡峭,同時網格也就是三維地形的整體尺寸會相對來說變大。在上圖中,如果兩個頂點間的距離我們設爲1米的話,那麼所生產地形的大小就是25平方米,很好理解吧。

最常用的灰度圖格式是後綴名爲RAW,我們在這裏使用的高度圖文件格式就是RAW,這個格式不包含諸如圖像類型和大小信息的文件頭,所以易於被讀取。RAW文件只是簡單的二進制文件,只包含地形的高度數據。在一個8位高度圖中,每個字節都表示頂點的高度。

1363567195_1665.jpg


2.高度圖的製作

高度圖的製作一般有兩種方式。

1、以某種算法爲基礎,寫個程序生成。比較有名的是Fault Formation和Midpoint Displacement這兩種算法。

2、通過圖像編輯軟件,三維建模軟件,或者專業製作地形的軟件來製作。

圖像編輯軟件首當其衝的當然是Photoshop,這個就是我們今天準備教大家的高度圖生成方式。(先把後面兩種介紹完,稍後就教大家怎麼做高度圖。)

三維建模軟件就如我們之前介紹過的3DS Max和Maya了,地形制作也是三維建模界的一個分支。

然後專業製作地形的軟件,比如一款叫Terragen。這款軟件用起來也很方便,大家不妨google一下去下一個玩玩。

3、用Photoshop製作高度圖

接下來,淺墨來教大家使用Photoshop生成高度圖。

1.打開Photoshop(淺墨用的是Photoshop CS6),【Ctrl+N】或者依次點擊菜單欄上的【文件】->【新建】,新建一個畫布。如下圖,我們的畫布的大小取64x64像素。

1363567462_2572.png


2.創建完畫布,接下來就是最關鍵的一步。依次點擊菜單欄上的【濾鏡】->【渲染】->【雲彩】。


這時候,我們就可以發現,我們創建的空白畫布上有了隨機的灰度顏色值,如果你對這次生成的隨機灰度圖不滿意的話,大可再次點擊【濾鏡】->【渲染】->【雲彩】(或者【Ctrl+F】)來重新生成一次隨機的灰度效果圖,直到顏色分佈滿意爲止。我也也可以用畫筆來在圖上塗抹,自己來設定高度。這是淺墨通過處理後得到的一張灰度圖,這樣後面如果我們用這張圖作爲高度圖,得到的就是一個凹下去型的愛心地形圖:

1363567490_3661.png

記得在用【雲彩】濾鏡的時候,最好把調色板的顏色前景色設爲純黑色,不然可能得到的隨機灰度圖效果出不來。即調色板中的顏色設置成如下圖:

1363567505_7277.png


另外,我們可以通過對圖片色階的調整,來對生成的灰度圖的整體顏色進行調節。比如想讓地形整體來說高一些,就把灰度圖整體調亮一些,反之,地形整體來說要顯得低一些的話,就把繪圖圖整體調按。色階對話框通過【圖像】->【調整】->【色階】打開,或者直接按快捷鍵【Ctrl+F】。

另外在點擊【圖像】->【調整】後彈出的對話框中還有曲線、色相、飽和度等等選項,大家不妨也試試。

製作完成,我們點擊【文件】->【儲存爲…】或者直接按快捷鍵【Shift+Ctrl+S】來製作好的高度圖進行保存。保存的格式隨意,因爲我們稍後寫的一個地形類原則上支持幾乎所有的圖片格式高度圖的導入,只不過對有些圖片格式得到的效果圖比較奇葩而已。

這裏我們選擇8位的raw格式:

1363602375_2453.png


點擊確定後,會彈出如下導出raw的對話框,記得要把【通道儲存在】這個選項改成非隔行順序,如圖:

1363567569_8114.png


其實,大家不想用Photshop的話,可以直接google一下“heighmap”,搜索結果中隨便找就是一張現成的,然後改成raw格式就好了。原則上我們可以直接隨便拿一張任意格式的圖片來做高度圖使用,只是可能做出來的地形顯得怪異一點而已。

4.在程序中讀取高度圖

讓我們針對使用最廣泛的raw類型的高度圖進行講解。由於raw格式文件是按字節爲單位保存圖像中的每個像素的灰度值的,那麼我們可以容易地讀取保存在該文件中的高度信息。在這次的地形類的實現中,我們用到了C++中模板以及文件流的知識,如果對下面這段代碼不太熟悉的話就去看看《C++Primer》的相應章節吧。好了,下面貼出詳細註釋的代碼:

// 從文件中讀取高度信息     std::ifstream inFile;         inFile.open(pRawFileName,std::ios::binary);   //用二進制的方式打開文件       inFile.seekg(0,std::ios::end);                                              //把文件指針移動到文件末尾     std::vector<BYTE>inData(inFile.tellg());                 //用模板定義一個vector<BYTE>類型的變量inData並初始化,其值爲緩衝區當前位置,即緩衝區大小       inFile.seekg(std::ios::beg);                                                       //將文件指針移動到文件的開頭,準備讀取高度信息     inFile.read((char*)&inData[0],inData.size());    //關鍵的一步,讀取整個高度信息     inFile.close();                                                                                          //操作結束,可以關閉文件了



且由於保存在raw文件中的每個灰度數據只是用一個字節存儲的,那麼這樣所表示的地形高度只能在[0,255]之間取值。我們顯然不高興這樣。所以,我們繼續將讀取的高度信息重新保存到一個浮點型的模板類型中,這樣就能舒心地取到任何範圍的高度值了。注意下面這段代碼中vHeightInfo的定義是在類頭文件中的:

   std::vector<FLOAT>   m_vHeightInfo;               // 用於存放高度信息 …………     m_vHeightInfo.resize(inData.size());                                //將m_vHeightInfo尺寸取爲緩衝區的尺寸        //遍歷整個緩衝區,將inData中的值賦給m_vHeightInfo     for (unsigned int i=0; i<inData.size();i++)                      m_vHeightInfo[i] = inData[i];



三、地形類輪廓的書寫


在繼續展開講解之前,讓我們先來把這個地形類的整體輪廓給勾勒出來。這個類我們取名爲TerrainClass,它能通過載入二進制類型的文件(以raw格式爲首)來得到地形的高度信息,通過載入圖片得到地形所採用的紋理。載入文件的過程我們封裝在一個名爲LoadTerrainFromFile的函數中。

在上文中講高度圖的概念相關知識的時候我們就提到過,需要把高度圖所傳達的信息轉化到頂點網格中去,這樣纔好繪製出來。所以在類中既是重點也是難點的就是這個“轉化”的過程,這個過程我們放到一個名爲InitTerrain的函數中。高度圖到頂點的“轉化”完成後,接下來當然需要把這些頂點配合着紋理都繪製出來,繪製的過程我們放在一個名爲RenderTerrain的函數中。加上構造函數和析構函數,FVF頂點格式的定義以及若干必須的成員變量,我們就可以勾勒出TerrainClass類的輪廓如下,即下面貼出來的是Terrain.h頭文件的全部代碼:

//============================================================================= // Name: TerrainClass.h //	Des: 一個封裝了三維地形系統的類的頭文件 // 2013年 3月17日  Create by 淺墨  //=============================================================================  #pragma once  #include <d3d9.h> #include <d3dx9.h> #include <vector> #include <fstream> #include  "D3DUtil.h"  class TerrainClass { private:     LPDIRECT3DDEVICE9		m_pd3dDevice;			//D3D設備     LPDIRECT3DTEXTURE9		m_pTexture;			//紋理     LPDIRECT3DINDEXBUFFER9	m_pIndexBuffer;			//頂點緩存     LPDIRECT3DVERTEXBUFFER9	m_pVertexBuffer;		//索引緩存      int							m_nCellsPerRow;		// 每行的單元格數     int							m_nCellsPerCol;			// 每列的單元格數     int							m_nVertsPerRow;		// 每行的頂點數     int							m_nVertsPerCol;			// 每列的頂點數     int							m_nNumVertices;		// 頂點總數     FLOAT						m_fTerrainWidth;		// 地形的寬度     FLOAT						m_fTerrainDepth;		// 地形的深度     FLOAT						m_fCellSpacing;			// 單元格的間距     FLOAT						m_fHeightScale;			// 高度縮放係數     std::vector<FLOAT>   m_vHeightInfo;			// 用於存放高度信息  	//定義一個地形的FVF頂點格式     struct TERRAINVERTEX     {         FLOAT _x, _y, _z;         FLOAT _u, _v;         TERRAINVERTEX(FLOAT x, FLOAT y, FLOAT z, FLOAT u, FLOAT v)              :_x(x), _y(y), _z(z), _u(u), _v(v) {}         static const DWORD FVF = D3DFVF_XYZ | D3DFVF_TEX1;     };  public:     TerrainClass(IDirect3DDevice9 *pd3dDevice); //構造函數     virtual ~TerrainClass(void);		//析構函數  public:     BOOL LoadTerrainFromFile(wchar_t *pRawFileName, wchar_t *pTextureFile);		//從文件加載高度圖和紋理的函數     BOOL InitTerrain(INT nRows, INT nCols, FLOAT fSpace, FLOAT fScale);  //地形初始化函數     BOOL RenderTerrain(D3DXMATRIX *pMatWorld, BOOL bDrawFrame=FALSE);  //地形渲染函數 }; 



四、地形頂點的計算

下面我們來看看如何計算出地形中的每個頂點。

在計算頂點之前,還需要做一些準備工作。在創建地形時,需要通過指定地形的行數、列數以及頂點間的距離來指定地形的大小。上面我們在給類寫輪廓的時候剛貼出來過,封裝着地形頂點計算的InitTerrain函數的原型是這樣的:

BOOLInitTerrain(INT nRows, INT nCols, FLOAT fSpace, FLOAT fScale);  //地形初始化函數


其中前兩個參數分別爲地形的行數和列數,需要我們在初始化時指定。也就是說在計算地形的時候行數和列數是已知的,那麼,地形在x方向和z方向上的頂點數也就明瞭了,也就是z方向上頂點數爲地形的行數加1,而在x方向上的頂點數爲地形列數加上1.

需要注意的是地形在x方向和z方向上的頂點數都不能大於與高度圖對應的分辨率分量。因爲高度圖中的每個元素都描述地形型中某個頂點的高度值。如果高度圖中只描述了128x128分辨率的地形信息,我們在初始化時InitTerrain函數的前兩個參數都不能取超過128的值。以高度圖的角度來想一下,既然我只跟你準備了128x128的高度信息,那麼你就得在我規定的範圍之內取頂點數,如果你取多了,我可管不了你這麼多,等着內存溢出吧。

接着,第三個fSpace爲頂點間的間隔,第四個參數fScale爲縮放的係數。

關於頂點的計算思路,我們通過下面這幅圖,就可以寫出來:


1363567672_8136.png

對每行的單元格數目、每列的單元格數目、單元格間的間距、高度縮放係數、地形的寬度、地形的深度、每行的頂點數、每列的頂點數、頂點總數各個擊破,就寫出了下面這幾句代碼:

   m_nCellsPerRow  = nRows; //每行的單元格數目     m_nCellsPerCol  = nCols; //每列的單元格數目     m_fCellSpacing  = fSpace;    //單元格間的間距     m_fHeightScale  = fScale; //高度縮放係數     m_fTerrainWidth = nRows * fSpace;  //地形的寬度     m_fTerrainDepth = nCols * fSpace;  //地形的深度     m_nVertsPerRow  = m_nCellsPerCol + 1;  //每行的頂點數     m_nVertsPerCol  = m_nCellsPerRow + 1; //每列的頂點數     m_nNumVertices  = m_nVertsPerRow * m_nVertsPerCol;  //頂點總數



另外,我們在計算地形頂點前,還需要將地形的高度值乘以一個縮放係數,以便能夠調整高度的整體變化幅度,就是下面這兩句代碼:

// 通過一個for循環,逐個把地形原始高度乘以縮放係數,得到縮放後的高度     for(unsigned int i=0;i<m_vHeightInfo.size(); i++)         m_vHeightInfo[i] *= m_fHeightScale;


接着,就是頂點的正式計算時刻,我們按照着之前專門講解頂點緩存時用的四步曲,以及對着上面的這幅圖,下面的這些實現代碼就很好理解了:

    //---------------------------------------------------------------     // 處理地形的頂點     //---------------------------------------------------------------        //1,創建頂點緩存     if(FAILED(m_pd3dDevice->CreateVertexBuffer(m_nNumVertices * sizeof(TERRAINVERTEX),         D3DUSAGE_WRITEONLY, TERRAINVERTEX::FVF,D3DPOOL_MANAGED, &m_pVertexBuffer, 0)))         return FALSE;        //2,加鎖     TERRAINVERTEX *pVertices = NULL;     m_pVertexBuffer->Lock(0, 0,(void**)&pVertices, 0);        //3,訪問,賦值     FLOAT fStartX = -m_fTerrainWidth / 2.0f,fEndX =  m_fTerrainWidth / 2.0f;          //指定起始點和結束點的X座標值     FLOAT fStartZ =  m_fTerrainDepth / 2.0f, fEndZ =-m_fTerrainDepth / 2.0f;  //指定起始點和結束點的Z座標值     FLOAT fCoordU = 3.0f /(FLOAT)m_nCellsPerRow;     //指定紋理的橫座標值     FLOAT fCoordV = 3.0f /(FLOAT)m_nCellsPerCol;           //指定紋理的縱座標值       int nIndex = 0, i = 0, j = 0;     for (float z = fStartZ; z > fEndZ; z -=m_fCellSpacing, i++)          //Z座標方向上起始頂點到結束頂點行間的遍歷     {         j = 0;         for (float x = fStartX; x < fEndX; x+= m_fCellSpacing, j++)  //X座標方向上起始頂點到結束頂點行間的遍歷         {             nIndex = i * m_nCellsPerRow + j;         //指定當前頂點在頂點緩存中的位置             pVertices[nIndex] =TERRAINVERTEX(x, m_vHeightInfo[nIndex], z, j*fCoordU, i*fCoordV); //把頂點位置索引在高度圖中對應的各個頂點參數以及紋理座標賦值給賦給當前的頂點             nIndex++;                                                                          //索引數自加1         }     }        //4,解鎖     m_pVertexBuffer->Unlock();



已經逐行註釋了,理解起來應該是沒問題的吧。



五、地形索引的計算

頂點值算完了,當然還需要接着計算頂點的索引。頂點索引的計算關鍵是推導出一個用於求構成第i行,第j列的頂點處右下方兩個三角形的頂點索引的通用公式。下面我們來看看這個公式如何推導,下圖依舊解釋得很清楚了:

1363590858_8963.jpg

對頂點緩存中的任意一點A,如果該點位於地形中的第i行、第j列的話,那麼該點在頂點緩存中所對應的位置應該就是i*m+j(m爲每行的頂點數)。如果A點在索引緩存中的位置爲k的話,那麼A點爲起始點構成的三角形ABC中,B、C頂點在頂點緩存中的位置就爲(i+1)x m+j和i x m+(j+1)。且B點索引值爲k+1,C點索引值爲k+2.這樣。這樣,公式就可以推導爲如下:

三角形ABC=【i*每行頂點數+j,i*每行頂點數+(j+1),(i+1)*行頂點數+j】

三角形CBD=【(i+1)*每行頂點數+j,i*每行頂點數+(j+1),(i+1)*行頂點數+(j+1)】

通過上面我們推導出的這個公式,就可以寫出下面計算索引緩存的相關代碼:

//--------------------------------------------------------------- // 處理地形的索引 //--------------------------------------------------------------- 	//1.創建索引緩存     if (FAILED(m_pd3dDevice->CreateIndexBuffer(m_nNumVertices * 6 *sizeof(WORD),          D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_MANAGED, &m_pIndexBuffer, 0)))         return FALSE; 	//2.加鎖     WORD* pIndices = NULL;     m_pIndexBuffer->Lock(0, 0, (void**)&pIndices, 0); 	//3.訪問,賦值     nIndex = 0;     for(int row = 0; row < m_nCellsPerRow-1; row++)   //遍歷每行     {         for(int col = 0; col < m_nCellsPerCol-1; col++)  //遍歷每列         { 			//三角形ABC的三個頂點             pIndices[nIndex]   =  row * m_nCellsPerRow + col;			//頂點A             pIndices[nIndex+1] =  row * m_nCellsPerRow + col + 1;  //頂點B             pIndices[nIndex+2] = (row+1) * m_nCellsPerRow + col;	//頂點C 			//三角形CBD的三個頂點             pIndices[nIndex+3] = (row+1) * m_nCellsPerRow + col;		//頂點C             pIndices[nIndex+4] =  row * m_nCellsPerRow + col + 1;		//頂點B             pIndices[nIndex+5] = (row+1) * m_nCellsPerRow + col + 1;//頂點D 			//處理完一個單元格,索引加上6             nIndex += 6;  //索引自加6         }     } 	//4、解鎖     m_pIndexBuffer->Unlock();



六、渲染出地形


沒有渲染我們前面就算白忙活了。

地形的渲染我們封裝在了一個名爲RenderTerrain的函數中,註釋很詳細,我們直接上代碼:

//-------------------------------------------------------------------------------------- // Name:TerrainClass::RenderTerrain() // Desc: 繪製出地形,可以通過第二個參數選擇是否繪製出線框 //-------------------------------------------------------------------------------------- BOOLTerrainClass::RenderTerrain(D3DXMATRIX *pMatWorld, BOOL bRenderFrame) {     m_pd3dDevice->SetStreamSource(0,m_pVertexBuffer, 0, sizeof(TERRAINVERTEX));  ///把包含的幾何體信息的頂點緩存和渲染流水線相關聯      m_pd3dDevice->SetFVF(TERRAINVERTEX::FVF);//指定我們使用的靈活頂點格式的宏名稱    m_pd3dDevice->SetIndices(m_pIndexBuffer);//設置索引緩存      m_pd3dDevice->SetTexture(0,m_pTexture);//設置紋理      m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);      //關閉光照     m_pd3dDevice->SetTransform(D3DTS_WORLD,pMatWorld); //設置世界矩陣    m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0,         m_nNumVertices, 0, m_nNumVertices * 2);              //繪製頂點      m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, TRUE);  //打開光照     m_pd3dDevice->SetTexture(0, 0); //紋理置空       if (bRenderFrame)  //如果要渲染出線框的話     {        m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); //把填充模式設爲線框填充        m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0,             m_nNumVertices, 0, m_nNumVertices *2);       //繪製頂點         m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);    //把填充模式調回實體填充     }     return TRUE; }





七、完成地形類的設計

上面都是零零散散地說出了我們地形類的某些實現細節,下面我們把他們整合在一起,構成一個整體,完成地形類的設計,即貼出TerrainClass.cpp的全部代碼:

//============================================================================= // Name:TerrainClass.cpp //    Des: 一個封裝了三維地形系統的類的源文件 // 2013年 3月17日  Create by 淺墨 //=============================================================================   #include"TerrainClass.h"   //----------------------------------------------------------------------------- // Desc: 構造函數 //----------------------------------------------------------------------------- TerrainClass::TerrainClass(IDirect3DDevice9*pd3dDevice) {        //給各個成員變量賦初值     m_pd3dDevice = pd3dDevice;     m_pTexture = NULL;     m_pIndexBuffer = NULL;     m_pVertexBuffer = NULL;     m_nCellsPerRow = 0;     m_nCellsPerCol = 0;     m_nVertsPerRow = 0;     m_nVertsPerCol = 0;     m_nNumVertices = 0;     m_fTerrainWidth = 0.0f;     m_fTerrainDepth = 0.0f;     m_fCellSpacing = 0.0f;     m_fHeightScale = 0.0f; }     //-------------------------------------------------------------------------------------- // Name:TerrainClass::LoadTerrainFromFile() // Desc: 加載地形高度信息以及紋理 //-------------------------------------------------------------------------------------- BOOLTerrainClass::LoadTerrainFromFile(wchar_t *pRawFileName, wchar_t *pTextureFile) {     // 從文件中讀取高度信息     std::ifstream inFile;         inFile.open(pRawFileName,std::ios::binary);   //用二進制的方式打開文件       inFile.seekg(0,std::ios::end);                                              //把文件指針移動到文件末尾     std::vector<BYTE>inData(inFile.tellg());                 //用模板定義一個vector<BYTE>類型的變量inData並初始化,其值爲緩衝區當前位置,即緩衝區大小       inFile.seekg(std::ios::beg);                                                       //將文件指針移動到文件的開頭,準備讀取高度信息     inFile.read((char*)&inData[0],inData.size());    //關鍵的一步,讀取整個高度信息     inFile.close();                                                                                          //操作結束,可以關閉文件了       m_vHeightInfo.resize(inData.size());                                //將m_vHeightInfo尺寸取爲緩衝區的尺寸        //遍歷整個緩衝區,將inData中的值賦給m_vHeightInfo     for (unsigned int i=0; i<inData.size();i++)                      m_vHeightInfo[i] = inData[i];       // 加載地形紋理     if (FAILED(D3DXCreateTextureFromFile(m_pd3dDevice,pTextureFile, &m_pTexture)))         return FALSE;       return TRUE; }   //-------------------------------------------------------------------------------------- // Name:TerrainClass::InitTerrain() // Desc: 初始化地形的高度, 填充頂點和索引緩存 //-------------------------------------------------------------------------------------- BOOLTerrainClass::InitTerrain(INT nRows, INT nCols, FLOAT fSpace, FLOAT fScale) {     m_nCellsPerRow  = nRows; //每行的單元格數目     m_nCellsPerCol  = nCols; //每列的單元格數目     m_fCellSpacing  = fSpace;    //單元格間的間距     m_fHeightScale  = fScale; //高度縮放係數     m_fTerrainWidth = nRows * fSpace;  //地形的寬度     m_fTerrainDepth = nCols * fSpace;  //地形的深度     m_nVertsPerRow  = m_nCellsPerCol + 1;  //每行的頂點數     m_nVertsPerCol = m_nCellsPerRow + 1; //每列的頂點數     m_nNumVertices  = m_nVertsPerRow * m_nVertsPerCol;  //頂點總數       // 通過一個for循環,逐個把地形原始高度乘以縮放係數,得到縮放後的高度     for(unsigned int i=0;i<m_vHeightInfo.size(); i++)         m_vHeightInfo[i] *= m_fHeightScale;     //---------------------------------------------------------------     // 處理地形的頂點     //---------------------------------------------------------------        //1,創建頂點緩存     if(FAILED(m_pd3dDevice->CreateVertexBuffer(m_nNumVertices *sizeof(TERRAINVERTEX),         D3DUSAGE_WRITEONLY, TERRAINVERTEX::FVF,D3DPOOL_MANAGED, &m_pVertexBuffer, 0)))         return FALSE;        //2,加鎖     TERRAINVERTEX *pVertices = NULL;     m_pVertexBuffer->Lock(0, 0,(void**)&pVertices, 0);        //3,訪問,賦值     FLOAT fStartX = -m_fTerrainWidth / 2.0f,fEndX =  m_fTerrainWidth / 2.0f;          //指定起始點和結束點的X座標值     FLOAT fStartZ =  m_fTerrainDepth / 2.0f, fEndZ =-m_fTerrainDepth / 2.0f;  //指定起始點和結束點的Z座標值     FLOAT fCoordU = 3.0f /(FLOAT)m_nCellsPerRow;     //指定紋理的橫座標值     FLOAT fCoordV = 3.0f /(FLOAT)m_nCellsPerCol;           //指定紋理的縱座標值       int nIndex = 0, i = 0, j = 0;     for (float z = fStartZ; z > fEndZ; z -=m_fCellSpacing, i++)          //Z座標方向上起始頂點到結束頂點行間的遍歷     {         j = 0;         for (float x = fStartX; x < fEndX; x+= m_fCellSpacing, j++)  //X座標方向上起始頂點到結束頂點行間的遍歷         {             nIndex = i * m_nCellsPerRow + j;         //指定當前頂點在頂點緩存中的位置             pVertices[nIndex] =TERRAINVERTEX(x, m_vHeightInfo[nIndex], z, j*fCoordU, i*fCoordV); //把頂點位置索引在高度圖中對應的各個頂點參數以及紋理座標賦值給賦給當前的頂點             nIndex++;                                                                          //索引數自加1         }     }        //4,解鎖     m_pVertexBuffer->Unlock();       //---------------------------------------------------------------     // 處理地形的索引     //---------------------------------------------------------------        //1.創建索引緩存     if(FAILED(m_pd3dDevice->CreateIndexBuffer(m_nNumVertices * 6 *sizeof(WORD),         D3DUSAGE_WRITEONLY, D3DFMT_INDEX16,D3DPOOL_MANAGED, &m_pIndexBuffer, 0)))         return FALSE;        //2.加鎖     WORD* pIndices = NULL;     m_pIndexBuffer->Lock(0, 0, (void**)&pIndices,0);        //3.訪問,賦值     nIndex = 0;     for(int row = 0; row < m_nCellsPerRow-1;row++)   //遍歷每行     {         for(int col = 0; col <m_nCellsPerCol-1; col++)  //遍歷每列         {                      //三角形ABC的三個頂點             pIndices[nIndex]   =  row* m_nCellsPerRow + col;                  //頂點A             pIndices[nIndex+1] =  row * m_nCellsPerRow + col + 1;  //頂點B             pIndices[nIndex+2] = (row+1) *m_nCellsPerRow + col;      //頂點C                      //三角形CBD的三個頂點             pIndices[nIndex+3] = (row+1) *m_nCellsPerRow + col;             //頂點C             pIndices[nIndex+4] =  row * m_nCellsPerRow + col + 1;           //頂點B             pIndices[nIndex+5] = (row+1) *m_nCellsPerRow + col + 1;//頂點D                      //處理完一個單元格,索引加上6             nIndex += 6;  //索引自加6         }     }        //4、解鎖     m_pIndexBuffer->Unlock();       return TRUE; }   //-------------------------------------------------------------------------------------- // Name:TerrainClass::RenderTerrain() // Desc: 繪製出地形,可以通過第二個參數選擇是否繪製出線框 //-------------------------------------------------------------------------------------- BOOLTerrainClass::RenderTerrain(D3DXMATRIX *pMatWorld, BOOL bRenderFrame) {     m_pd3dDevice->SetStreamSource(0,m_pVertexBuffer, 0, sizeof(TERRAINVERTEX));  ///把包含的幾何體信息的頂點緩存和渲染流水線相關聯     m_pd3dDevice->SetFVF(TERRAINVERTEX::FVF);//指定我們使用的靈活頂點格式的宏名稱    m_pd3dDevice->SetIndices(m_pIndexBuffer);//設置索引緩存      m_pd3dDevice->SetTexture(0,m_pTexture);//設置紋理      m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);      //關閉光照     m_pd3dDevice->SetTransform(D3DTS_WORLD,pMatWorld); //設置世界矩陣    m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0,         m_nNumVertices, 0, m_nNumVertices * 2);              //繪製頂點      m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, TRUE);  //打開光照     m_pd3dDevice->SetTexture(0, 0); //紋理置空       if (bRenderFrame)  //如果要渲染出線框的話     {        m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); //把填充模式設爲線框填充        m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0,             m_nNumVertices, 0, m_nNumVertices *2);       //繪製頂點         m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);    //把填充模式調回實體填充     }     return TRUE; }   //----------------------------------------------------------------------------- // Desc: 析構函數 //----------------------------------------------------------------------------- TerrainClass::~TerrainClass(void) {        SAFE_RELEASE(m_pTexture);        SAFE_RELEASE(m_pIndexBuffer);        SAFE_RELEASE(m_pVertexBuffer); }



一個地形類就這樣被我們一步一步設計出來了。下面我們來看一下,這個類到底如何用。

八、詳細註釋的源代碼欣賞


本次的配套程序有點科幻的味道,我們地形渲染得像水晶寶石山,然後載入了一個變形金剛中大黃蜂的模型,非常帥氣。


源代碼包含了8個文件,主要用於公共輔助宏定義的D3DUtil.h,用於封裝了DirectInput輸入控制API的DirectInputClass.h和DirectInputClass.cpp,以及封裝了虛擬攝像機類的CameraClass.h和CameraClass.cpp,封裝了地形系統的TerrainClass.h和TerrainClass.cpp,當還還有核心代碼main.cpp。

DirectInputClass.h和DirectInputClass.cpp較之前的文章中依然沒有任何修改,依然不再貼出,TerrainClass.cpp和TerrainClass.h在上面講解的過程中以及貼出來了,這裏也不貼出,我們依然只貼核心代碼main.cpp,大家要看得爽的話,源代碼在文章末尾有下載鏈接,下回去用VisualStuido看就行了。下面就是main.cpp的全部代碼:

//***************************************************************************************** // //【Visual C++】遊戲開發筆記系列配套源碼四十八  淺墨DirectX教程十六  三維地形系統的實現 //		 VS2010版 // 2013年 3月17日  Create by 淺墨  //*****************************************************************************************    //***************************************************************************************** // Desc: 宏定義部分    //***************************************************************************************** #define SCREEN_WIDTH	800						//爲窗口寬度定義的宏,以方便在此處修改窗口寬度 #define SCREEN_HEIGHT	600							//爲窗口高度定義的宏,以方便在此處修改窗口高度 #define WINDOW_TITLE	_T("【Visual C++】遊戲開發筆記系列配套示例程序四十八  淺墨DirectX教程十六  三維地形系統的實現") //爲窗口標題定義的宏    //***************************************************************************************** // Desc: 頭文件定義部分   //*****************************************************************************************                                                                                        #include <d3d9.h> #include <d3dx9.h> #include <tchar.h> #include <time.h>  #include "DirectInputClass.h" #include "CameraClass.h" #include "TerrainClass.h"  //***************************************************************************************** // Desc: 庫文件定義部分   //*****************************************************************************************  #pragma comment(lib,"d3d9.lib") #pragma comment(lib,"d3dx9.lib") #pragma comment(lib, "dinput8.lib")     // 使用DirectInput必須包含的庫文件,注意這裏有8 #pragma comment(lib,"dxguid.lib") #pragma comment(lib, "winmm.lib")     //***************************************************************************************** // Desc: 全局變量聲明部分   //***************************************************************************************** LPDIRECT3DDEVICE9					g_pd3dDevice = NULL;				//Direct3D設備對象 LPD3DXFONT								g_pTextFPS =NULL;    //字體COM接口 LPD3DXFONT								g_pTextAdaperName = NULL;  // 顯卡信息的2D文本 LPD3DXFONT								g_pTextHelper = NULL;  // 幫助信息的2D文本 LPD3DXFONT								g_pTextInfor= NULL;  // 繪製信息的2D文本 float											g_FPS= 0.0f;       //一個浮點型的變量,代表幀速率 wchar_t										g_strFPS[50] ={0};    //包含幀速率的字符數組 wchar_t										g_strAdapterName[60] ={0};   //包含顯卡名稱的字符數組 D3DXMATRIX							g_matWorld;							//世界矩陣 LPD3DXMESH							g_pMesh = NULL;				// 網格對象 D3DMATERIAL9*						g_pMaterials= NULL;			// 網格的材質信息 LPDIRECT3DTEXTURE9*				g_pTextures = NULL;			// 網格的紋理信息 DWORD									g_dwNumMtrls = 0;			// 材質的數目 LPD3DXMESH							g_cylinder = NULL;				//柱子網格對象 D3DMATERIAL9							g_MaterialCylinder;				//柱子的材質 DInputClass*								g_pDInput = NULL;				//DInputClass類的指針實例 CameraClass*							g_pCamera = NULL;				//攝像機類的指針實例 TerrainClass*								g_pTerrain = NULL;		//地形類的指針實例     //***************************************************************************************** // Desc: 全局函數聲明部分  //*****************************************************************************************  LRESULT CALLBACK		WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ); HRESULT						Direct3D_Init(HWND hwnd,HINSTANCE hInstance); HRESULT						Objects_Init(); void								Direct3D_Render( HWND hwnd); void								Direct3D_Update( HWND hwnd); void								Direct3D_CleanUp( ); float								Get_FPS(); void								HelpText_Render(HWND hwnd);  //***************************************************************************************** // Name: WinMain( ) // Desc: Windows應用程序入口函數 //***************************************************************************************** int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd) {  	//開始設計一個完整的窗口類 	WNDCLASSEX wndClass = { 0 };				//用WINDCLASSEX定義了一個窗口類,即用wndClass實例化了WINDCLASSEX,用於之後窗口的各項初始化     	wndClass.cbSize = sizeof( WNDCLASSEX ) ;	//設置結構體的字節數大小 	wndClass.style = CS_HREDRAW | CS_VREDRAW;	//設置窗口的樣式 	wndClass.lpfnWndProc = WndProc;				//設置指向窗口過程函數的指針 	wndClass.cbClsExtra		= 0; 	wndClass.cbWndExtra		= 0; 	wndClass.hInstance = hInstance;				//指定包含窗口過程的程序的實例句柄。 	wndClass.hIcon=(HICON)::LoadImage(NULL,_T("icon.ico"),IMAGE_ICON,0,0,LR_DEFAULTSIZE|LR_LOADFROMFILE); //從全局的::LoadImage函數從本地加載自定義ico圖標 	wndClass.hCursor = LoadCursor( NULL, IDC_ARROW );    //指定窗口類的光標句柄。 	wndClass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH);  //爲hbrBackground成員指定一個灰色畫刷句柄 	wndClass.lpszMenuName = NULL;						//用一個以空終止的字符串,指定菜單資源的名字。 	wndClass.lpszClassName = _T("ForTheDreamOfGameDevelop");		//用一個以空終止的字符串,指定窗口類的名字。  	if( !RegisterClassEx( &wndClass ) )				//設計完窗口後,需要對窗口類進行註冊,這樣才能創建該類型的窗口 		return -1;		  	HWND hwnd = CreateWindow( _T("ForTheDreamOfGameDevelop"),WINDOW_TITLE,			//喜聞樂見的創建窗口函數CreateWindow 		WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, SCREEN_WIDTH, 		SCREEN_HEIGHT, NULL, NULL, hInstance, NULL );   	//Direct3D資源的初始化,調用失敗用messagebox予以顯示 	if (!(S_OK==Direct3D_Init (hwnd,hInstance))) 	{ 		MessageBox(hwnd, _T("Direct3D初始化失敗~!"), _T("淺墨的消息窗口"), 0); //使用MessageBox函數,創建一個消息窗口  	} 	PlaySound(L"雅尼 - 蘭花.wav", NULL, SND_FILENAME | SND_ASYNC|SND_LOOP);   //循環播放背景音樂  	  	MoveWindow(hwnd,200,50,SCREEN_WIDTH,SCREEN_HEIGHT,true);   //調整窗口顯示時的位置,窗口左上角位於屏幕座標(200,50)處 	ShowWindow( hwnd, nShowCmd );    //調用Win32函數ShowWindow來顯示窗口 	UpdateWindow(hwnd);  //對窗口進行更新,就像我們買了新房子要裝修一樣  	//進行DirectInput類的初始化 	g_pDInput = new DInputClass(); 	g_pDInput->Init(hwnd,hInstance,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);  	//消息循環過程 	MSG msg = { 0 };  //初始化msg 	while( msg.message != WM_QUIT )			//使用while循環 	{ 		if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) )   //查看應用程序消息隊列,有消息時將隊列中的消息派發出去。 		{ 			TranslateMessage( &msg );		//將虛擬鍵消息轉換爲字符消息 			DispatchMessage( &msg );		//該函數分發一個消息給窗口程序。 		} 		else 		{ 			Direct3D_Update(hwnd);         //調用更新函數,進行畫面的更新 			Direct3D_Render(hwnd);			//調用渲染函數,進行畫面的渲染			 		} 	}  	UnregisterClass(_T("ForTheDreamOfGameDevelop"), wndClass.hInstance); 	return 0;   }    //***************************************************************************************** // Name: WndProc() // Desc: 對窗口消息進行處理 //***************************************************************************************** LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )   //窗口過程函數WndProc { 	switch( message )				//switch語句開始 	{ 	case WM_PAINT:					 // 客戶區重繪消息 		Direct3D_Render(hwnd);          //調用Direct3D_Render函數,進行畫面的繪製 		ValidateRect(hwnd, NULL);   // 更新客戶區的顯示 		break;									//跳出該switch語句  	case WM_KEYDOWN:                // 鍵盤按下消息 		if (wParam == VK_ESCAPE)    // ESC鍵 			DestroyWindow(hwnd);    // 銷燬窗口, 併發送一條WM_DESTROY消息 		break; 	case WM_DESTROY:				//窗口銷燬消息 		Direct3D_CleanUp();     //調用Direct3D_CleanUp函數,清理COM接口對象 		PostQuitMessage( 0 );		//向系統表明有個線程有終止請求。用來響應WM_DESTROY消息 		break;						//跳出該switch語句  	default:						//若上述case條件都不符合,則執行該default語句 		return DefWindowProc( hwnd, message, wParam, lParam );		//調用缺省的窗口過程來爲應用程序沒有處理的窗口消息提供缺省的處理。 	}  	return 0;					//正常退出 }   //***************************************************************************************** // Name: Direct3D_Init( ) // Desc: 初始化Direct3D // Point:【Direct3D初始化四步曲】 //		1.初始化四步曲之一,創建Direct3D接口對象 //		2.初始化四步曲之二,獲取硬件設備信息 //		3.初始化四步曲之三,填充結構體 //		4.初始化四步曲之四,創建Direct3D設備接口 //*****************************************************************************************  HRESULT Direct3D_Init(HWND hwnd,HINSTANCE hInstance) {  	//-------------------------------------------------------------------------------------- 	// 【Direct3D初始化四步曲之一,創接口】:創建Direct3D接口對象, 以便用該Direct3D對象創建Direct3D設備對象 	//-------------------------------------------------------------------------------------- 	LPDIRECT3D9  pD3D = NULL; //Direct3D接口對象的創建 	if( NULL == ( pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) ) //初始化Direct3D接口對象,並進行DirectX版本協商  			return E_FAIL;  	//-------------------------------------------------------------------------------------- 	// 【Direct3D初始化四步曲之二,取信息】:獲取硬件設備信息 	//-------------------------------------------------------------------------------------- 	D3DCAPS9 caps; int vp = 0; 	if( FAILED( pD3D->GetDeviceCaps( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps ) ) ) 		{ 			return E_FAIL; 		} 	if( caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT ) 		vp = D3DCREATE_HARDWARE_VERTEXPROCESSING;   //支持硬件頂點運算,我們就採用硬件頂點運算,妥妥的 	else 		vp = D3DCREATE_SOFTWARE_VERTEXPROCESSING; //不支持硬件頂點運算,無奈只好採用軟件頂點運算  	//-------------------------------------------------------------------------------------- 	// 【Direct3D初始化四步曲之三,填內容】:填充D3DPRESENT_PARAMETERS結構體 	//-------------------------------------------------------------------------------------- 	D3DPRESENT_PARAMETERS d3dpp;  	ZeroMemory(&d3dpp, sizeof(d3dpp)); 	d3dpp.BackBufferWidth            = SCREEN_WIDTH; 	d3dpp.BackBufferHeight           = SCREEN_HEIGHT; 	d3dpp.BackBufferFormat           = D3DFMT_A8R8G8B8; 	d3dpp.BackBufferCount            = 2; 	d3dpp.MultiSampleType            = D3DMULTISAMPLE_NONE; 	d3dpp.MultiSampleQuality         = 0; 	d3dpp.SwapEffect                 = D3DSWAPEFFECT_DISCARD;  	d3dpp.hDeviceWindow              = hwnd; 	d3dpp.Windowed                   = true; 	d3dpp.EnableAutoDepthStencil     = true;  	d3dpp.AutoDepthStencilFormat     = D3DFMT_D24S8; 	d3dpp.Flags                      = 0; 	d3dpp.FullScreen_RefreshRateInHz = 0; 	d3dpp.PresentationInterval       = D3DPRESENT_INTERVAL_IMMEDIATE;  	//-------------------------------------------------------------------------------------- 	// 【Direct3D初始化四步曲之四,創設備】:創建Direct3D設備接口 	//-------------------------------------------------------------------------------------- 	if(FAILED(pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,  		hwnd, vp, &d3dpp, &g_pd3dDevice))) 		return E_FAIL;   	//獲取顯卡信息到g_strAdapterName中,並在顯卡名稱之前加上“當前顯卡型號:”字符串 	 wchar_t TempName[60]=L"當前顯卡型號:";   //定義一個臨時字符串,且方便了把"當前顯卡型號:"字符串引入我們的目的字符串中 	 D3DADAPTER_IDENTIFIER9 Adapter;  //定義一個D3DADAPTER_IDENTIFIER9結構體,用於存儲顯卡信息 	 pD3D->GetAdapterIdentifier(0,0,&Adapter);//調用GetAdapterIdentifier,獲取顯卡信息 	 int len = MultiByteToWideChar(CP_ACP,0, Adapter.Description, -1, NULL, 0);//顯卡名稱現在已經在Adapter.Description中了,但是其爲char類型,我們要將其轉爲wchar_t類型 	 MultiByteToWideChar(CP_ACP, 0, Adapter.Description, -1, g_strAdapterName, len);//這步操作完成後,g_strAdapterName中就爲當前我們的顯卡類型名的wchar_t型字符串了 	 wcscat_s(TempName,g_strAdapterName);//把當前我們的顯卡名加到“當前顯卡型號:”字符串後面,結果存在TempName中 	 wcscpy_s(g_strAdapterName,TempName);//把TempName中的結果拷貝到全局變量g_strAdapterName中,大功告成~  	if(!(S_OK==Objects_Init())) return E_FAIL;  	SAFE_RELEASE(pD3D) //LPDIRECT3D9接口對象的使命完成,我們將其釋放掉  	return S_OK; }   HRESULT Objects_Init() { 	//創建字體 	D3DXCreateFont(g_pd3dDevice, 36, 0, 0, 1000, false, DEFAULT_CHARSET,  		OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, _T("Calibri"), &g_pTextFPS); 	D3DXCreateFont(g_pd3dDevice, 20, 0, 1000, 0, false, DEFAULT_CHARSET,  		OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"華文中宋", &g_pTextAdaperName);  	D3DXCreateFont(g_pd3dDevice, 23, 0, 1000, 0, false, DEFAULT_CHARSET,  		OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"微軟雅黑", &g_pTextHelper);  	D3DXCreateFont(g_pd3dDevice, 26, 0, 1000, 0, false, DEFAULT_CHARSET,  		OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"黑體", &g_pTextInfor);     	// 從X文件中加載網格數據 	LPD3DXBUFFER pAdjBuffer  = NULL; 	LPD3DXBUFFER pMtrlBuffer = NULL;  	D3DXLoadMeshFromX(L"bee.X", D3DXMESH_MANAGED, g_pd3dDevice,  		&pAdjBuffer, &pMtrlBuffer, NULL, &g_dwNumMtrls, &g_pMesh); 	// 讀取材質和紋理數據 	D3DXMATERIAL *pMtrls = (D3DXMATERIAL*)pMtrlBuffer->GetBufferPointer(); //創建一個D3DXMATERIAL結構體用於讀取材質和紋理信息 	g_pMaterials = new D3DMATERIAL9[g_dwNumMtrls]; 	g_pTextures  = new LPDIRECT3DTEXTURE9[g_dwNumMtrls]; 	for (DWORD i=0; i<g_dwNumMtrls; i++)  	{ 		//獲取材質,並設置一下環境光的顏色值 		g_pMaterials[i] = pMtrls[i].MatD3D; 		g_pMaterials[i].Ambient = g_pMaterials[i].Diffuse;  		//創建一下紋理對象 		g_pTextures[i]  = NULL; 		D3DXCreateTextureFromFileA(g_pd3dDevice, pMtrls[i].pTextureFilename, &g_pTextures[i]); 	} 	SAFE_RELEASE(pAdjBuffer) 	SAFE_RELEASE(pMtrlBuffer)    	//創建柱子 	D3DXCreateCylinder(g_pd3dDevice, 8000.0f, 100.0f, 50000.0f, 60, 60,  &g_cylinder, 0); 	g_MaterialCylinder.Ambient  = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f);   	g_MaterialCylinder.Diffuse  = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f);   	g_MaterialCylinder.Specular = D3DXCOLOR(0.5f, 0.0f, 0.3f, 0.3f);   	g_MaterialCylinder.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.0f, 1.0f);  	// 設置光照   	D3DLIGHT9 light;   	::ZeroMemory(&light, sizeof(light));   	light.Type          = D3DLIGHT_DIRECTIONAL;   	light.Ambient       = D3DXCOLOR(0.7f, 0.7f, 0.7f, 1.0f);   	light.Diffuse       = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);   	light.Specular      = D3DXCOLOR(0.9f, 0.9f, 0.9f, 1.0f);   	light.Direction     = D3DXVECTOR3(1.0f, 1.0f, 1.0f);   	g_pd3dDevice->SetLight(0, &light);   	g_pd3dDevice->LightEnable(0, true);   	g_pd3dDevice->SetRenderState(D3DRS_NORMALIZENORMALS, true);   	g_pd3dDevice->SetRenderState(D3DRS_SPECULARENABLE, true);  	// 創建並初始化虛擬攝像機 	g_pCamera = new CameraClass(g_pd3dDevice); 	g_pCamera->SetCameraPosition(&D3DXVECTOR3(0.0f, 12000.0f, -30000.0f));  //設置攝像機所在的位置 	g_pCamera->SetTargetPosition(&D3DXVECTOR3(0.0f, 6000.0f, 0.0f));  //設置目標觀察點所在的位置 	g_pCamera->SetViewMatrix();  //設置取景變換矩陣 	g_pCamera->SetProjMatrix();  //設置投影變換矩陣  	// 創建並初始化地形 	g_pTerrain = new TerrainClass(g_pd3dDevice);    	g_pTerrain->LoadTerrainFromFile(L"heighmap.raw", L"green.jpg");		//從文件加載高度圖和紋理 	g_pTerrain->InitTerrain(200, 200, 500.0f, 60.0f);  //四個值分別是頂點行數,頂點列數,頂點間間距,縮放係數  	return S_OK; }  void				Direct3D_Update( HWND hwnd) { 	//使用DirectInput類讀取數據 	g_pDInput->GetInput();  	// 沿攝像機各分量移動視角 	if (g_pDInput->IsKeyDown(DIK_A))  g_pCamera->MoveAlongRightVec(-10.0f); 	if (g_pDInput->IsKeyDown(DIK_D))  g_pCamera->MoveAlongRightVec( 10.0f); 	if (g_pDInput->IsKeyDown(DIK_W)) g_pCamera->MoveAlongLookVec( 10.0f); 	if (g_pDInput->IsKeyDown(DIK_S))  g_pCamera->MoveAlongLookVec(-10.0f); 	if (g_pDInput->IsKeyDown(DIK_I))  g_pCamera->MoveAlongUpVec( 10.0f); 	if (g_pDInput->IsKeyDown(DIK_K))  g_pCamera->MoveAlongUpVec(-10.0f);  	//沿攝像機各分量旋轉視角 	if (g_pDInput->IsKeyDown(DIK_LEFT))  g_pCamera->RotationUpVec(-0.003f); 	if (g_pDInput->IsKeyDown(DIK_RIGHT))  g_pCamera->RotationUpVec( 0.003f); 	if (g_pDInput->IsKeyDown(DIK_UP))  g_pCamera->RotationRightVec(-0.003f); 	if (g_pDInput->IsKeyDown(DIK_DOWN))  g_pCamera->RotationRightVec( 0.003f); 	if (g_pDInput->IsKeyDown(DIK_J)) g_pCamera->RotationLookVec(-0.001f); 	if (g_pDInput->IsKeyDown(DIK_L)) g_pCamera->RotationLookVec( 0.001f);  	//鼠標控制右向量和上向量的旋轉 	g_pCamera->RotationUpVec(g_pDInput->MouseDX()* 0.001f); 	g_pCamera->RotationRightVec(g_pDInput->MouseDY() * 0.001f);  	//鼠標滾輪控制觀察點收縮操作 	static FLOAT fPosZ=0.0f; 	fPosZ += g_pDInput->MouseDZ()*0.03f;  	//計算並設置取景變換矩陣 	D3DXMATRIX matView; 	g_pCamera->CalculateViewMatrix(&matView); 	g_pd3dDevice->SetTransform(D3DTS_VIEW, &matView);  	//把正確的世界變換矩陣存到g_matWorld中 	D3DXMatrixTranslation(&g_matWorld, 0.0f, 0.0f, fPosZ);  	//以下這段代碼用於限制鼠標光標移動區域 	POINT lt,rb; 	RECT rect; 	GetClientRect(hwnd,&rect);  //取得窗口內部矩形 	//將矩形左上點座標存入lt中 	lt.x = rect.left; 	lt.y = rect.top; 	//將矩形右下座標存入rb中 	rb.x = rect.right; 	rb.y = rect.bottom; 	//將lt和rb的窗口座標轉換爲屏幕座標 	ClientToScreen(hwnd,<); 	ClientToScreen(hwnd,&rb); 	//以屏幕座標重新設定矩形區域 	rect.left = lt.x; 	rect.top = lt.y; 	rect.right = rb.x; 	rect.bottom = rb.y; 	//限制鼠標光標移動區域 	ClipCursor(&rect);  }    //***************************************************************************************** // Name: Direct3D_Render() // Desc: 進行圖形的渲染操作 // Point:【Direct3D渲染五步曲】 //		1.渲染五步曲之一,清屏操作 //		2.渲染五步曲之二,開始繪製 //		3.渲染五步曲之三,正式繪製 //		4.渲染五步曲之四,結束繪製 //		5.渲染五步曲之五,翻轉顯示 //*****************************************************************************************  void Direct3D_Render(HWND hwnd) { 	//-------------------------------------------------------------------------------------- 	// 【Direct3D渲染五步曲之一】:清屏操作 	//-------------------------------------------------------------------------------------- 	g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER|D3DCLEAR_STENCIL, D3DCOLOR_XRGB(0, 108, 255), 1.0f, 0);  	//-------------------------------------------------------------------------------------- 	// 【Direct3D渲染五步曲之二】:開始繪製 	//-------------------------------------------------------------------------------------- 	g_pd3dDevice->BeginScene();                     // 開始繪製 	 	//-------------------------------------------------------------------------------------- 	// 【Direct3D渲染五步曲之三】:正式繪製 	//--------------------------------------------------------------------------------------  	//繪製大黃蜂 	D3DXMATRIX mScal,mRot1,mRot2,mTrans,mFinal;   //定義一些矩陣,準備對大黃蜂進行矩陣變換 	D3DXMatrixScaling(&mScal,20.0f,20.0f,20.0f);   	D3DXMatrixTranslation(&mTrans,0,8000,0); 	D3DXMatrixRotationX(&mRot1, D3DX_PI/2);  	D3DXMatrixRotationY(&mRot2, D3DX_PI/2);  	mFinal=mScal*mRot1*mRot2*mTrans*g_matWorld; 	g_pd3dDevice->SetTransform(D3DTS_WORLD, &mFinal);//設置模型的世界矩陣,爲繪製做準備 	// 用一個for循環,進行模型的網格各個部分的繪製 	for (DWORD i = 0; i < g_dwNumMtrls; i++) 	{ 		g_pd3dDevice->SetMaterial(&g_pMaterials[i]);  //設置此部分的材質 		g_pd3dDevice->SetTexture(0, g_pTextures[i]);//設置此部分的紋理 		g_pMesh->DrawSubset(i);  //繪製此部分 	}  	//繪製地形 	g_pTerrain->RenderTerrain(&g_matWorld, false);  //渲染地形,且第二個參數設爲false,表示不渲染出地形的線框 	 	//繪製柱子 	D3DXMATRIX TransMatrix, RotMatrix, FinalMatrix; 	D3DXMatrixRotationX(&RotMatrix, -D3DX_PI * 0.5f); 	g_pd3dDevice->SetMaterial(&g_MaterialCylinder); 	for(int i = 0; i < 4; i++) 	{ 		D3DXMatrixTranslation(&TransMatrix, -10000.0f, 0.0f, -15000.0f + (i * 20000.0f)); 		FinalMatrix = RotMatrix * TransMatrix ; 		g_pd3dDevice->SetTransform(D3DTS_WORLD, &FinalMatrix); 		g_cylinder->DrawSubset(0);  		D3DXMatrixTranslation(&TransMatrix, 10000.0f, 0.0f, -15000.0f + (i * 20000.0f)); 		FinalMatrix = RotMatrix * TransMatrix ; 		g_pd3dDevice->SetTransform(D3DTS_WORLD, &FinalMatrix); 		g_cylinder->DrawSubset(0); 	}  	//繪製文字信息 	HelpText_Render(hwnd);   	//-------------------------------------------------------------------------------------- 	// 【Direct3D渲染五步曲之四】:結束繪製 	//-------------------------------------------------------------------------------------- 	g_pd3dDevice->EndScene();                       // 結束繪製 	//-------------------------------------------------------------------------------------- 	// 【Direct3D渲染五步曲之五】:顯示翻轉 	//-------------------------------------------------------------------------------------- 	g_pd3dDevice->Present(NULL, NULL, NULL, NULL);  // 翻轉與顯示 	  }   void HelpText_Render(HWND hwnd) { 	//定義一個矩形,用於獲取主窗口矩形 	RECT formatRect; 	GetClientRect(hwnd, &formatRect);  	//在窗口右上角處,顯示每秒幀數 	formatRect.top = 5; 	int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() ); 	g_pTextFPS->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_RGBA(0,239,136,255));  	//顯示顯卡類型名 	g_pTextAdaperName->DrawText(NULL,g_strAdapterName, -1, &formatRect,  		DT_TOP | DT_LEFT, D3DXCOLOR(1.0f, 0.5f, 0.0f, 1.0f));  	// 輸出幫助信息 	formatRect.left = 0,formatRect.top = 380; 	g_pTextInfor->DrawText(NULL, L"控制說明:", -1, &formatRect,  		DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(235,123,230,255)); 	formatRect.top += 35; 	g_pTextHelper->DrawText(NULL, L"    W:向前飛翔     S:向後飛翔 ", -1, &formatRect,  		DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); 	formatRect.top += 25; 	g_pTextHelper->DrawText(NULL, L"    A:向左飛翔     D:向右飛翔", -1, &formatRect,  		DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); 	formatRect.top += 25; 	g_pTextHelper->DrawText(NULL, L"    I:垂直向上飛翔     K:垂直向下飛翔", -1, &formatRect,  		DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); 	formatRect.top += 25; 	g_pTextHelper->DrawText(NULL, L"    J:向左傾斜       L:向右傾斜", -1, &formatRect,  		DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); 	formatRect.top += 25; 	g_pTextHelper->DrawText(NULL, L"    上、下、左、右方向鍵、鼠標移動:視角變化 ", -1, &formatRect,  		DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); 	formatRect.top += 25; 	g_pTextHelper->DrawText(NULL, L"     鼠標滾輪:人物模型Y軸方向移動", -1, &formatRect,  		DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); 	formatRect.top += 25; 	g_pTextHelper->DrawText(NULL, L"    ESC鍵 : 退出程序", -1, &formatRect,  		DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); }  //***************************************************************************************** // Name:Get_FPS()函數 // Desc: 用於計算幀速率 //***************************************************************************************** float Get_FPS() {  	//定義四個靜態變量 	static float  fps = 0; //我們需要計算的FPS值 	static int    frameCount = 0;//幀數 	static float  currentTime =0.0f;//當前時間 	static float  lastTime = 0.0f;//持續時間  	frameCount++;//每調用一次Get_FPS()函數,幀數自增1 	currentTime = timeGetTime()*0.001f;//獲取系統時間,其中timeGetTime函數返回的是以毫秒爲單位的系統時間,所以需要乘以0.001,得到單位爲秒的時間  	//如果當前時間減去持續時間大於了1秒鐘,就進行一次FPS的計算和持續時間的更新,並將幀數值清零 	if(currentTime - lastTime > 1.0f) //將時間控制在1秒鐘 	{ 		fps = (float)frameCount /(currentTime - lastTime);//計算這1秒鐘的FPS值 		lastTime = currentTime; //將當前時間currentTime賦給持續時間lastTime,作爲下一秒的基準時間 		frameCount    = 0;//將本次幀數frameCount值清零 	}  	return fps; }    //***************************************************************************************** // Name: Direct3D_CleanUp() // Desc: 對Direct3D的資源進行清理,釋放COM接口對象 //***************************************************************************************** void Direct3D_CleanUp() {  	//釋放COM接口對象 	for (DWORD i = 0; i<g_dwNumMtrls; i++)  		SAFE_RELEASE(g_pTextures[i]); 	SAFE_DELETE(g_pTextures);  	SAFE_DELETE(g_pMaterials);  	SAFE_DELETE(g_pDInput); 	SAFE_RELEASE(g_cylinder); 	SAFE_RELEASE(g_pMesh); 	SAFE_RELEASE(g_pd3dDevice); 	SAFE_RELEASE(g_pTextAdaperName) 	SAFE_RELEASE(g_pTextHelper) 	SAFE_RELEASE(g_pTextInfor) 	SAFE_RELEASE(g_pTextFPS) 	SAFE_RELEASE(g_pd3dDevice) }



然後是幾張程序截圖:

1363567707_5711.png

1363567745_2173.png

1363567776_8734.png


1363566478_4817.png



如果是嫌這個地形還不過癮,不夠大或者不夠陡峭,我們可以修改頂點間的間距以及縮放係數,來得到陡峭而一望無際的山峯。下圖對應的是在初始化地形時將頂點間的間距以及縮放係數調大一些的情況:

g_pTerrain->InitTerrain(200, 200, 2000.0f, 600.0f);  //四個值分別是頂點行數,頂點列數,頂點間間距,縮放係數



按我們的飛行速度,達到這山峯的地圖邊界得飛幾分鐘。。。。不過這時候大黃蜂不見了,在地形整體下方了,此時攝像機初始位置也最好調一下,不然一出來也是在地形整體的下方。

1363595075_8916.png


1363595102_4740.png



文章最後,依舊是放出本篇文章配套源代碼的下載:

本節筆記配套源代碼請點擊這裏下載:


【淺墨DirectX提高班】配套源代碼之十六下載



以上就是本節筆記的全部內容,更多精彩內容,且聽下回分解。

淺墨在這裏,希望喜歡遊戲開發系列文章的朋友們能留下你們的評論,每次淺墨登陸博客看到大家的留言的時候都會非常開心,感覺自己正在傳遞一種信仰,一種精神。


文章最後,依然是【每文一語】欄目,今天的句子是:


這世上有兩樣東西是別人搶不走的:一是藏在心中的夢想,二是讀進大腦的書。


1363591646_4592.jpg

下週一,讓我們離遊戲開發的夢想更近一步。

下週一,遊戲開發筆記,我們,不見不散。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章