一.頂點緩存與索引緩存
3D中,各種圖形一般都是由多邊形來逼近的,一般采用三角形來逼近。例如像下圖展示的那樣:
這個藍色的球體是由大量的三角形來組成,當然三角形的數量越多球體就會顯得更加的逼真。需要指出的是,任何物體都可以用三角形網格來逼近表示,三角形網格是構建物體模型的基本單元。而一個三角形是由三個頂點組成,所以頂點就可以說是組成物體模型的基本單位。這里的頂點並不像我平常所說的點一樣,它不僅僅只保存了位置信息,還有可以保存顏色,法線,紋理坐標等信息。
1.頂點緩存
在D3D中,頂點的具體表現形式是頂點緩存(Vertex Buffer),頂點緩存保存了頂點數據的內存空間。我們可以創建很多的頂點,將其保存在頂點緩存對應的內存空間,然后就可以調用相應的函數渲染出頂點對應的圖形。使用頂點緩存的具體步驟如下:
(1)定義頂點結構
前面說過,頂點中可以保存的內容不限於位置信息,還可以有很多其他的內容。那我們需要頂點保存什么信息呢,所以我們必須先定義頂點結構,確定頂點中需要保存那些信息。頂點的定義很簡單,就是一個簡單的結構體定義。例如下面的頂點結構保存了頂點的位置信息和顏色信息:
struct Vertex { float x, y, z; D3DCOLOR color; };
也可以包含其他信息,例如下面的包含了位置信息和紋理坐標:
struct Vertex { float x, y, z; lloat u, v; };
頂點結構定義好了,這個還不夠,畢竟只是你知道這個頂點結構包含了什么信息。D3D並不知道你這個結構所包含的信息,所以你還必須指定頂點結構的格式。靈活頂點格式(Flexible Vertex Format,FVF)用來描述三角形網格的每個頂點。靈活頂點格式可以讓我們隨心所欲地自定義其中所包含的頂點屬性信息。例如,上面的第一個頂點結構的靈活頂點格式可以按如下方式定義:
#define VERTEX_FVF D3DFVF_XYZ | D3DFVF_DIFFUSE
上面的頂點格式的宏定義中用到了D3DFVF_XYZ和D3DFVF_DIFFUSE,這兩個都是D3D中已經定義好的一些宏。用來代表各種屬性,比較幾個常用的如下,其他的可以查看幫助文檔。
D3DFVF_XYZ | 包含未經過坐標變換的頂點坐標值,不可以和D3DFVF_XYZRHW一起使用 |
D3DFVF_XYZRHW | 包含經過坐標變換的頂點坐標值,不可以和D3DFVF_XYZ以D3DFVF_NORMAL一起使用 |
D3DFVF_DIFFUSE | 包含漫反射的顏色值 |
D3DFVF_SPECULAR | 包含鏡面反射的數值 |
D3DFVF_NORMAL | 包含法線向量的數值 |
D3DFVF_TEX1-TEX8 | 表示包含1~8個紋理坐標信息,是幾重紋理后綴就用幾,最多8層紋理 |
下面舉一個關於頂點結構定義的完整例子:
struct Vertex { float x, y, z; D3DCOLOR color; }; #define VERTEX_FVF D3DFVF_XYZ | D3DFVF_DIFFUSE //或者也可以用靜態變量將頂點格式定義在結構內部 struct Vertex { float x, y, z; D3DCOLOR color; const static DWORD VERTEX_FVF; }; const DWORD Vertex::VERTEX_FVF = D3DFVF_XYZ | D3DFVF_DIFFUSE;
(2)創建頂點緩存
定義好了頂點結構,就可以創建頂點緩存了。D3D中使用IDirect3DVertexBuffer9接口對象來代表頂點緩存。創建頂點緩存的基本步驟如下:
a.獲取IDirect3DVertexBuffer9接口指針
D3D中通過CreateVertexBuffer函數來獲取IDirect3DVertexBuffer9接口指針,對應函數的原型聲明如下:
HRESULT CreateVertexBuffer( [in] UINT Length, [in] DWORD Usage, [in] DWORD FVF, [in] D3DPOOL Pool, [out, retval] IDirect3DVertexBuffer9 **ppVertexBuffer, [in] HANDLE *pSharedHandle );
參數說明:
Length | 表示頂點緩沖區的長度,單位為字節 |
Usage | 設置緩存的一些附加屬性。可以設置為0,表示沒有附加屬性。具體取值參看幫助文檔 |
FVF | 頂點的靈活頂點格式 |
Pool | 用於指定頂點緩存的存儲的位置,具體參看幫助文檔 |
ppVertexBuffer | 用於返回IDirectDVertexBuffer9對象指針 |
pSharedHandle | 保留參數,一般設為NULL或者0 |
例如一個IDirectDVertexBuffer9對象指針的例子:
IDirectDVertexBuffer9 *pVertexBuffer = nullptr; pDevice->CreateVertexBuffer(sizeof(Vertex) * 3, 0, VERTEX_FVF, D3DPOOL_DEFAULT, &pVertexBuffer, nullptr);
b.數據復制
IDirectDVertexBuffer9對象指針我們已經得到了,但是我們還並沒有向緩存中寫入數據,所以說我們的下一步就是向緩存中寫入數據。
首先第一步我們需要鎖定需要寫入數據的緩存,通過調用IDirect3DVertexBuffer9::Lock()來執行該操作。函數的原型聲明如下:
HRESULT Lock( [in] UINT OffsetToLock, [in] UINT SizeToLock, [out] VOID **ppbData, [in] DWORD Flags );
OffsetToLock | 表示加鎖區域自存儲空間的起始位置到開始鎖定位置的偏移量,單位為字節 |
SizeToLock | 表示要鎖定的字節數,也就是加鎖區域的大小 |
ppbData | 指向被鎖定的存儲區的起始地址的指針 |
Flags | 表示鎖定的方式,我們可以把它設為0,具體參看幫助文檔D3DLOCK |
鎖定時的具體示意圖如下:
得到了被鎖定緩存的首地址之后就可以直接向該地址下的存儲區寫入數據了。寫入數據之后然后解鎖緩存,通過調用IDirect3DVertexBuffer9::Unlock()來實現。函數原型聲明如下:
HRESULT Unlock();
函數聲明很簡單,調用也很簡單,直接簡單調用一下就可以了。
pVertexBuffer->Unlock();
下面附上一段關於向緩存寫入數據的完整代碼:
Vertex triangle[] = { {400, 100, 0, 1.0, D3DCOLOR_XRGB(255, 0, 0)}, {700, 500, 0, 1.0, D3DCOLOR_XRGB(0, 255, 0)}, {100, 500, 0, 1.0, D3DCOLOR_XRGB(0, 0, 255)} }; void *temp; pDevice->CreateVertexBuffer(sizeof(triangle), 0, Vertex::FVF, D3DPOOL_MANAGED, &g_pVertexBuffer, NULL); g_pVertexBuffer->Lock(0, sizeof(triangle), (void **)&temp, 0); memcpy(temp, triangle, sizeof(triangle)); g_pVertexBuffer->Unlock();
(3)使用頂點緩存進行圖形渲染
現在已經創建好了頂點緩存,並且已經將其中填充好了頂點信息,下一步就可以進行渲染工作了,在正式進行渲染之前還有幾個工作要完成。
a.設置數據流
首先需要設置數據流,通過調用IDirect3DDevice9::SetStreamSource來完成。IDirect3DDevice9::SetStreamSource用於把包含的幾何體信息的頂點緩存和渲染流水線相關聯,函數原型如下:
HRESULT SetStreamSource( [in] UINT StreamNumber, [in] IDirect3DVertexBuffer9 *pStreamData, [in] UINT OffsetInBytes, [in] UINT Stride );
StreamNumber | 用於指定與該頂點緩存建立連接的數據流,由於一般只用一個數據流,所以通常設為0 |
pStreamData | 頂點緩存的指針 |
OffsetInBytes | 表示在數據流中以字節為單位的偏移量,通常設為0 |
Stride | 表示在頂點緩存中存儲的每個頂點結構的大小,單位為字節 |
一個調用的具體實例為:
pDevice->SetStreamSource(0, g_pVertexBuffer, 0, sizeof(Vertex));
b.設置靈活頂點格式
在渲染之前還需要設置靈活頂點格式,通過函數IDirect3DDevice9::SetFVF來完成,函數聲明如下:
HRESULT SetFVF( [in] DWORD FVF );
調用很簡單,參數就是頂點結構的靈活頂點格式,如下所示:
pDevice->SetFVF(Vertex::FVF);
c.進行渲染
所有東西都已經設置好了(如果使用了紋理,材質,還需要進行相關的設置),現在就可以進行渲染了。通過調用函數IDirect3DDevice9::DrawPrimitive來完成,函數的具體聲明如下:
HRESULT DrawPrimitive( [in] D3DPRIMITIVETYPE PrimitiveType, [in] UINT StartVertex, [in] UINT PrimitiveCount );
參數說明:
PrimitiveType | 繪制的圖元類型,為D3DPRIMITIVETYPE枚舉類型 |
StartVertex | 從頂點緩存中讀取頂點數據的起始索引位置 |
PrimitiveCount | 需要繪制的圖元的數量 |
說一下D3DPRIMITIVETYPE,D3DPRIMITIVETYPE是一個枚舉類型,用來表示圖元的類型,聲明如下:
typedef enum D3DPRIMITIVETYPE { D3DPT_POINTLIST = 1, D3DPT_LINELIST = 2, D3DPT_LINESTRIP = 3, D3DPT_TRIANGLELIST = 4, D3DPT_TRIANGLESTRIP = 5, D3DPT_TRIANGLEFAN = 6, D3DPT_FORCE_DWORD = 0x7fffffff } D3DPRIMITIVETYPE, *LPD3DPRIMITIVETYPE;
D3DPT_POINTLIST | 用來繪制一系列的點 |
D3DPT_LINELIST | 繪制線列,例如:1 2頂點為第一條線 3 4頂點為第二條線 以此類推 |
D3DPT_LINESTRIP | 繪制線帶,例如:1 2頂點為第一條線 2 3頂點為第二條線 以此類推 |
D3DPT_TRIANGLELIST | 繪制三角形序列,例如: 1 2 3為第一個三角形 4 5 6為第二個三角形 |
D3DPT_TRIANGLESTRIP | 繪制三角形帶, 例如:1 2 3為第一個三角形 2 3 4則為第二個三角形 |
D3DPT_TRIANGLEFAN | 繪制三角形扇,例如:1 2 3為第一個三角形 1 3 4為第二個三角形 |
調用實例如下:
pDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
(4)實例:渲染矩形
現在要畫一個矩形,有了前面的准備,應該來非常簡單了。一個矩形是由兩個三角形組成,所以,填充的頂點數據如下:
Vertex rect[] = { {100, 100, 0, 1.0, D3DCOLOR_XRGB(255, 0, 0)}, {700, 100, 0, 1.0, D3DCOLOR_XRGB(0, 255, 0)}, {100, 500, 0, 1.0, D3DCOLOR_XRGB(0, 0, 255)}, {100, 500, 0, 1.0, D3DCOLOR_XRGB(0, 0, 255)}, {700, 100, 0, 1.0, D3DCOLOR_XRGB(0, 255, 0)}, {700, 500, 0, 1.0, D3DCOLOR_XRGB(255, 255, 255)} }; void *temp; IDirect3DDevice9 *pDevice = g_pDevice->GetDevice(); pDevice->CreateVertexBuffer(sizeof(rect), 0, Vertex::FVF, D3DPOOL_MANAGED, &g_pVertexBuffer, NULL); g_pVertexBuffer->Lock(0, sizeof(rect), (void **)&temp, 0); memcpy(temp, rect, sizeof(rect)); g_pVertexBuffer->Unlock();
最后再按照前面所說的步驟,設置數據流,設置靈活頂點格式,然后最后渲染矩形。與筆記一中所不同的是,這里是畫的一個矩形,所以需要渲染兩個三角形,IDirect3DDevice9::DrawPrimitive中的第三個參數要改為2,代碼如下:
pDevice->SetStreamSource(0, g_pVertexBuffer, 0, sizeof(Vertex)); pDevice->SetFVF(Vertex::FVF); pDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);
與筆記一中畫三角形的代碼,需要改變的就上面所說的兩個地方。第一個改變寫入緩存的數據,改變圖元繪制時的數量,附上運行截圖:
2.索引緩存
在前面,我們學習了頂點緩存的知識,並且渲染了一個矩形。但是我們會發現一個問題,在向頂點緩存中寫入數據的時候,我們填充了6個頂點結構,但是一個矩形只有4個頂點。有兩個頂點被寫入緩存了兩次,這樣可能會造成一些內存的浪費,特別是需要渲染的模型很復雜的時候,那時重復的頂點會變得更多,所以在D3D中還引入了索引緩存。索引緩存((Index Buffers)),人如其名,它就是一個索引,用於記錄頂點緩存中每一個頂點的索引位置。例如一個矩形:
如果在沒有使用索引緩存的情況下,我們需要創建一個這樣的頂點緩存:
vertex = {v1, v2, v3, v3, v2, v4};
需要六個頂點來渲染這個矩形,如果現在我們使用索引緩存的話,就可以這樣實現:
vertex = {v1, v2, v3, v4}; index = {1, 2, 3, 3, 2, 4};
索引中表明了,繪制時使用的頂點在頂點緩存中的位置。D3D中用IDirect3DIndexBuffer9接口對象來表示索引緩存,它的創建和使用和頂點緩存類似。具體步驟如下:
(1)獲取IDirect3DIndexBuffer9對象指針
D3D中通過CreateIndexBuffer函數來獲取IDirect3DIndexBuffer9接口指針,對應函數的原型聲明如下:
HRESULT CreateIndexBuffer( [in] UINT Length, [in] DWORD Usage, [in] D3DFORMAT Format, [in] D3DPOOL Pool, [out, retval] IDirect3DIndexBuffer9 **ppIndexBuffer, [in] HANDLE *pSharedHandle );
這個函數中的幾個參數和CreateIndexBuffer的參數類似,除了第三個參數和第四個參數。第四個參數分別用於返回頂點緩存和索引緩存接口指針。而對於第三個參數,CreateIndexBuffer中傳入靈活頂點格式,而CreateIndexBuffer則用於指定索引的格式,為一個D3DFORMAT類型的變量。它的取值一般是以下兩個:
D3DFMT_INDEX16 |
表示為16位的索引 |
D3DFMT_INDEX32 |
表示為32位的索引 |
一個具體的實例如下:
IDirect3DIndexBuffer9 *g_pIndexBuffer = nullptr; pDevice->CreateIndexBuffer(sizeof(WORD) * 6, 0, D3DFMT_INDEX16, D3DPOOL_MANAGED, &g_pIndexBuffer, nullptr);
(2)寫入索引數據
和前面頂點緩存一樣,我們在獲取IDirect3DIndexBuffer9接口指針之后,還需要向緩存中寫入索引數據。至於寫入索引數據的方法和頂點緩存的方法一模一樣,都是通過調用Lock和Unlock來完成,這里不多說。附上代碼實例:
WORD index[] = {0, 1, 2, 2, 1, 3}; void *temp; pDevice->CreateIndexBuffer(sizeof(WORD) * 6, 0, D3DFMT_INDEX16, D3DPOOL_MANAGED, &g_pIndexBuffer, nullptr); g_pIndexBuffer->Lock(0, sizeof(index), (void **)&temp, 0); memcpy(temp, index, sizeof(index)); g_pIndexBuffer->Unlock();
(3)結合頂點緩存進行渲染
在使用索引緩存和頂點緩存進行渲染之前,也需要完成一些預先的工作。
a.設置索引
頂點緩存在進行渲染之前,需要設置數據流將它和渲染流水線相關聯。索引緩存也需要類似的工作,所以說我們首先必須要設置索引。通過IDirect3DDevice9::SetIndices函數來完成這個工作,函數的聲明如下:
HRESULT SetIndices( [in] IDirect3DIndexBuffer9 *pIndexData );
參數很簡單,就是我們前面已經填充好索引數據的IDirect3DIndexBuffer9對象的指針。調用實例如下:
pDevice->SetIndices(g_pIndexBuffer);
b.使用IDirect3DDevice9::DrawIndexedPrimitive進行渲染
設置好了索引然后就可以開始渲染了,通過調用IDirect3DDevice9::DrawIndexedPrimitive來完成,函數的聲明如下:
HRESULT DrawIndexedPrimitive( [in] D3DPRIMITIVETYPE Type, [in] INT BaseVertexIndex, [in] UINT MinIndex, [in] UINT NumVertices, [in] UINT StartIndex, [in] UINT PrimitiveCount );
參數說明:
Type | 表示將要繪制的圖元類型 |
BaseVertexIndex | 表示將要進行繪制的索引緩存的起始頂點的索引位置 |
MinIndex | 表示索引數組中最小的索引值,通常都設為0 |
NumVertices | 表示頂點的數量 |
StartIndex | 表示從索引中的第幾個索引處開始繪制我們的圖元 |
PrimitiveCount | 表示要繪制的圖元數量 |
調用實例:
pDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 4, 0, 2);
(4)實例:矩形渲染
現在進行一個矩形的渲染,只需要將頂點緩存中的頂點去掉兩個,然后創建好索引緩存,設置好相關的東西就可以渲染了。修改部分的代碼如下:
Vertex rect[] = { {100, 100, 0, 1.0, D3DCOLOR_XRGB(255, 0, 0)}, {700, 100, 0, 1.0, D3DCOLOR_XRGB(0, 255, 0)}, {100, 500, 0, 1.0, D3DCOLOR_XRGB(0, 0, 255)}, {700, 500, 0, 1.0, D3DCOLOR_XRGB(255, 255, 255)} }; void *temp; IDirect3DDevice9 *pDevice = g_pDevice->GetDevice(); pDevice->CreateVertexBuffer(sizeof(rect), 0, Vertex::FVF, D3DPOOL_MANAGED, &g_pVertexBuffer, NULL); g_pVertexBuffer->Lock(0, sizeof(rect), (void **)&temp, 0); memcpy(temp, rect, sizeof(rect)); g_pVertexBuffer->Unlock(); WORD index[] = {0, 1, 2, 2, 1, 3}; pDevice->CreateIndexBuffer(sizeof(WORD) * 6, 0, D3DFMT_INDEX16, D3DPOOL_MANAGED, &g_pIndexBuffer, nullptr); g_pIndexBuffer->Lock(0, sizeof(index), (void **)&temp, 0); memcpy(temp, index, sizeof(index)); g_pIndexBuffer->Unlock();
pDevice->SetStreamSource(0, g_pVertexBuffer, 0, sizeof(Vertex)); pDevice->SetFVF(Vertex::FVF); pDevice->SetIndices(g_pIndexBuffer); pDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 4, 0, 2);
二.背面消隱與着色模式
1.背面消隱
每個多邊形都有兩個面,一個正面,另一個是背面。通常情況下,多邊形的背面是不可見的。在D3D中會將多邊形的背面加以剔除,這個就是背面消隱。為了實現背面消隱,D3D必須區分哪些多邊形是正面朝向的,哪些多邊形是背面朝向的。在D3D中,默認頂點排列順序為順時針的三角形是正面朝向的,排列順序為逆時針的是背面朝向的。可以看見,在前面無論是三角形還是矩形的渲染中,我們三角形的排列順序都是順時針的。那是因為我們不想D3D將其剔除,而是正確的顯示出來。在D3D你也可以改變背面消隱的模式,通過SetRenderState函數來實現。函數的原型聲明如下:
HRESULT SetRenderState( [in] D3DRENDERSTATETYPE State, [in] DWORD Value );
State是一個D3DRENDERSTATETYPE枚舉類型的變量,表示要設置的狀態類型。而第一個表示要將第一個的狀態類型設置為什么,第二個參數的取值范圍取決於第一個參數。這里我們要設置背面消隱的模式,第一個就必須設置為D3DRS_CULLMODE。第二個參數這個時候可以取以下幾個值:
D3DCULL_NONE | 禁用背面消隱 |
D3DCULL_CW | 對順時針方向的三角形進行消隱 |
D3DCULL_CCW | 默認值,對逆時針方向的三角形進行消隱 |
例如,我們要對順時針方向的三角形進行消隱操作,可以進行如下調用:
pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);
2.着色模式
在上面渲染的三角形中,我們將第一個頂點的顏色設為紅色,第二頂點設為了綠色,第三個頂點設為了藍色,第四個頂點設為了灰色。但是最后運行出來的結果,有點不一樣。只有在每個頂點附近的那一塊區域和頂點的顏色相似度最高,離頂點越遠和該頂點設置的顏色差別就越大,這是為什么呢?這就是因為D3D中着色模式的原因,D3D中默認的着色模式是Gouraud着色。在Gouraud着色模式之下,圖元中各個像素的顏色值由各個頂點經過線性插值得到。例如:
上面的圖中一端的顏色為紅色,另一端的顏色為藍色。他們之間中點的顏色和3/4處的顏色值就是通過如上圖線性插值的方式得到的。所以說我們渲染出來的矩形,在離他們頂點處越近的地方,顏色值就和該頂點設置的顏色值越接近,因為在那個位置該頂點顏色在插值時的權重越大。Gouraud着色模式是D3D中默認的着色模式,D3D中還有另一種着色模式叫平面着色模式。在這種模式下,對於一個圖元,它取圖元頂點中的第一個頂點的顏色為圖元中每個像素的顏色值。例如,下面的代碼在平面着色模式下,三角形的顏色為紅色,因為第一個頂點的顏色為紅色。
Vertex rect[] = { {100, 100, 0, 1.0, D3DCOLOR_XRGB(255, 0, 0)}, {700, 100, 0, 1.0, D3DCOLOR_XRGB(0, 255, 0)}, {100, 500, 0, 1.0, D3DCOLOR_XRGB(0, 0, 255)} };
着色模式也可以通過SetRenderState函數來改變,第一個參數為D3DRS_SHADEMODE,第二個參數為可以取:
D3DSHADE_FLAT | 平面着色模式 |
D3DSHADE_GOURAUD | Gouraud着色模式 |
例如下面的代碼,可以將其設置為平面着色模式:
pDevice->SetRenderState(D3DRS_SHADEMODE, D3DSHADE_FLAT);
我們可以再上面的矩形渲染截圖中,將着色模式設置為平面着色模式,然后看下效果。附上運行截圖:
可以看到矩形變成了一半紅色,一半藍色。那是因為第一個三角形的第一個頂點顏色設置為紅色,第二個三角形的第一個頂點顏色設置為藍色。
(完)