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工做。可能在之后的文章中我会讨论更多的技巧和效果。