http://flcstudio.blog.163.com/blog/static/756035392008115111123672/程序員
最近,我看到不少關於DirectX8在最新的API中摒棄DirectDraw的問題。不少人回到了之前的DX7.1中。我能夠理解那些在DX7.1中有不少開發經驗的人爲何這樣作,可是有不少問題倒是來自於那些剛學DX,尚未學過之前的API的初學者。人們爭辯說不少人沒有3D硬件,所以D3D對於DirectDraw是個錯誤的選擇。我不相信那是真的,在D3D中進行2D渲染只須要作一點頂點操做,而其餘的事情均可以被精簡來提升填充率。簡言之,在D3D中使用2D硬件進行2D渲染,能夠作到和DirectDraw同樣好的性能,有很好的填充率。而優勢是,程序員能夠學習最新的API,而且在更新的硬件中得到更好的性能。這篇文章將給出一個在DX8中進行2D渲染的框架,以便於從DirectDraw到Direct3D的轉變。在每一節裏,你會看到一些你不喜歡的東西(「我是一個2D程序員,我不用關心頂點!」)。可是,請放心,只要你將這個簡單的框架實現一次,你就不再會考慮那些了。
假設你已經有DX8 SDK,那兒有一組指南講述瞭如何建立一個D3D設備,如何放置渲染循環,所以我不想再在這上面花費時間。按照這篇文章的意圖,我將談論位於[DX8SDK]samplesMultimediaDirect3DTutorialsTut01_CreateDevice目錄裏的指南,你可能將它放到了任何地方。在那個例子中,我將加入如下函數:
- 其餘全部事情都設置完以後,這個函數被app調用,你已經建立了你的設備而且全部東西都已經初始化了。若是你跟着往下看指南里的代碼,你會看到WinMain是這樣的:安全
if( SUCCEEDED( InitD3D( hWnd ) ) )
{
PostInitialize(200.0f, 200.0f); // This is my added line. The values of
// 200.0f were chosen based on the sizes
// used in the call to CreateWindow.app
ShowWindow( hWnd, SW_SHOWDEFAULT );
...框架
void Render2D()函數
- 這個函數當你渲染你的場景時會被調用,指南里的Render函數如今看起來是這樣的:性能
VOID Render()
{
if( NULL == g_pd3dDevice )
return;學習
// Clear the backbuffer to a blue color
g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );優化
// Begin the scene
g_pd3dDevice->BeginScene();編碼
Render2D(); //My added line...3d
// End the scene
g_pd3dDevice->EndScene();
// Present the backbuffer contents to the display
g_pd3dDevice->Present( NULL, NULL, NULL, NULL );
}
好了,這就是咱們的程序外殼,如今來準備好的內容填充它吧...
爲在D3D中進行2D繪製進行設置
注意:從這兒開始咱們就要談論一些和D3D相關的使人討厭的數學知識了。不要被嚇倒了---若是你願意,你能夠選擇乎略大多數細節。大多數Direct3D繪製都受三個矩陣控制:投影矩陣,世界矩陣,觀察矩陣。咱們將談論的第一個矩陣是投影矩陣。你能夠認爲投影矩陣定義了你的攝像機的鏡頭屬性。在3D應用裏,它定義了象透視方法,等等的東西。可是咱們用不着透視---咱們正在談論2D!!因此咱們只談論正交投影。簡短得說,就是讓咱們進行2D繪製而不用考慮那些附加在3D繪製中的屬性。爲了建立一個正交投影矩陣,咱們須要調用D3DXMatrixOrthoLH函數,它將爲咱們建立一個矩陣。其餘的矩陣(觀察矩陣和世界矩陣)定義了攝像機的位置和世界(或一個在世界裏的對象)的位置。爲了咱們的2D繪製,咱們不須要移動攝像機,也不用想移動世界,因此咱們將使用一個單位矩陣,將攝像機和世界放置在缺省的位置。咱們能夠用D3DXMatrixIdentity函數來建立單位矩陣。咱們須要加入下面的頭文件,以使用D3DX函數。
PostInitialize函數如今是這樣子的:
void PostInitialize(float WindowWidth, float WindowHeight)
{
D3DXMATRIX Ortho2D;
D3DXMATRIX Identity;
D3DXMatrixOrthoLH(&Ortho2D, WindowWidth, WindowHeight, 0.0f, 1.0f);
D3DXMatrixIdentity(&Identity);
g_pd3dDevice->SetTransform(D3DTS_PROJECTION, &Ortho2D);
g_pd3dDevice->SetTransform(D3DTS_WORLD, &Identity);
g_pd3dDevice->SetTransform(D3DTS_VIEW, &Identity);
}
咱們如今正在爲2D繪製進行設置,咱們須要繪製些東西。這樣設置了以後,咱們的繪製區域就是從-WindowWidth/2 到 WindowWidth/2和從 -WindowHeight/2 到 WindowHeight/2。要注意的一件事是,在代碼裏,寬度和高度都是用象素爲單位指定的。這容許咱們用象素來考慮全部的事情,但咱們也能設置寬度和高度爲1.0,而後容許咱們用屏幕空間的百分比來指定大小,這樣就很容易的支持了多分辨率的狀況。改變矩陣就可以支持各類各樣的巧妙的事情,但爲了簡單起見,咱們如今將談論象素。
設置一個2D「面板」
當我進行2D繪製時,我有一個叫CDX8Panel的類,它裝封了全部我繪製2D矩形所須要的東西。簡單起見,它消除了C++說明,我已經將代碼拿了出來了。不管如何,當咱們建造咱們的一個繪製面板的代碼時,你將可能看到一個類的價值,或者若是你不使用C++時一個更高層的API的價值。一樣,依靠ID3DXSprite接口,咱們也可得以更加清閒。我將在這兒解釋最基本的東西,以展顯事情工做的方法,可是若是Sprite接口適合你的須要,你也可使用它。
個人面板定義是一個簡單的2D紋理矩形,咱們將會把它繪製到屏幕上。繪製一個面板很是相似於2D的blit操做。有經驗的2D程序員可能會想一個blit操做會有大量的工做,可是這些工做完成了不少容許的特效。首先,咱們不得不考慮咱們的矩形的幾何結構。這樣就包括了關於頂點的思想。若是你有3D硬件,硬件將很是快地處理這些頂點。若是你只有2D硬件,咱們所談論的如此少的頂點也將很快的被CPU處理完成。首先,讓咱們定義咱們的頂點格式。將如下的代碼放置到靠近#i nclude:的地方
struct PANELVERTEX
{
FLOAT x, y, z;
DWORD color;
FLOAT u, v;
};
#define D3DFVF_PANELVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1)
這個結構和靈活的頂點格式(FVF)定義了咱們所談論的包含位置,顏色和一組紋理座標的頂點。
如今咱們須要一個頂點緩衝。加入下面代碼行到全局列表中。又是爲了簡單,我讓它成爲全局的---這可不是一個好的編碼習慣的示例。
LPDIRECT3DVERTEXBUFFER8 g_pVertices = NULL;
如今,加入下面的代碼行到PostInitialize函數(下面再說明):
float PanelWidth = 50.0f;
float PanelHeight = 100.0f;
g_pd3dDevice->CreateVertexBuffer(4 * sizeof(PANELVERTEX), D3DUSAGE_WRITEONLY, D3DFVF_PANELVERTEX, D3DPOOL_MANAGED, &g_pVertices);
PANELVERTEX* pVertices = NULL;
g_pVertices->Lock(0, 4 * sizeof(PANELVERTEX), (BYTE**)&pVertices, 0);
//Set all the colors to white
pVertices[0].color = pVertices[1].color = pVertices[2].color = pVertices[3].color = 0xffffffff;
//Set positions and texture coordinates
pVertices[0].x = pVertices[3].x = -PanelWidth / 2.0f;
pVertices[1].x = pVertices[2].x = PanelWidth / 2.0f;
pVertices[0].y = pVertices[1].y = PanelHeight / 2.0f;
pVertices[2].y = pVertices[3].y = -PanelHeight / 2.0f;
pVertices[0].z = pVertices[1].z = pVertices[2].z = pVertices[3].z = 1.0f;
pVertices[1].u = pVertices[2].u = 1.0f;
pVertices[0].u = pVertices[3].u = 0.0f;
pVertices[0].v = pVertices[1].v = 0.0f;
pVertices[2].v = pVertices[3].v = 1.0f;
g_pVertices->Unlock();
這實際上比看起來還要簡單。首先,我構造了面板的大小,咱們有一些工做會用到它們。接下來,我請求設備建立一個包含用個人格式定義的四個頂點的足夠大內存的頂點緩衝。而後我鎖定緩衝以使我可以設置頂點的值。值得注意的一點是,鎖定緩衝是一很昂貴的,因此,我將只這樣作一次。咱們能夠操做這些頂點而不用鎖定,不過咱們將在之後討論。在這個例子中,我設置了四個對(0,0)居中的點。記住這一點,之後會有衍生而出的討論。另外,我設置了紋理座標。SDK已經很好的說明了,因此我沒有進行討論。簡短的說就是咱們進行了設置,來繪製整個紋理。這樣,如今咱們已經設置好了矩形。下一步就是繪出它?
繪製面板
繪製矩形是很簡單的。增長下面的代碼行到你的Render2D函數:
g_pd3dDevice->SetVertexShader(D3DFVF_PANELVERTEX);
g_pd3dDevice->SetStreamSource(0, g_pVertices, sizeof(PANELVERTEX));
g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLEFAN, 0, 2);
這些行告訴設備頂點如何被格式化,使用哪些頂點,以及如何使用它們。我選擇將這些看成三角形扇進行繪製,由於這樣比繪製兩個三角形更緊湊。注意,由於咱們沒有和別的頂點格式或頂點緩衝的處理,咱們能夠移動第一行到咱們的PostInitialize函數裏去。我將它們放到這兒是爲了強調你不得不告訴設備它將要處理的是什麼。若是你不這樣作,它將假設頂點是不一樣格式的,而且致使崩潰。這時,你就能夠編譯運行代碼了,若是一切正常,你會看到在藍色的背景裏有一個黑色的矩形。這還不是很正確的,由於咱們設置頂點的顏色是白色的。這個問題是設備容許了光照,這是咱們不須要的。經過在PostInitialize函數里加入這行來關掉光照:
g_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
如今,從新編譯,設備將會使用頂點的顏色了。若是你喜歡,你能夠改變頂點的顏色看看效果。到目前爲止,一切順利,可是一個顯示一個白色矩形的遊戲看上去是很使人厭煩的,咱們尚未觸及到blit一個位圖。因此,咱們不得不加入紋理。
爲面板粘貼紋理
紋理是一個可以從文件裝入或者經過數據生成的基本的位圖。爲簡單起見,咱們只使用文件。將下面的變量加入到你的全局變量中:
LPDIRECT3DTXTURE8 g_pTexture = NULL;
這即是咱們將要使用的紋理對象。加入這行代碼到PostInitialize函數以從文件裝入紋理。
D3DXCreateTextureFromFileEx(g_pd3dDevice, [Some Image File], 0, 0, 0, 0,
D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, D3DX_DEFAULT,
D3DX_DEFAULT , 0, NULL, NULL, &g_pTexture);
你能夠用你選擇的文件名替換[Some Image File]。D3DX函數能夠裝入不少標準格式的文件。咱們所使用的象素格式是有alpha通道的,因此咱們裝入帶alpha通道的格式文件,象.dss文件。另外,我也乎略了ColorKey參數,可是你也能夠指定一個ColorKey以進行透明。我會回頭來討論一點關於透明的知識。如今,咱們有了一個紋理而且已經裝入了一個圖片。而後咱們要告訴設備使用它。加入下面的代碼行到Render2D函數的開頭:
g_pd3dDevice->SetTexture(0, g_pTexture);
這告訴了設備用紋理來渲染三角形。這兒要特別記住的事情是考慮到簡單我沒有加入錯誤檢查。你應該進行正確的錯誤檢查,以肯定在紋理被使用以前已經被實際的裝入了。一個可能的錯誤是,在不少硬件中,紋理的大小必須是2的冪,如:64X64,128X512,等等。對於最新的nVidia硬件,這個約束再也不是正確的了,可是安全起見,請使用2的冪。這個限制讓不少人感到煩心,因此一下子我會告訴你如何繞過這個限制。如今,編譯運行,你能夠看到你的圖片已經映射到了三角形上了。
紋理座標
注意紋理會被拉伸和縮短以適合矩形。你能夠經過調整紋理的座標來調整這些。例如,若是你把u=1.0那行改成u=0.5,那麼只有一半的紋理被使用,而剩下的另外一半不會被壓縮。因此,若是你有一個640X480的圖片,你想把它放到一個640X480的窗口中去,你應該將640X480大小的圖片放到一個1024X512大小的紋理中去而後指定紋理座標爲0.625,0.9375。你可使用紋理中剩餘的部分來放置那些會被映射到其餘的面板中去的子圖片(經過相應的紋理座標)。一般,你會想優化紋理被使用的方式,由於它們吃光了圖片內存而且在總線中移動。這看上去很象blit中的大量的工做,可是不少都會被新式的爲3D進行優化的顯卡處理掉了。此外,多多考慮如何在系統中移動大塊的內存決不是一個壞的想法。可是我仍是開始個人演說吧。
讓咱們來看看咱們走了多遠。首先,咱們寫了不少代碼來blit一個簡單的位圖。可是,但願你可以看到一些好處和機會。例如,紋理座標自動縮放圖片以適應咱們的幾何定義。這爲咱們作了不少工做,可是考慮到後面。若是咱們設置使用一個基於百分比映射的垂直矩陣,而且,咱們指定一個佔據屏幕底部四分之一位置的面板(讓咱們說它是UI吧),並且咱們也用正確的紋理座標來指定它的紋理,這樣,咱們的UI在任何選定的窗口/屏幕大小下都會被自動的正確繪製出來。(Not exactly cold fusion),但這只是不少例子中的一個。如今咱們已經讓紋理能夠工做的很好了,咱們回過頭來談論一下透明。
透明
象我之前所說的,加入透明的一個簡單的方法就是在調用D3DXCreateTextureFromFileEX函數裏指定一個ColorKey值。另外一個辦法是使用一個實際帶alpha通道的圖片。不管使用哪一種方法爲紋理指定透明(使用alpha通道,或者ColorKey),而後運行,你都會看不到有什麼區別。這是由於alpha混合尚未被容許。在PostInitialize中加入這些行以容許alpha混合:
g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
g_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
g_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
g_pd3dDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
第一行容許了混合。下兩行指定混合如何工做。這會有不少的可能性,不過這是最基本的類型。最後一行進行一些設置以至當改變頂點顏色的alpha成分時會縮放紋理值來減弱整個面板。關於可以使用設置的更深層討論,請參見SDK。一旦這些行被加入進來,你會看到正確的透明。試着改變頂點的顏色來看看它將如何影響面板。
移動面板
如今咱們的面板已經有了不少咱們須要的視覺屬性,但它還只是粘在咱們的視口中央。在遊戲中,你能夠想讓一些東西移動起來。一個顯而易見的方法是從新鎖定頂點,而後改變它們的位置。千萬不要這樣作!鎖定是很昂貴的操做,它包括數據的移動,而且這是沒必要要的。一個更好的方法是指定世界變換矩陣來移動這些點。對於不少人來講,矩陣看上去是有一點嚇人的,可是在D3DX中有一大羣函數讓矩陣使用起來很是簡單。例如,爲了移動面板,在Render2D函數的開頭加入下面的代碼:
D3DXMATRIX Position;
D3DXMatrixTranslation(&Position, 50.0f, 0.0f, 0.0f);
g_pd3dDevice->SetTransform(D3DTS_WORLD, &Position);
這裏建立了一個能夠在X方向移動面板50個象素的矩陣,而後告訴設備應用這個移動。這能夠被裝封到一個象MoveTo(X,Y)的函數裏去,不過我沒有實際給出這樣的代碼。前面,我說過要記住頂點是相對於原點來定義的。由於咱們是這樣作的,因此,平移(移動)移動了面板的中心位置。若是你認爲移動左上角或是其餘的角會更加適合,請改變頂點定義的方式。你也能夠經過傳遞正確的參數到MoveTo函數來建立不一樣的座標系統。例如,咱們的視口當前是從-100到100。若是我想將視口認爲是從0到200那樣來使用MoveTo函數,我能夠簡單的在我調用D3DXMatrixTranslation時從X座標中減去100來進行更正。有不少方法能夠很快的改變以使你能看到你所想要的效果,可是,做爲實驗這將提供一個好的基礎。
其餘的矩陣操做
有不少其餘的矩陣操做能夠影響面板。最有趣的多是縮放和旋轉了。有一些D3DX函數能夠很好的建立這些矩陣。我將把這些實驗留給你來作,不過這兒有一些提示。關於Z軸的旋轉將會在屏幕上旋轉。而關於X和Y軸的旋轉將看上去象是Y軸和X軸在收縮。另外,應用多個操做的方法是經過乘法,而後將結果矩陣送給設備:
D3DXMATRIX M = M1 * M2 * M3 * M4;
g_pd3dDevice->SetTransform(D3DTS_WORLD, &M);
不過,記住矩陣乘法的結果是依賴於操做數的順序的。例如,Rotation*Position將移動面板而後旋轉它。Position*Rotation將致使一個沿軌道而行的效果。若是你排列了幾個矩陣在一塊兒,可是獲得了並不期待的結果,請仔細的看看排列的順序。
當你變得更加輕鬆時,你能夠想去試試象紋理矩陣這樣的東西,它將容許你移動紋理座標。你也能夠移動觀察矩陣來影響你的座標系統。記得一點:鎖定是很是昂貴的,在你鎖定你的頂點緩衝以前,老是先看看象矩陣這樣的東西。
裝封
看看這兒列出的全部的代碼,爲了進行blit咱們走了很長的一段路,可是好事是不少均可以被裝封到一些小的函數或是類中去,這樣咱們就能夠一勞永逸了。請注意,這兒是使用一種很是梗概的並且沒有優化的方法來表示的。有不少方法能夠將這些包裝起來以得到最大的收益。這多是在當前的和之後的硬件上建立2D應用程序的最佳方法,並且你也能夠得到在硬件上能夠很簡單的實現那些效果的好處。這種方法也能夠幫助你在3D中混合進2D元素,由於在矩陣面前,它們是同樣的。這些代碼也能夠簡單的適合在OpenGL中進行2D工做,因此你甚至能夠寫一個抽象裝封來支持兩種API。個人但願是這可讓人們使用DX8來作2D工做。可能在之後的文章中我會討論更多的技巧和效果。