本筆記是基於Microsfot DirectX 9.0 SDK Update的SimpleSample產生的架構.
在3D情境中,所有的對象和模型均由三角形構成;而三角形由三個頂點構成;每個頂點不僅包括其座標資訊,還包括頂點的顏色、法向量和貼圖座標等資訊。
第一步:準備頂點資訊
1. 要先定義個頂點資訊結構。
struct CUSTOMVERTEX{
D3DXVECTOR3 position; // 位置座標
D3DXVECTOR3 normal; // 法向量座標,根據當前截面的法向量跟入射光源的夾角來計算光源照射的效果。
};
這裡的CUSTOMVERTEX表示custom vertext(自訂頂點)。
2. 定義頂點格式的常量。
由於後面要將頂點資料送入Vertex Buffer,然後再轉給D3D去做著色處理,所以要告訴D3D頂點資料裡包含什麼內容。鑒於使用方便的原因,我們這裡定義一個常量來表示頂點的資料項目,即頂點裡包含了哪些資料。
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_NORMAL)
這裡的D3DFVF表示Direct3D Flexible Vertex Format(D3D動態頂點格式)。因為頂點結構內的內容包含什麼由設計者自行決定。
D3DFVF_XYZ和D3DFVF_NORMAL是D3D預先定義好的幾個標識符常數,用來表示頂點的座標位置和法向量。
3. 建立Vertex Buffer。
1). 先定義一個全域指標,用來指向Vextex Buffer。
LPDIRECT3DVERTEXBUFFER9 g_pVB; // Vertex Buffer
2). 然後在InitApp()裡初始化它。
g_pVB = NULL;
3). 最後在OnCreateDevice(),即Device的Create事件裡建立Vertex Buffer。
V_RETURN(pd3dDevice->CreateVertexBuffer(
3*2*sizeof(CUSTOMVERTEX), // 6個頂點的Buffer大小(位元組),3個是三角形正面,3個是三角形反面
0, // 指定未來頂點資訊的處理方式,這裡暫且放0
D3DFVF_CUSTOMVERTEX, // 指明我們的頂點資料結構中包含了哪些頂點資訊.
D3DPOOL_MANAGED, // 指明Vertex Buffer要存放的記憶體(顯存或系統記憶體),這裡指定由D3D自行管理
&g_pVB, // Vertex Buffer的指標
NULL // 保留參數
));
說明:在3D設計裡,每個三角形都有正反面之分,而且只有正面的三角形才能被看見。所以,設計一個三角形需要正反各3個頂點共6個。區分一個三角形是正面還是反面取決於構成這個三角形三個頂點的輸入順序和採用哪種座標系。在左手座標系裡,如果以順時針方面輸入三個頂點所構成的三角形則是正面,反之是反面;在右手座標系裡,如果以逆時針方面輸入三個頂點所構成的三角形則是正面,反之是反面;
4. 將Vertex資料填入Vertex Buffer。
由於Windows的記憶體管理機制,Vertex Buffer有可能被系統轉移出真實的實體記憶體。所以在對Vertex Buffer進行資料輸入時,需要先將其鎖定。輸入完資料後,再將其解鎖。在OnCreateDevice()裡繼續寫下面的代碼。
CUSTOMVERTEX* pVertices; // 定義一個頂點資料結構的指標,用於輸入資料到Vertex Buffer時進行索引
V_RETURN(g_pVB->Lock(0, // 從Buffer的第0個Byte開始Lock
0, // 要鎖定多少Bytes,若要鎖定整塊Buffer,則設為0
(VOID**)&pVertices, // Buffer要存放的頂點資料結構在Buffer裡的索引指標
0 // Buffer存取時的限制,若沒什麼限制,則為0
));
/*========== 正面三角形的三個頂點(採用右手座標系,逆時針輸入) =============== */
pVertices[0].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f); // 左下角頂點座標
pVertices[0].normal = D3DXVECTOR3(0.0f, 0.0f, 1.0f); // 左下角頂點法向量
pVertices[1].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f); // 右下角頂點座標
pVertices[1].normal = D3DXVECTOR3(0.0f, 0.0f, 1.0f); // 右下角頂點法向量
pVertices[2].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f); // 中間上面頂點座標
pVertices[2].normal = D3DXVECTOR3(0.0f, 0.0f, 1.0f); // 中間上面頂點法向量
/*========== 反面三角形的三個頂點(採用右手座標系,順時針輸入) =============== */
pVertices[3].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f);
pVertices[3].normal = D3DXVECTOR3(0.0f, 0.0f, -1.0f);
pVertices[4].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f);
pVertices[4].normal = D3DXVECTOR3(0.0f, 0.0f, -1.0f);
pVertices[5].position = D3DXVECTOR3(-1.0f, -1.0f, 0.0f);
pVertices[5].normal = D3DXVECTOR3(0.0f, 0.0f, -1.0f);
g_pVB->Unlock(); // 資料輸入完畢後,解鎖。
說明: 一般來說,頂點的法向量等同於該頂點所構成的三角形的法向量,也就是說,頂點的法向量就是頂點所在三角形的平面法向量,而一個平面的法向量定義為垂直於這個平面的向量.向量的方向可以根據右手法則(在右手座標繫上)來獲得,即按照頂點輸入的順序握手,然後伸直大拇指,大拇指的方向就是法向量的方向.所以,正面三角形的法向量為(0.0f, 0.0f, 1.0f),即原點指向該點的向量. 反面三角形同理.
5. 設定三角形的Material,即三角形表面的光特性。
構成三角形的頂點資料被存放入頂點緩衝區(Vertex Buffer)後,然後需要設定三角形的Material。Material指的是模型表面的材質對光的反應特性。在3D程式裡,通常存在三種光源:
- Diffuse Reflection --- 由非直射/斜射光源產生的光線,比如其他物體的反射光線。
- Specular Reflection --- 由直射/斜射光源產生的光線
- Ambient Light --- 純粹由其他物體表面所反射的微弱光源所產生的光線,它通常指情境裡最基本的光源亮度。
Material通常包含Ambient、Specular、Diffuse、Emissive和Power。除了Power是浮點數,其他的都是RGB顏色值。它們在Material中代表的意思如下:
- Ambient -- 被Ambient Light照到時物體表面所表現出來的顏色。
- Specular -- 被Specular Reflection照到時物體表面所表現出來的顏色,因為是直射,所以光線比較強,通常該值常被設定為相當於白色或比較亮的顏色。
- Diffuse -- 被Diffuse Reflection照到時物體表面所表現出來的顏色,特別的,在Material設定裡,Diffuse代表物體表面的顏色,因為在光線的Diffuse Reflection下,物體表面所反射的光的顏色就代表物體的顏色。
- Emissive -- 物體自己發光的顏色。
- Power -- 是Specular的相關值,Power越大,Specular所反映出來的光就越亮,越集中。
有了上面的基本知識,開始設定Material, 通常設定的過程放在OnResetDevice()中,即Device的Reset事件中處理。
D3DMATERIAL9 mtrl;
ZeroMemory(&mtrl, sizeof(D3DMATERIAL9));
mtrl.Diffuse.r = mtrl.Ambient.r = 1.0f;
mtrl.Diffuse.g = mtrl.Ambient.g = 0.0f;
mtrl.Diffuse.b = mtrl.Ambient.b = 0.0f;
mtrl.Diffuse.a = mtrl.Ambient.a = 1.0f;
pd3dDevice->SetMatrial(&mtrl);
上面的代碼將Material的Diffuse和Ambient設定為RGBA(1,0,0,1),即紅色不透明。其餘的Specular、Emissive和Power均為0。
6. 設定三角形的Texture,即三角形表面的貼圖。
Texture指的是模型上的貼圖,因為真實世界裡物體的質料太複雜,不可能用電腦模擬出來,最簡單的方法,就是在模型的表面貼上該質料的圖片。由於當前的三角形的表面不需要貼圖,這部分可以略掉。
7. 著色狀態值的設定
D3D結構本身是一個狀態機器,每次著色(Render)時,都會參考目前的狀態值,因此要先設定一些狀態值。D3D提供了一個設定狀態值的函數SetRenderState。
我們在OnResetDevice()中設定了幾個基本狀態值:
pd3dDevice->SetRenderState(D3DRS_DITHERENABLE, FALSE); // 不需要顏色補差
pd3dDevice->SetRenderState(D3DRS_SPECULARENABLE,FALSE); // 關閉Specular光源
pd3dDevice->SetRenderState(D3DRS_ZENABLE,TRUE); // 開啟深度值
pd3dDevice->SetRenderState(D3DRS_AMBIENT,0x000F0F0F); // 開啟基本環境光線源
這裡第一句是要關閉顏色補差,即取消Dither功能。Dither就是指顏色補差法,如果一個系統經過種種計算出來的顏色值不在目前的調色盤中,那麼可以用近似的幾種顏色來表示,這就是Dither。
最後一句是開啟基本環境光線源,0x000F0F0F是環境光線源的ARGB(00,0F,0F,0F)。若當前環境內的物體的Material的Ambient不為0的話,它就會作用於當前環境內的物體。Material的Ambient為當前物體在基本環境光線源的作用下所顯示的顏色。
8. Matrix初始化.
Matrix在3D程式裡,主要用來做座標轉換。常用的Matrix有3個,分別是World Matrix、View Matrix和Projection Matrix。在OnResetDevice()裡需要對這三個Matrix進行初始化和設定。
1). World Matrix
它主要用於轉換物件模型的座標。這裡暫時不需要,只需要將其填成單位矩陣(Identity Matrix)即可(必填).
D3DXMATRIX matIdentity; // 定義一個Matrix
D3DXMatrixIdentity(&matIdentity); // 將其轉變成單位矩陣
pd3dDevice->SetTransform(D3DTS_WORLD, &matIdentity); // 設定World Matrix
2). View Matrix
它主要用來設定觀察者,即觀察者的View,也就是對Camera的位置設定。
D3DXMATRIX matView; // 定義一個Matrix
D3DXVECTOR3 vFromPt = D3DXVECTOR3(0.0f, 0.0f, 5.0f); // Camera位置,因為三角形位於z=0的XY平面上。
D3DXVECTOR3 vLookatPt = D3DXVECTOR3(0.0f, 0.0f, 0.0f); // 目標的位置(0,0,0),即三角形所在的平面位置
D3DXVECTOR3 vUpVec = D3DXVECTOR3(0.0f, 1.0f, 0.0f); // Camera的方向向量,朝正Y軸方向。即端正了照相機照相
D3DXMatrixLookAtRH(&matView, &vFromPt, &vLookatPt, &vUpVec); // 產生View Matrix
pd3dDevice->SetTransform(D3DTS_VIEW, &matView); // 用產生的View Matrix來設定系統的View Matrix
上述的過程主要是通過設定Camera的位置和其方向向量,還有Camera所要照相的目標位置來產生一個View Matrix,然後用它來設定系統裡的View Matrix.
3). Projection Matrix
Projection Matrix幾乎是每個3D程式必需的,它的主要用途是將3D情境投影到視窗上。
D3DXMATRIX matProj;
FLOAT fAspect = ((FLOAT)pBackBufferSurfaceDesc->Width)/pBackBufferSurfaceDesc->Height;
D3DXMatrixPerspectiveFovRH(&matProj, D3DX_PI/4, fAspect, 1.0f, 100.0f);
pd3dDevice->SetTransform(D3DTS_PROJECTION, &matProj);
3D的投影過程就是將Frustum(平頂菱椎體,即Near Plane和Far Plane之間的部分)中的景物投影到視窗(Near Plane)上,而要產生負責投影計算的Projection Matrix,需要Near, Far, FOV及Aspect等值。他們分別的含義如下:
- Near -- Near Plane到Camera的距離
- Far -- Far Plaen到Camera的距離
- FOV -- 指垂直方向的觀察角度,也就是眼睛上下俯視的觀察角度
- Aspect -- 視窗的寬高比
上面的代碼裡分別設定Near = 1.0f; Far = 100.0f; FOV = PI/4 = 45度; Aspect的值由調用OnResetDevice時傳進來的pBackBufferSurfaceDesc的寬高比可的。然後通過D3DXMatrixPerspectiveFovRH計算獲得Projection Matrix, 最後用這個Projection Maxtrix來設定系統。
9. 建立光源
D3D中主要有三種光源:
- Point Light(點光源) -- 指位於某個位置的發光體,它所照射的光線是全方位的。
- Directional Light(定向光線源) -- 指只有方向性而無發光點位置的光線,且其照射的光線是平行光線,因其發光點位置可以假設為無窮遠,例如生活中的太陽光。
- Spot Light(聚光燈) -- 指某種位置所照射出的光線,照射範圍呈漏鬥狀。例如舞台上的燈光。
在3D程式中,計算光源明暗的主要原理就是先求出光線的向量與物體表面的法向量,然後再根據這兩個向量的夾角來計算此物體表面的受光強度。在本例中。只設定了一種光源Directional Light。 代碼也是在OnResetDevice()中實現。
D3DLIGHT9 light; // 定義一個光源變數
D3DXVECTOR3 vecLightDirUnnormalized(-1.0f, -1.0f, -2.0f); // 定義一個光線的向量,即原點到該點的向量
ZeroMemory(&light, sizeof(D3DLIGHT9));
light.Type = D3DLIGHT_DIRECTIONAL; // 設定光源類型
light.Diffuse.r = 1.0f; // 設定光源顏色
light.Diffuse.g = 1.0f;
light.Diffuse.b = 1.0f;
D3DXVec3Normalize((D3DXVECTOR3*)&light.Direction, &vecLightDirUnnormalized); // 設定向量單位化
pd3dDevice->SetLight(0,&light); // 0表示系統光源的編號,設定0號光源
pd3dDevice->LightEnable(0, TRUE); // 0表示系統光源的編號,啟用0號光源
pd3dDevice->SetRenderState(D3DRS_LIGHTING, TRUE); // 最後用光源對環境進行著色處理,讓情境具備光源效果
10. 設定CullMode
三角形分正反面,為了提高計算效率,我們可以利用CullMode來不計算反面的三角形(因為看不到,沒有計算的必要)。這也是在OnResetDevice()裡設定。
pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); // 刪除順時針的面。
11. 轉動三角形
到此,基本上已經完成了對情境內三角形的各種屬性的設定。如果需要在情境內控制三角形,則需要將控制碼放在OnFrameMove()裡。我們這裡要用上、下、左和右鍵來控制三角形。要先定義一個全域結構和幾個變數。
struct UserInput{
BOOL bRotateUp; // 記錄上鍵
BOOL bRotateDown; // 記錄下鍵
BOOL bRotateLeft; // 記錄左鍵
BOOL bRotateRight; // 記錄右鍵
};
FLOAT g_fWorldRotX;
FLOAT g_fWorldRotY;
UserInput g_UserInput;
設計一個函數UpdateInput來更新使用者輸入。
void UpdateInput(UserInput* pUserInput)
{
pUserInput->bRotateUp = (GetAsyncKeyState(VK_UP)&0x8000) == 0x8000;
pUserInput->bRotateDown = (GetAsyncKeyState(VK_Down)&0x8000) == 0x8000;
pUserInput->bRotateLeft = (GetAsyncKeyState(VK_Left)&0x8000) == 0x8000;
pUserInput->bRotateRight = (GetAsyncKeyState(VK_Right)&0x8000) == 0x8000;
}
在InitApp()裡初始化這幾個全域變數
ZeroMemory(&g_UserInput, sizeof(UserInput));
g_fWorldRotX = 0.0f;
g_fWorldRotY = 0.0f;
最後在OnFrameMove()裡實現三角形的轉動控制,通過計算對Y軸和X軸的旋轉計算來更新World Matrix。
UpdateInput(&g_UserInput);
D3DXMATRIX matWorld; // 定義一個World Matrix
D3DXMATRIX matRotY; // 定義一個Y軸 Matrix
D3DXMATRIX matRotX; // 定義一個X軸 Matrix
if(g_UserInput.bRotateLeft && !g_UserInput.bRotateRight)
g_fWorldRotY += fElamsedTime;
if(!g_UserInput.bRotateLeft && g_UserInput.bRotateRight)
g_fWorldRotY -= fElamsedTime;
if(g_UserInput.bRotateUp && !g_UserInput.bRotateDown)
g_fWorldRotX += fElamsedTime;
if(!g_UserInput.bRotateUp && g_UserInput.bRotateDown)
g_fWorldRotX += fElamsedTime;
D3DXMatrixRotationX(&matRotX, g_fWorldRotX); // 產生一個X軸Matrix
D3DXMatrixRotationY(&matRotY, g_fWorldRotY); // 產生一個Y軸Matrix
D3DXMatrixMultiply(&matWorld, &matRotX, &matRotY); // 產生一個World Matrix
pd3dDevice->SetTransform(D3DTS_WORLD, &matWorld); // 設定系統的World Matrix
12. 畫出三角形
在定義了三角形的各種屬性和運動控制以後,現在就要在OnFrameRender()裡畫出三角形。
pd3dDevice->SetStreamSource(
0, // 將Vertex Buffer放入第0個流中
g_pVB, // Vertex Buffer的指標
0, // 從第0個Vertex開始
sizeof(CUSTOMVERTEX) // 每個Vertex的Bytes
);
pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX); // 告訴系統,Vertex Buffer裡包含了哪些資料項目
pd3dDevice->DrawPrimitive(
D3DPT_TRIANGLELIST, // 要畫的類型
0, // 從第0個Vertex開始
2 // 要畫2個三角形
); // 畫出三角形
13. 釋放資源
程式退出時,需要將Vertex Buffer釋放掉. 這放到OnDestroyDevice()裡處理.
SAFE_RELEASE(g_pVB);