您好,登錄后才能下訂單哦!
本系列文章由zhmxy555(毛星云)編寫,轉載請注明出處。
作者:毛星云(淺墨) 郵箱: happylifemxy@163.com
上個星期淺墨寫的介紹三維攝像機的文章和示例程序放出以后,大家似乎都表現出了很高漲的熱情,不少朋友評論或者給淺墨發郵件問什么時候講地形和天空頂。本來淺墨是準備這個星期就開始講可編程渲染流水線的,看大家這么強烈的要求,淺墨決定干脆把準備在后面講的地形天空一氣呵成,跟在攝像機后面一起講了得了。所以,這篇文章就誕生了。
先上一張配套示例程序的截圖:
修改頂點間距和縮放比例可以得到更廣闊,更陡峭的山峰:
想創造出極具真實感的三維游戲世界,三維地形的模擬是必不可少,至關重要的。
三維地形模擬其實是一個很廣闊的課題,它其實不僅僅局限于我們的游戲開發領域,在三維仿真,虛擬現實等領域都涉及。說起三維地形模擬,似乎有那么一絲神秘,其實,只要了解其實現原理了這所謂的地形系統模擬也就是紙老虎一只。這篇文章里我們就來揭開三維地形模擬的面紗,看看到底怎樣利用一個C++類的書寫,實現我們專屬的三維地形系統,然后就只需幾句代碼,兩幅圖片,一個“活生生”的三維地形就躍然紙上了。
一、三維地形繪制思路分析
關于地形繪制的大體思路,其實非常簡單,讓我們先來看三幅圖。
我們可以發現,以上的三幅圖就概括了三維地形模擬的大體走向與思路。
首先是第一幅圖,我們在圖中可以看到,圖中描繪的就是在同一平面上的三角形網格組成的一個大的矩形區域。在這里我們把他看做是一張大的均勻的同一平面上的“漁網”,顯然它是一個二維的平面。圖中的每一個頂點都可以用一個二維的坐標(x,y)來唯一表示。
然后第二幅圖,我們就像“揠苗助長”一樣,拉著第一幅圖中的“漁網”的某些頂點往上提(或者往下壓)。這里往上提一點,那里提一點,這樣,我們就為每一個頂點都賦予了一個高度(就算有的頂點沒有移動,它的高度就為0),第一幅圖中的漁網就變形了,成了三維圖形了。每個頂點就都有了一個高度值。用z坐標來表示這個高度值的話,那么現在三維空間中這個變形的“漁網”中的每個頂點都可以用(x,y,z)來唯一表示。
最后第三幅圖,在第二幅圖中的三維“漁網”的表面我們“鍍上”紋理不盡相同的“薄膜”,也就是進行了一個紋理包裝的過程。這樣奇跡就發生了,逼真的雪原山川,奇峰怪石展現在了我們眼前。
所以,繪制三維地形的玄機,就被這三幅圖聯手一語道破了。
其中,第二幅圖中的那個“揠苗助長”的過程可謂三維地形繪制的一招“妙棋”。
這招“妙棋”我們常常是借助高度圖來完成。下面我們就來講一講什么是高度圖。
二、關于高度圖
高度圖在三維地形模擬中扮演著非常重要的角色。下面讓我們來一起探討一下高度圖的方方面面。
1.高度圖的概念
高度圖說白了其實就是一組連續的數組,這個數組中的元素與地形網格中的頂點一一對應,且每一個元素都指定了地形網格的某個頂點的高度值。當然,高度圖至少還有一種實現方案,就是用數值中的每一個元素來指定每個三角形柵格的高度值,而不是頂點的高度值。
高度圖有多種可能的圖形表示,其中最常用的一種是灰度圖(grayscale map)。地形中某一點的海拔越高的話,相應地該點對應的灰度圖中的亮度就越大。下面就是一幅灰度圖:
我們通常只為每一個元素分配一個字節的存儲空間,這樣高度也就只能在0~255之間取值。
因此,地形中最低點將用0表示,而最高點使用255表示(當然,這樣做可能會 出現一些問題,比如地形中大部分區域的高度差別都不大,但是有少數地方高度差特別大時,不過大多數情況下這個系統都能運行的很好)
這個范圍大體上來反應地形中的高度變化完全沒問題,但是在實際運用中,為了匹配3D世界的尺寸,可能需要對高度值進行比例變換,然而一進行比例變換,往往就可能超出上面的0~255這個區間。所以我們把高度數據加載到應用程序中時,我們重新分配一個整型或者浮點型的數組來存儲這些高度值,這樣我們就不必拘泥于0~255這個范圍,這樣就可以隨心所欲地構建出我們心儀的三維世界了。
對于灰度圖中的每個像素來說,同樣使用0~~255之間的值來表示一個灰度。這樣,我們就能把不同的灰度映射為高度,并且用像素索引表示不同網格。
要從高度圖創建一個地形,我們需要創建一個與高度圖相同大小的頂點網格,并使用高度圖上每個像素的高度值作為頂點的高度。例如,我們可以使用一張6×6像素分辨率的高度圖生成一個6×6大小的頂點網格。
網格上的頂點不僅包含位置,還包含諸如法線和紋理坐標的信息。下圖就是一個在XZ平面中的6×6大小的頂點網格,其中每個頂點的高度對應在Y坐標上。
另外我們在設計三維地形模擬系統的時候,會指定一下相鄰頂點的距離(水平距離和垂直距離一樣)。這個距離在上圖中用“Block Scale”表示。這個距離如果取小一點的話,會使頂點間的高度過渡平滑,但是會減少網格也就是三維地形的整體大小;反之,相鄰間頂點的距離取大一點的話,頂點間的過渡會變得陡峭,同時網格也就是三維地形的整體尺寸會相對來說變大。在上圖中,如果兩個頂點間的距離我們設為1米的話,那么所生產地形的大小就是25平方米,很好理解吧。
最常用的灰度圖格式是后綴名為RAW,我們在這里使用的高度圖文件格式就是RAW,這個格式不包含諸如圖像類型和大小信息的文件頭,所以易于被讀取。RAW文件只是簡單的二進制文件,只包含地形的高度數據。在一個8位高度圖中,每個字節都表示頂點的高度。
2.高度圖的制作
高度圖的制作一般有兩種方式。
1、以某種算法為基礎,寫個程序生成。比較有名的是Fault Formation和Midpoint Displacement這兩種算法。
2、通過圖像編輯軟件,三維建模軟件,或者專業制作地形的軟件來制作。
圖像編輯軟件首當其沖的當然是Photoshop,這個就是我們今天準備教大家的高度圖生成方式。(先把后面兩種介紹完,稍后就教大家怎么做高度圖。)
三維建模軟件就如我們之前介紹過的3DS Max和Maya了,地形制作也是三維建模界的一個分支。
然后專業制作地形的軟件,比如一款叫Terragen。這款軟件用起來也很方便,大家不妨google一下去下一個玩玩。
3、用Photoshop制作高度圖
接下來,淺墨來教大家使用Photoshop生成高度圖。
1.打開Photoshop(淺墨用的是Photoshop CS6),【Ctrl+N】或者依次點擊菜單欄上的【文件】->【新建】,新建一個畫布。如下圖,我們的畫布的大小取64x64像素。
2.創建完畫布,接下來就是最關鍵的一步。依次點擊菜單欄上的【濾鏡】->【渲染】->【云彩】。
這時候,我們就可以發現,我們創建的空白畫布上有了隨機的灰度顏色值,如果你對這次生成的隨機灰度圖不滿意的話,大可再次點擊【濾鏡】->【渲染】->【云彩】(或者【Ctrl+F】)來重新生成一次隨機的灰度效果圖,直到顏色分布滿意為止。我也也可以用畫筆來在圖上涂抹,自己來設定高度。這是淺墨通過處理后得到的一張灰度圖,這樣后面如果我們用這張圖作為高度圖,得到的就是一個凹下去型的愛心地形圖:
記得在用【云彩】濾鏡的時候,最好把調色板的顏色前景色設為純黑色,不然可能得到的隨機灰度圖效果出不來。即調色板中的顏色設置成如下圖:
另外,我們可以通過對圖片色階的調整,來對生成的灰度圖的整體顏色進行調節。比如想讓地形整體來說高一些,就把灰度圖整體調亮一些,反之,地形整體來說要顯得低一些的話,就把繪圖圖整體調按。色階對話框通過【圖像】->【調整】->【色階】打開,或者直接按快捷鍵【Ctrl+F】。
另外在點擊【圖像】->【調整】后彈出的對話框中還有曲線、色相、飽和度等等選項,大家不妨也試試。
制作完成,我們點擊【文件】->【儲存為…】或者直接按快捷鍵【Shift+Ctrl+S】來制作好的高度圖進行保存。保存的格式隨意,因為我們稍后寫的一個地形類原則上支持幾乎所有的圖片格式高度圖的導入,只不過對有些圖片格式得到的效果圖比較奇葩而已。
這里我們選擇8位的raw格式:
點擊確定后,會彈出如下導出raw的對話框,記得要把【通道儲存在】這個選項改成非隔行順序,如圖:
其實,大家不想用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為縮放的系數。
關于頂點的計算思路,我們通過下面這幅圖,就可以寫出來:
對每行的單元格數目、每列的單元格數目、單元格間的間距、高度縮放系數、地形的寬度、地形的深度、每行的頂點數、每列的頂點數、頂點總數各個擊破,就寫出了下面這幾句代碼:
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列的頂點處右下方兩個三角形的頂點索引的通用公式。下面我們來看看這個公式如何推導,下圖依舊解釋得很清楚了:
對頂點緩存中的任意一點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) }
然后是幾張程序截圖:
如果是嫌這個地形還不過癮,不夠大或者不夠陡峭,我們可以修改頂點間的間距以及縮放系數,來得到陡峭而一望無際的山峰。下圖對應的是在初始化地形時將頂點間的間距以及縮放系數調大一些的情況:
g_pTerrain->InitTerrain(200, 200, 2000.0f, 600.0f); //四個值分別是頂點行數,頂點列數,頂點間間距,縮放系數
按我們的飛行速度,達到這山峰的地圖邊界得飛幾分鐘。。。。不過這時候大黃蜂不見了,在地形整體下方了,此時攝像機初始位置也最好調一下,不然一出來也是在地形整體的下方。
文章最后,依舊是放出本篇文章配套源代碼的下載:
本節筆記配套源代碼請點擊這里下載:
【淺墨DirectX提高班】配套源代碼之十六下載
以上就是本節筆記的全部內容,更多精彩內容,且聽下回分解。
淺墨在這里,希望喜歡游戲開發系列文章的朋友們能留下你們的評論,每次淺墨登陸博客看到大家的留言的時候都會非常開心,感覺自己正在傳遞一種信仰,一種精神。
文章最后,依然是【每文一語】欄目,今天的句子是:
這世上有兩樣東西是別人搶不走的:一是藏在心中的夢想,二是讀進大腦的書。
下周一,讓我們離游戲開發的夢想更近一步。
下周一,游戲開發筆記,我們,不見不散。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。