在DirectX 8 中进行2D渲染
Background
背景
I have been reading alot of questions lately related to DirectX 8 and the exclusion of DirectDraw from the new API. Many people have fallen back to DX7. I can understand people using DX7 if they have alot of experience with that API, but many of the questions seem to be coming from people who are just learning DX, and they are stuck learning an old API. People have argued that many people don't have 3D hardware, and therefore D3D would be a bad alternative for DirectDraw. I don't believe this is true - 2D rendering in D3D requires very little vertex manipulation, and everything else boils down to fillrate. In short, 2D rendering in D3D on 2D hardware should have pretty much the same performance as DirectDraw, assuming decent fillrate. The advantage is that the programmer can learn the newest API, and performance on newer hardware should be very good. This article will present a framework for 2D rendering in DX8 to ease the transition from DirectDraw to Direct3D. In each section, you may see things that you don't like ("I'm a 2D programmer, I don't care about vertices!"). Rest assured, if you implement this simple framework once, you'll never think about these things again.
最近,我看到很多关于DirectX8在最新的API中摒弃DirectDraw的问题。很多人回到了以前的DX7.1中。我可以理解那些在DX7.1中有很多开发经验的人为什么这样做,但是有很多问题却是来自于那些刚学DX,还没有学过以前的API的初学者。人们争辩说很多人没有3D硬件,因此D3D对于DirectDraw是个错误的选择。我不相信那是真的,在D3D中进行2D渲染只需要做一点顶点操作,而其他的事情都可以被精简来提高填充率。简言之,在D3D中使用2D硬件进行2D渲染,可以做到和DirectDraw一样好的性能,有很好的填充率。而优点是,程序员可以学习最新的API,并且在更新的硬件中获得更好的性能。这篇文章将给出一个在DX8中进行2D渲染的框架,以便于从DirectDraw到Direct3D的转变。在每一节里,你会看到一些你不喜欢的东西(“我是一个2D程序员,我不用关心顶点!”)。但是,请放心,只要你将这个简单的框架实现一次,你就再也不会考虑那些了。
Getting Started
开始
Assuming you have the DX8 SDK, there are a couple tutorials that present how to create a D3D device and set up a render loop, so I don't want to spend alot of time on that. For the purposes of this article, I'll talk about the tutorial found in [DX8SDK]samplesMultimediaDirect3DTutorialsTut01_CreateDevice, although you can add it to anything. To that sample, I'll add the following functions:
假设你已经有DX8 SDK,那儿有一组指南讲述了如何创建一个D3D设备,如何放置渲染循环,因此我不想再在这上面花费时间。按照这篇文章的意图,我将谈论位于[DX8SDK]samplesMultimediaDirect3DTutorialsTut01_CreateDevice目录里的指南,你可能将它放到了任何地方。在那个例子中,我将加入以下函数:
void PostInitialize(float WindowWidth, float WindowHeight)
- this function is called by the app after everything else is set up. You've created your device and initialized everything. If you're following along with the Tutorial code, WinMain looks like this:
- 其他所有事情都设置完之后,这个函数被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.
ShowWindow( hWnd, SW_SHOWDEFAULT );
...
void Render2D()
- This function is called each time you want to render your scene. Again, the Render function of the Tutorial now looks like this:
- 这个函数当你渲染你的场景时会被调用,指南里的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...
// End the scene
g_pd3dDevice->EndScene();
// Present the backbuffer contents to the display
g_pd3dDevice->Present( NULL, NULL, NULL, NULL );
}
OK, that's our shell of an application. Now for the good stuff...
好了,这就是我们的程序外壳,现在来准备好的内容填充它吧...
Setting Up for 2D drawing in D3D
为在D3D中进行2D绘制进行设置
NOTE: This is where we start talking about some of the nasty math involved with D3D. Don't be alarmed - if you want to, you can choose to ignore most of the details?/I> Most Direct3D drawing is controlled by three matrices: the projection matrix, the world matrix, and the view matrix. The first one we'll talk about is the projection matrix. You can think of the projection matrix as defining the properties of the lens of your camera. In 3D applications, it defines things like perspective, etc. But, we don't want perspective - we are talking about 2D!! So, we can talk about orthogonal projections. To make a long story very short, this allows us to draw in 2D without all the added properties of 3D drawing. To create an orthogonal matrix, we need to call D3DXMatrixOrthoLH and this will create a matrix for us. The other matrices (view and world) define the position of the camera and the position of the world (or an object in the world). For our 2D drawing, we don't need to move the camera, and we don't want to move the world for now, so we'll use an identity matrix, which basically sets the camera and world in a default position. We can create identity matrices with D3DXMatrixIdentity. To use the D3DX functions, we need to add:
注意:从这儿开始我们就要谈论一些和D3D相关的令人讨厌的数学知识了。不要被吓倒了---如果你愿意,你可以选择乎略大多数细节。大多数Direct3D绘制都受三个矩阵控制:投影矩阵,世界矩阵,观察矩阵。我们将谈论的第一个矩阵是投影矩阵。你可以认为投影矩阵定义了你的摄像机的镜头属性。在3D应用里,它定义了象透视方法,等等的东西。但是我们用不着透视---我们正在谈论2D!!所以我们只谈论正交投影。简短得说,就是让我们进行2D绘制而不用考虑那些附加在3D绘制中的属性。为了创建一个正交投影矩阵,我们需要调用D3DXMatrixOrthoLH函数,它将为我们创建一个矩阵。其他的矩阵(观察矩阵和世界矩阵)定义了摄像机的位置和世界(或一个在世界里的对象)的位置。为了我们的2D绘制,我们不需要移动摄像机,也不用想移动世界,所以我们将使用一个单位矩阵,将摄像机和世界放置在缺省的位置。我们可以用D3DXMatrixIdentity函数来创建单位矩阵。我们需要加入下面的头文件,以使用D3DX函数。
#include <d3dx8.h>
and add d3dx8dt.lib to the list of linked libraries. Once that was set up, the PostInitialize function now looks like this:
并且加入d3dx8dt.lib到连接库列表里。当那些设置了之后,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);
}
We are now set up for 2D drawing, now we need something to draw. The way things are set up, our drawing area goes from -WindowWidth/2 to WindowWidth/2 and -WindowHeight/2 to WindowHeight/2. One thing to note, in this code, the width and the height are being specified in pixels. This allows us to think about everything in terms of pixels, but we could have set the width and height to say 1.0 and that would have allowed us to specify sizes, etc. in terms of percentages of the screen space, which would be nice for supporting multiple resolutions easily. Changing the matrix allows for all sorts of neat things, but for simplicity, we'll talk about pixels for now?
我们现在正在为2D绘制进行设置,我们需要绘制些东西。这样设置了之后,我们的绘制区域就是从-WindowWidth/2 到 WindowWidth/2和从 -WindowHeight/2 到 WindowHeight/2。要注意的一件事是,在代码里,宽度和高度都是用象素为单位指定的。这允许我们用象素来考虑所有的事情,但我们也能设置宽度和高度为1.0,然后允许我们用屏幕空间的百分比来指定大小,这样就很容易的支持了多分辨率的情况。改变矩阵就能够支持各种各样的巧妙的事情,但为了简单起见,我们现在将谈论象素。
Setting Up a 2D "Panel"
设置一个2D“面板”
When I draw in 2D, I have a class called CDX8Panel that encapsulates everything I need to draw a 2D rectangle. For simplicity, and it avoid a C++ explanation, I have pulled out the code here. However, as we build up our code to draw a panel, you'll probably see the value of such a class or higher level API if you don't use C++. Also, we are about to recreate much that goes on in the ID3DXSprite interface. I'm explaining the basics here to show the way things work, but you may want to use the sprite interface if it suits your needs.
当我进行2D绘制时,我有一个叫CDX8Panel的类,它装封了所有我绘制2D矩形所需要的东西。简单起见,它消除了C++说明,我已经将代码拿了出来了。无论如何,当我们建造我们的一个绘制面板的代码时,你将可能看到一个类的价值,或者如果你不使用C++时一个更高层的API的价值。同样,依靠ID3DXSprite接口,我们也可得以更加悠闲。我将在这儿解释最基本的东西,以展显事情工作的方法,但是如果Sprite接口适合你的需要,你也可以使用它。
My definition of a panel is simply a 2D textured rectangle that we are going to draw on the screen. Drawing a panel will be extremely similar to a 2D blit. Experienced 2D programmers may think that this is a lot of work for a blit, but that work pays off with the amount of special effects that it enables. First, we have to think about the geometry of our rectangle. This involves thinking about vertices. If you have 3D hardware, the hardware will process these vertices extremely quickly. If you have 2D hardware, we are talking about so few vertices that they will be processed very quickly by the CPU. First, let's define our vertex format. Place the following code near the #includes:
我的面板定义是一个简单的2D纹理矩形,我们将会把它绘制到屏幕上。绘制一个面板非常类似于2D的blit操作。有经验的2D程序员可能会想一个blit操作会有大量的工作,但是这些工作完成了很多允许的特效。首先,我们不得不考虑我们的矩形的几何结构。这样就包括了关于顶点的思想。如果你有3D硬件,硬件将非常快地处理这些顶点。如果你只有2D硬件,我们所谈论的如此少的顶点也将很快的被CPU处理完成。首先,让我们定义我们的顶点格式。将以下的代码放置到靠近#include:的地方
struct PANELVERTEX
{
FLOAT x, y, z;
DWORD color;
FLOAT u, v;
};
#define D3DFVF_PANELVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1)
This structure and Flexible Vertex Format (FVF) specify that we are talking about a vertex that has a position, a color, and a set of texture coordinates.
这个结构和灵活的顶点格式(FVF)定义了我们所谈论的包含位置,颜色和一组纹理坐标的顶点。
Now we need a vertex buffer. Add the following line of code to the list of globals. Again, for simplicity, I'm making it global - this is not a demonstration of good coding practice.
现在我们需要一个顶点缓冲。加入下面代码行到全局列表中。又是为了简单,我让它成为全局的---这可不是一个好的编码习惯的示例。
LPDIRECT3DVERTEXBUFFER8 g_pVertices = NULL;
Now, add the following lines of code to the PostInitialize function (explanation to follow):
现在,加入下面的代码行到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();
This is actually much simpler than it may look. First, I made up a size for the panel just so we'd have something to work with. Next, I asked the device to create a vertex buffer that contained enough memory for four vertices of my format. Then I locked the buffer so I could set the values. One thing to note, locking buffers is very expensive, so I'm only going to do it once. We can manipulate the vertices without locking, but we'll discuss that later. For this example I have set the four points centered on the (0, 0). Keep this in the back of your mind; it will have ramifications later. Also, I set the texture coordinates. The SDK explains these pretty well, so I won't get into that. The short story is that we are set up to draw the entire texture. So, now we have a rectangle set up. The next step is to draw it?
这实际上比看起来还要简单。首先,我构造了面板的大小,我们有一些工作会用到它们。接下来,我请求设备创建一个包含用我的格式定义的四个顶点的足够大内存的顶点缓冲。然后我锁定缓冲以使我能够设置顶点的值。值得注意的一点是,锁定缓冲是一很昂贵的,所以,我将只这样做一次。我们可以操作这些顶点而不用锁定,不过我们将在以后讨论。在这个例子中,我设置了四个对(0,0)居中的点。记住这一点,以后会有衍生而出的讨论。另外,我设置了纹理坐标。SDK已经很好的说明了,所以我没有进行讨论。简短的说就是我们进行了设置,来绘制整个纹理。这样,现在我们已经设置好了矩形。下一步就是绘出它?
Drawing the Panel
绘制面板
Drawing the rectangle is pretty easy. Add the following lines of code to your Render2D function:
绘制矩形是很简单的。增加下面的代码行到你的Render2D函数:
g_pd3dDevice->SetVertexShader(D3DFVF_PANELVERTEX);
g_pd3dDevice->SetStreamSource(0, g_pVertices, sizeof(PANELVERTEX));
g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLEFAN, 0, 2);
These lines tell the device how the vertices are formatted, which vertices to use, and how to use them. I have chosen to draw this as a triangle fan, because it's more compact than drawing two triangles. Note that since we are not dealing with other vertex formats or other vertex buffers, we could have moved the first two lines to our PostInitialize function. I put them here to stress that you have to tell the device what it's dealing with. If you don't, it may assume that the vertices are a different format and cause a crash. At this point, you can compile and run the code. If everything is correct, you should see a black rectangle on a blue background. This isn't quite right because we set the vertex colors to white. The problem is that the device has lighting enabled, which we don't need. Turn lighting off by adding this line to the PostInitialize function:
这些行告诉设备顶点如何被格式化,使用哪些顶点,以及如何使用它们。我选择将这些当作三角形扇进行绘制,因为这样比绘制两个三角形更紧凑。注意,因为我们没有和别的顶点格式或顶点缓冲的处理,我们可以移动第一行到我们的PostInitialize函数里去。我将它们放到这儿是为了强调你不得不告诉设备它将要处理的是什么。如果你不这样做,它将假设顶点是不同格式的,并且导致崩溃。这时,你就可以编译运行代码了,如果一切正常,你会看到在蓝色的背景里有一个黑色的矩形。这还不是很正确的,因为我们设置顶点的颜色是白色的。这个问题是设备允许了光照,这是我们不需要的。通过在PostInitialize函数里加入这行来关掉光照:
g_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
Now, recompile and the device will use the vertex colors. If you'd like, you can change the vertex colors and see the effect. So far, so good, but a game that features a white rectangle is visually boring, and we haven't gotten to the idea of blitting a bitmap yet. So, we have to add a texture.
现在,重新编译,设备将会使用顶点的颜色了。如果你喜欢,你可以改变顶点的颜色看看效果。到目前为止,一切顺利,但是一个显示一个白色矩形的游戏看上去是很令人厌烦的,我们还没有触及到blit一个位图。所以,我们不得不加入纹理。
Texturing the Panel
为面板粘贴纹理
A texture is basically a bitmap that can be loaded from a file or generated from data. For simplicity, we'll just use files. Add the following to your global variables:
纹理是一个能够从文件装入或者通过数据生成的基本的位图。为简单起见,我们只使用文件。将下面的变量加入到你的全局变量中:
LPDIRECT3DTXTURE8 g_pTexture = NULL;
This is the texture object we'll be using. To load a texture from a file, add this line to PostInitialize:
这便是我们将要使用的纹理对象。加入这行代码到PostInitialize函数以从文件装入纹理。
D3DXCreateTextureFromFileEx(g_pd3dDevice, [Some Image File], 0, 0, 0, 0,
D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, D3DX_DEFAULT,
D3DX_DEFAULT , 0, NULL, NULL, &g_pTexture);
Replace [Some Image File] with a file of your choice. The D3DX function can load many standard formats. The pixel format we're using has an alpha channel, so we could load a format that has an alpha channel such as .dds. Also, I'm ignoring the ColorKey parameter, but you could specify a color key for transparency. I'll get back to transparency in a little bit. For now, we have a texture and we've loaded an image. Now we have to tell the device to use it. Add the following line to the beginning of Render2D:
你可以用你选择的文件名替换[Some Image File]。D3DX函数可以装入很多标准格式的文件。我们所使用的象素格式是有alpha通道的,所以我们装入带alpha通道的格式文件,象.dss文件。另外,我也乎略了ColorKey参数,但是你也可以指定一个ColorKey以进行透明。我会回头来讨论一点关于透明的知识。现在,我们有了一个纹理并且已经装入了一个图片。然后我们要告诉设备使用它。加入下面的代码行到Render2D函数的开头:
g_pd3dDevice->SetTexture(0, g_pTexture);
This tells the device to render the triangles using the texture. One important thing to remember here is that I am not adding error checking for simplicity. You should probably add error checking to make sure the texture is actually loaded before attempting to use it. One possible error is that for a lot of hardware, the textures must have dimensions that are powers of 2 such as 64x64, 128x512, etc. This constraint is no longer true on the latest nVidia hardware, but to be safe, use powers of 2. This limitation bothers a lot of people, so I'll tell you how to work around it in a moment. For now, compile and run and you should see your image mapped onto the rectangle.
这告诉了设备用纹理来渲染三角形。这儿要特别记住的事情是考虑到简单我没有加入错误检查。你应该进行正确的错误检查,以确定在纹理被使用之前已经被实际的装入了。一个可能的错误是,在很多硬件中,纹理的大小必须是2的幂,如:64X64,128X512,等等。对于最新的nVidia硬件,这个约束不再是正确的了,但是安全起见,请使用2的幂。这个限制让很多人感到烦心,所以一会儿我会告诉你如何绕过这个限制。现在,编译运行,你可以看到你的图片已经映射到了三角形上了。
Texture Coordinates
纹理坐标
Note that it is stretched/squashed to fit the rectangle. You can adjust that by adjusting the texture coordinates. For example, if you change the lines where u = 1.0 to u = 0.5, then only half of the texture is used and the remaining part will not be squashed. So, if you had a 640x480 image that you wanted to place on a 640x480 window, you could place the 640x480 image in a 1024x512 texture and specify 0.625, 0.9375 for the texture coordinates. You could use the remaining parts of the texture to hold other sub images that are mapped to other panels (through the appropriate texture coordinates). In general, you want to optimize the way textures are used because they are eating up graphics memory and/or moving across the bus. This may seem like a lot of work for a blit, but it has a lot to do with the way new cards are optimized for 3D (like it or not). Besides, putting some thought into how you are moving large chunks of memory around the system is never a bad idea. But I'll get off my soapbox.
注意纹理会被拉伸和缩短以适合矩形。你可以通过调整纹理的坐标来调整这些。例如,如果你把u=1.0那行改为u=0.5,那么只有一半的纹理被使用,而剩下的另一半不会被压缩。所以,如果你有一个640X480的图片,你想把它放到一个640X480的窗口中去,你应该将640X480大小的图片放到一个1024X512大小的纹理中去然后指定纹理坐标为0.625,0.9375。你可以使用纹理中剩余的部分来放置那些会被映射到其他的面板中去的子图片(通过相应的纹理坐标)。通常,你会想优化纹理被使用的方式,因为它们吃光了图片内存并且在总线中移动。这看上去很象blit中的大量的工作,但是很多都会被新式的为3D进行优化的显卡处理掉了。此外,多多考虑如何在系统中移动大块的内存决不是一个坏的想法。但是我还是开始我的演说吧。
Let's see where we are so far. At one level, we've written a lot of code to blit a simple bitmap. But, hopefully you can see some of the benefit and the opportunities for tweaking. For instance, the texture coordinates automatically scale the image to the area we've defined by the geometry. There are lots of things this does for us, but consider the following. If we had set up our ortho matrix to use a percentage based mapping, and we specified a panel as occupying the lower quarter of the screen (for a UI, let's say), and we specified a texture with the correct texture coordinates, then our UI would automagically be drawn correctly for any chosen window/screen size. Not exactly cold fusion, but it's one of many examples. Now that we have the texture working well, we have to get back to talking about transparency.
让我们来看看我们走了多远。首先,我们写了很多代码来blit一个简单的位图。但是,希望你能够看到一些好处和机会。例如,纹理坐标自动缩放图片以适应我们的几何定义。这为我们做了很多工作,但是考虑到后面。如果我们设置使用一个基于百分比映射的垂直矩阵,并且,我们指定一个占据屏幕底部四分之一位置的面板(让我们说它是UI吧),而且我们也用正确的纹理坐标来指定它的纹理,这样,我们的UI在任何选定的窗口/屏幕大小下都会被自动的正确绘制出来。(Not exactly cold fusion),但这只是很多例子中的一个。现在我们已经让纹理可以工作的很好了,我们回过头来谈论一下透明。
Transparency
透明
As I said before, one easy way of adding transparency is to specify a color key value in the call to D3DXCreateTextureFromFileEx. Another is to use an image that actually has an alpha channel. Either way, specify a texture with some transparency (either with an alpha channel or a color key value) and run the app. You should see no difference. This is because alpha blending is not enabled. To enable alpha blending, add these lines to PostInitialize:
象我以前所说的,加入透明的一个简单的方法就是在调用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);
The first line enables blending. The next two specify how the blending works. There are many possibilities, but this is the most basic type. The last line sets things up so that changing the alpha component of the vertex colors will fade the entire panel by scaling the texture values. For a more in depth discussion of the available settings, see the SDK. Once these lines are in place, you should see the proper transparency. Try changing the colors of the vertices to see how they affect the panel.
第一行允许了混合。下两行指定混合如何工作。这会有很多的可能性,不过这是最基本的类型。最后一行进行一些设置以致当改变顶点颜色的alpha成分时会缩放纹理值来减弱整个面板。关于可使用设置的更深层讨论,请参见SDK。一旦这些行被加入进来,你会看到正确的透明。试着改变顶点的颜色来看看它将如何影响面板。
Moving the Panel
移动面板
By now our panel has many of the visual properties we need, but it's still stuck in the center of our viewport. For a game, you probably want things to move. One obvious way is to relock the vertices and change their positions. DO NOT do this!! Locking is very expensive, involves moving data around, and is unnecessary. A better way is to specify a world transformation matrix to move the points. For many people, matrices may seem a bit scary, but there are a host of D3DX functions that make matrices very easy. For example, to move the panel, add the following code to the beginning of Render2D:
现在我们的面板已经有了很多我们需要的视觉属性,但它还只是粘在我们的视口中央。在游戏中,你可以想让一些东西移动起来。一个显而易见的方法是重新锁定顶点,然后改变它们的位置。千万不要这样做!锁定是很昂贵的操作,它包括数据的移动,并且这是不必要的。一个更好的方法是指定世界变换矩阵来移动这些点。对于很多人来说,矩阵看上去是有一点吓人的,但是在D3DX中有一大群函数让矩阵使用起来非常简单。例如,为了移动面板,在Render2D函数的开头加入下面的代码:
D3DXMATRIX Position;
D3DXMatrixTranslation(&Position, 50.0f, 0.0f, 0.0f);
g_pd3dDevice->SetTransform(D3DTS_WORLD, &Position);
This creates a matrix that moves the panel 50 pixels in the X direction and tells the device to apply that transform. This could be wrapped into a function like MoveTo(X, Y), which I won't actually give the code for. Earlier I said to remember the fact that the vertex code specified the vertices around the origin. Because we did that, translating (moving) the position moves the center of the panel. If you are more comfortable with moving the upper left or some other corner, change the way the vertices are specified. You could also create different coordinate systems by correcting the parameters sent to the MoveTo function. For example, our viewport currently goes from -100 to +100. If I wanted to use MoveTo as if it were going from 0 to 200, I could simply correct for it in my call to D3DXMatrixTranslation by subtracting 100 from the X position. There are many ways to quickly change this to meet your specific needs, but this will provide a good basis for experimentation.
这里创建了一个可以在X方向移动面板50个象素的矩阵,然后告诉设备应用这个移动。这可以被装封到一个象MoveTo(X,Y)的函数里去,不过我没有实际给出这样的代码。前面,我说过要记住顶点是相对于原点来定义的。因为我们是这样做的,所以,平移(移动)移动了面板的中心位置。如果你认为移动左上角或是其他的角会更加适合,请改变顶点定义的方式。你也可以通过传递正确的参数到MoveTo函数来创建不同的坐标系统。例如,我们的视口当前是从-100到100。如果我想将视口认为是从0到200那样来使用MoveTo函数,我可以简单的在我调用D3DXMatrixTranslation时从X坐标中减去100来进行更正。有很多方法可以很快的改变以使你能看到你所想要的效果,但是,作为实验这将提供一个好的基础。
Other Matrix Operations
其他的矩阵操作
There are many other matrix operations that will affect the panel. Perhaps the most interesting are scaling and rotation. There are D3DX functions for creating these matrices as well. I'll leave the experimenting up to you, but here are a few hints. Rotation about the Z-axis will create rotation on the screen. Rotating about X and Y will look like you are shrinking Y and X. Also, the way you apply multiple operations is through multiplication and then sending the resulting matrix to the device:
有很多其他的矩阵操作可以影响面板。最有趣的可能是缩放和旋转了。有一些D3DX函数可以很好的创建这些矩阵。我将把这些实验留给你来做,不过这儿有一些提示。关于Z轴的旋转将会在屏幕上旋转。而关于X和Y轴的旋转将看上去象是Y轴和X轴在收缩。另外,应用多个操作的方法是通过乘法,然后将结果矩阵送给设备:
D3DXMATRIX M = M1 * M2 * M3 * M4;
g_pd3dDevice->SetTransform(D3DTS_WORLD, &M);
But, remember that the product of matrix multiplication is dependent on the order of the operands. For instance, Rotation * Position will move the panel and then rotate it. Position * Rotation will cause an orbiting effect. If you string together several matrices and get unexpected results, look closely at the order.
不过,记住矩阵乘法的结果是依赖于操作数的顺序的。例如,Rotation*Position将移动面板然后旋转它。Position*Rotation将导致一个沿轨道而行的效果。如果你排列了几个矩阵在一起,但是得到了并不期待的结果,请仔细的看看排列的顺序。
As you become more comfortable, you may want to experiment with things like the texture matrix, which will allow you to transform the texture coordinates. You could also move the view matrix to affect your coordinate system. One thing to remember: locks are very costly, always look to things like matrices before locking your vertex buffers.
当你变得更加轻松时,你可以想去试试象纹理矩阵这样的东西,它将允许你移动纹理坐标。你也可以移动观察矩阵来影响你的坐标系统。记得一点:锁定是非常昂贵的,在你锁定你的顶点缓冲之前,总是先看看象矩阵这样的东西。
Wrapping Up
装封
Looking at all the code listed here, this is really a long, drawn out way to do a blit, but the nice thing is that most of this can be wrapped into tidy functions or classes that make all this a one time cost for long term benefit. Please remember that this is presented in a very bare bones, unoptimized way. There are many ways to package this to get maximum benefit. This method should be the optimal way to create 2D applications on current and coming hardware and also will pay off in terms of the effects that you can implement very easily on top of it. This approach will also help you blend 2D with 3D because, aside from some matrices, the two are the same. The code was easily adapted from 2D work that I did in OpenGL, so you could even write abstract wrappers around the approach to support both APIs. My hope is that this will get people started using DX8 for 2D work. Perhaps in future articles I will talk about more tricks and effects.
看看这儿列出的所有的代码,为了进行blit我们走了很长的一段路,但是好事是很多都可以被装封到一些小的函数或是类中去,这样我们就可以一劳永逸了。请注意,这儿是使用一种非常梗概的而且没有优化的方法来表示的。有很多方法可以将这些包装起来以获得最大的收益。这可能是在当前的和以后的硬件上创建2D应用程序的最佳方法,而且你也可以获得在硬件上可以很简单的实现那些效果的好处。这种方法也可以帮助你在3D中混合进2D元素,因为在矩阵面前,它们是一样的。这些代码也可以简单的适合在OpenGL中进行2D工作,所以你甚至可以写一个抽象装封来支持两种API。我的希望是这可以让人们使用DX8来做2D工作。可能在以后的文章中我会讨论更多的技巧和效果。
背景
I have been reading alot of questions lately related to DirectX 8 and the exclusion of DirectDraw from the new API. Many people have fallen back to DX7. I can understand people using DX7 if they have alot of experience with that API, but many of the questions seem to be coming from people who are just learning DX, and they are stuck learning an old API. People have argued that many people don't have 3D hardware, and therefore D3D would be a bad alternative for DirectDraw. I don't believe this is true - 2D rendering in D3D requires very little vertex manipulation, and everything else boils down to fillrate. In short, 2D rendering in D3D on 2D hardware should have pretty much the same performance as DirectDraw, assuming decent fillrate. The advantage is that the programmer can learn the newest API, and performance on newer hardware should be very good. This article will present a framework for 2D rendering in DX8 to ease the transition from DirectDraw to Direct3D. In each section, you may see things that you don't like ("I'm a 2D programmer, I don't care about vertices!"). Rest assured, if you implement this simple framework once, you'll never think about these things again.
最近,我看到很多关于DirectX8在最新的API中摒弃DirectDraw的问题。很多人回到了以前的DX7.1中。我可以理解那些在DX7.1中有很多开发经验的人为什么这样做,但是有很多问题却是来自于那些刚学DX,还没有学过以前的API的初学者。人们争辩说很多人没有3D硬件,因此D3D对于DirectDraw是个错误的选择。我不相信那是真的,在D3D中进行2D渲染只需要做一点顶点操作,而其他的事情都可以被精简来提高填充率。简言之,在D3D中使用2D硬件进行2D渲染,可以做到和DirectDraw一样好的性能,有很好的填充率。而优点是,程序员可以学习最新的API,并且在更新的硬件中获得更好的性能。这篇文章将给出一个在DX8中进行2D渲染的框架,以便于从DirectDraw到Direct3D的转变。在每一节里,你会看到一些你不喜欢的东西(“我是一个2D程序员,我不用关心顶点!”)。但是,请放心,只要你将这个简单的框架实现一次,你就再也不会考虑那些了。
Getting Started
开始
Assuming you have the DX8 SDK, there are a couple tutorials that present how to create a D3D device and set up a render loop, so I don't want to spend alot of time on that. For the purposes of this article, I'll talk about the tutorial found in [DX8SDK]samplesMultimediaDirect3DTutorialsTut01_CreateDevice, although you can add it to anything. To that sample, I'll add the following functions:
假设你已经有DX8 SDK,那儿有一组指南讲述了如何创建一个D3D设备,如何放置渲染循环,因此我不想再在这上面花费时间。按照这篇文章的意图,我将谈论位于[DX8SDK]samplesMultimediaDirect3DTutorialsTut01_CreateDevice目录里的指南,你可能将它放到了任何地方。在那个例子中,我将加入以下函数:
void PostInitialize(float WindowWidth, float WindowHeight)
- this function is called by the app after everything else is set up. You've created your device and initialized everything. If you're following along with the Tutorial code, WinMain looks like this:
- 其他所有事情都设置完之后,这个函数被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.
ShowWindow( hWnd, SW_SHOWDEFAULT );
...
void Render2D()
- This function is called each time you want to render your scene. Again, the Render function of the Tutorial now looks like this:
- 这个函数当你渲染你的场景时会被调用,指南里的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...
// End the scene
g_pd3dDevice->EndScene();
// Present the backbuffer contents to the display
g_pd3dDevice->Present( NULL, NULL, NULL, NULL );
}
OK, that's our shell of an application. Now for the good stuff...
好了,这就是我们的程序外壳,现在来准备好的内容填充它吧...
Setting Up for 2D drawing in D3D
为在D3D中进行2D绘制进行设置
NOTE: This is where we start talking about some of the nasty math involved with D3D. Don't be alarmed - if you want to, you can choose to ignore most of the details?/I> Most Direct3D drawing is controlled by three matrices: the projection matrix, the world matrix, and the view matrix. The first one we'll talk about is the projection matrix. You can think of the projection matrix as defining the properties of the lens of your camera. In 3D applications, it defines things like perspective, etc. But, we don't want perspective - we are talking about 2D!! So, we can talk about orthogonal projections. To make a long story very short, this allows us to draw in 2D without all the added properties of 3D drawing. To create an orthogonal matrix, we need to call D3DXMatrixOrthoLH and this will create a matrix for us. The other matrices (view and world) define the position of the camera and the position of the world (or an object in the world). For our 2D drawing, we don't need to move the camera, and we don't want to move the world for now, so we'll use an identity matrix, which basically sets the camera and world in a default position. We can create identity matrices with D3DXMatrixIdentity. To use the D3DX functions, we need to add:
注意:从这儿开始我们就要谈论一些和D3D相关的令人讨厌的数学知识了。不要被吓倒了---如果你愿意,你可以选择乎略大多数细节。大多数Direct3D绘制都受三个矩阵控制:投影矩阵,世界矩阵,观察矩阵。我们将谈论的第一个矩阵是投影矩阵。你可以认为投影矩阵定义了你的摄像机的镜头属性。在3D应用里,它定义了象透视方法,等等的东西。但是我们用不着透视---我们正在谈论2D!!所以我们只谈论正交投影。简短得说,就是让我们进行2D绘制而不用考虑那些附加在3D绘制中的属性。为了创建一个正交投影矩阵,我们需要调用D3DXMatrixOrthoLH函数,它将为我们创建一个矩阵。其他的矩阵(观察矩阵和世界矩阵)定义了摄像机的位置和世界(或一个在世界里的对象)的位置。为了我们的2D绘制,我们不需要移动摄像机,也不用想移动世界,所以我们将使用一个单位矩阵,将摄像机和世界放置在缺省的位置。我们可以用D3DXMatrixIdentity函数来创建单位矩阵。我们需要加入下面的头文件,以使用D3DX函数。
#include <d3dx8.h>
and add d3dx8dt.lib to the list of linked libraries. Once that was set up, the PostInitialize function now looks like this:
并且加入d3dx8dt.lib到连接库列表里。当那些设置了之后,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);
}
We are now set up for 2D drawing, now we need something to draw. The way things are set up, our drawing area goes from -WindowWidth/2 to WindowWidth/2 and -WindowHeight/2 to WindowHeight/2. One thing to note, in this code, the width and the height are being specified in pixels. This allows us to think about everything in terms of pixels, but we could have set the width and height to say 1.0 and that would have allowed us to specify sizes, etc. in terms of percentages of the screen space, which would be nice for supporting multiple resolutions easily. Changing the matrix allows for all sorts of neat things, but for simplicity, we'll talk about pixels for now?
我们现在正在为2D绘制进行设置,我们需要绘制些东西。这样设置了之后,我们的绘制区域就是从-WindowWidth/2 到 WindowWidth/2和从 -WindowHeight/2 到 WindowHeight/2。要注意的一件事是,在代码里,宽度和高度都是用象素为单位指定的。这允许我们用象素来考虑所有的事情,但我们也能设置宽度和高度为1.0,然后允许我们用屏幕空间的百分比来指定大小,这样就很容易的支持了多分辨率的情况。改变矩阵就能够支持各种各样的巧妙的事情,但为了简单起见,我们现在将谈论象素。
Setting Up a 2D "Panel"
设置一个2D“面板”
When I draw in 2D, I have a class called CDX8Panel that encapsulates everything I need to draw a 2D rectangle. For simplicity, and it avoid a C++ explanation, I have pulled out the code here. However, as we build up our code to draw a panel, you'll probably see the value of such a class or higher level API if you don't use C++. Also, we are about to recreate much that goes on in the ID3DXSprite interface. I'm explaining the basics here to show the way things work, but you may want to use the sprite interface if it suits your needs.
当我进行2D绘制时,我有一个叫CDX8Panel的类,它装封了所有我绘制2D矩形所需要的东西。简单起见,它消除了C++说明,我已经将代码拿了出来了。无论如何,当我们建造我们的一个绘制面板的代码时,你将可能看到一个类的价值,或者如果你不使用C++时一个更高层的API的价值。同样,依靠ID3DXSprite接口,我们也可得以更加悠闲。我将在这儿解释最基本的东西,以展显事情工作的方法,但是如果Sprite接口适合你的需要,你也可以使用它。
My definition of a panel is simply a 2D textured rectangle that we are going to draw on the screen. Drawing a panel will be extremely similar to a 2D blit. Experienced 2D programmers may think that this is a lot of work for a blit, but that work pays off with the amount of special effects that it enables. First, we have to think about the geometry of our rectangle. This involves thinking about vertices. If you have 3D hardware, the hardware will process these vertices extremely quickly. If you have 2D hardware, we are talking about so few vertices that they will be processed very quickly by the CPU. First, let's define our vertex format. Place the following code near the #includes:
我的面板定义是一个简单的2D纹理矩形,我们将会把它绘制到屏幕上。绘制一个面板非常类似于2D的blit操作。有经验的2D程序员可能会想一个blit操作会有大量的工作,但是这些工作完成了很多允许的特效。首先,我们不得不考虑我们的矩形的几何结构。这样就包括了关于顶点的思想。如果你有3D硬件,硬件将非常快地处理这些顶点。如果你只有2D硬件,我们所谈论的如此少的顶点也将很快的被CPU处理完成。首先,让我们定义我们的顶点格式。将以下的代码放置到靠近#include:的地方
struct PANELVERTEX
{
FLOAT x, y, z;
DWORD color;
FLOAT u, v;
};
#define D3DFVF_PANELVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1)
This structure and Flexible Vertex Format (FVF) specify that we are talking about a vertex that has a position, a color, and a set of texture coordinates.
这个结构和灵活的顶点格式(FVF)定义了我们所谈论的包含位置,颜色和一组纹理坐标的顶点。
Now we need a vertex buffer. Add the following line of code to the list of globals. Again, for simplicity, I'm making it global - this is not a demonstration of good coding practice.
现在我们需要一个顶点缓冲。加入下面代码行到全局列表中。又是为了简单,我让它成为全局的---这可不是一个好的编码习惯的示例。
LPDIRECT3DVERTEXBUFFER8 g_pVertices = NULL;
Now, add the following lines of code to the PostInitialize function (explanation to follow):
现在,加入下面的代码行到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();
This is actually much simpler than it may look. First, I made up a size for the panel just so we'd have something to work with. Next, I asked the device to create a vertex buffer that contained enough memory for four vertices of my format. Then I locked the buffer so I could set the values. One thing to note, locking buffers is very expensive, so I'm only going to do it once. We can manipulate the vertices without locking, but we'll discuss that later. For this example I have set the four points centered on the (0, 0). Keep this in the back of your mind; it will have ramifications later. Also, I set the texture coordinates. The SDK explains these pretty well, so I won't get into that. The short story is that we are set up to draw the entire texture. So, now we have a rectangle set up. The next step is to draw it?
这实际上比看起来还要简单。首先,我构造了面板的大小,我们有一些工作会用到它们。接下来,我请求设备创建一个包含用我的格式定义的四个顶点的足够大内存的顶点缓冲。然后我锁定缓冲以使我能够设置顶点的值。值得注意的一点是,锁定缓冲是一很昂贵的,所以,我将只这样做一次。我们可以操作这些顶点而不用锁定,不过我们将在以后讨论。在这个例子中,我设置了四个对(0,0)居中的点。记住这一点,以后会有衍生而出的讨论。另外,我设置了纹理坐标。SDK已经很好的说明了,所以我没有进行讨论。简短的说就是我们进行了设置,来绘制整个纹理。这样,现在我们已经设置好了矩形。下一步就是绘出它?
Drawing the Panel
绘制面板
Drawing the rectangle is pretty easy. Add the following lines of code to your Render2D function:
绘制矩形是很简单的。增加下面的代码行到你的Render2D函数:
g_pd3dDevice->SetVertexShader(D3DFVF_PANELVERTEX);
g_pd3dDevice->SetStreamSource(0, g_pVertices, sizeof(PANELVERTEX));
g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLEFAN, 0, 2);
These lines tell the device how the vertices are formatted, which vertices to use, and how to use them. I have chosen to draw this as a triangle fan, because it's more compact than drawing two triangles. Note that since we are not dealing with other vertex formats or other vertex buffers, we could have moved the first two lines to our PostInitialize function. I put them here to stress that you have to tell the device what it's dealing with. If you don't, it may assume that the vertices are a different format and cause a crash. At this point, you can compile and run the code. If everything is correct, you should see a black rectangle on a blue background. This isn't quite right because we set the vertex colors to white. The problem is that the device has lighting enabled, which we don't need. Turn lighting off by adding this line to the PostInitialize function:
这些行告诉设备顶点如何被格式化,使用哪些顶点,以及如何使用它们。我选择将这些当作三角形扇进行绘制,因为这样比绘制两个三角形更紧凑。注意,因为我们没有和别的顶点格式或顶点缓冲的处理,我们可以移动第一行到我们的PostInitialize函数里去。我将它们放到这儿是为了强调你不得不告诉设备它将要处理的是什么。如果你不这样做,它将假设顶点是不同格式的,并且导致崩溃。这时,你就可以编译运行代码了,如果一切正常,你会看到在蓝色的背景里有一个黑色的矩形。这还不是很正确的,因为我们设置顶点的颜色是白色的。这个问题是设备允许了光照,这是我们不需要的。通过在PostInitialize函数里加入这行来关掉光照:
g_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
Now, recompile and the device will use the vertex colors. If you'd like, you can change the vertex colors and see the effect. So far, so good, but a game that features a white rectangle is visually boring, and we haven't gotten to the idea of blitting a bitmap yet. So, we have to add a texture.
现在,重新编译,设备将会使用顶点的颜色了。如果你喜欢,你可以改变顶点的颜色看看效果。到目前为止,一切顺利,但是一个显示一个白色矩形的游戏看上去是很令人厌烦的,我们还没有触及到blit一个位图。所以,我们不得不加入纹理。
Texturing the Panel
为面板粘贴纹理
A texture is basically a bitmap that can be loaded from a file or generated from data. For simplicity, we'll just use files. Add the following to your global variables:
纹理是一个能够从文件装入或者通过数据生成的基本的位图。为简单起见,我们只使用文件。将下面的变量加入到你的全局变量中:
LPDIRECT3DTXTURE8 g_pTexture = NULL;
This is the texture object we'll be using. To load a texture from a file, add this line to PostInitialize:
这便是我们将要使用的纹理对象。加入这行代码到PostInitialize函数以从文件装入纹理。
D3DXCreateTextureFromFileEx(g_pd3dDevice, [Some Image File], 0, 0, 0, 0,
D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, D3DX_DEFAULT,
D3DX_DEFAULT , 0, NULL, NULL, &g_pTexture);
Replace [Some Image File] with a file of your choice. The D3DX function can load many standard formats. The pixel format we're using has an alpha channel, so we could load a format that has an alpha channel such as .dds. Also, I'm ignoring the ColorKey parameter, but you could specify a color key for transparency. I'll get back to transparency in a little bit. For now, we have a texture and we've loaded an image. Now we have to tell the device to use it. Add the following line to the beginning of Render2D:
你可以用你选择的文件名替换[Some Image File]。D3DX函数可以装入很多标准格式的文件。我们所使用的象素格式是有alpha通道的,所以我们装入带alpha通道的格式文件,象.dss文件。另外,我也乎略了ColorKey参数,但是你也可以指定一个ColorKey以进行透明。我会回头来讨论一点关于透明的知识。现在,我们有了一个纹理并且已经装入了一个图片。然后我们要告诉设备使用它。加入下面的代码行到Render2D函数的开头:
g_pd3dDevice->SetTexture(0, g_pTexture);
This tells the device to render the triangles using the texture. One important thing to remember here is that I am not adding error checking for simplicity. You should probably add error checking to make sure the texture is actually loaded before attempting to use it. One possible error is that for a lot of hardware, the textures must have dimensions that are powers of 2 such as 64x64, 128x512, etc. This constraint is no longer true on the latest nVidia hardware, but to be safe, use powers of 2. This limitation bothers a lot of people, so I'll tell you how to work around it in a moment. For now, compile and run and you should see your image mapped onto the rectangle.
这告诉了设备用纹理来渲染三角形。这儿要特别记住的事情是考虑到简单我没有加入错误检查。你应该进行正确的错误检查,以确定在纹理被使用之前已经被实际的装入了。一个可能的错误是,在很多硬件中,纹理的大小必须是2的幂,如:64X64,128X512,等等。对于最新的nVidia硬件,这个约束不再是正确的了,但是安全起见,请使用2的幂。这个限制让很多人感到烦心,所以一会儿我会告诉你如何绕过这个限制。现在,编译运行,你可以看到你的图片已经映射到了三角形上了。
Texture Coordinates
纹理坐标
Note that it is stretched/squashed to fit the rectangle. You can adjust that by adjusting the texture coordinates. For example, if you change the lines where u = 1.0 to u = 0.5, then only half of the texture is used and the remaining part will not be squashed. So, if you had a 640x480 image that you wanted to place on a 640x480 window, you could place the 640x480 image in a 1024x512 texture and specify 0.625, 0.9375 for the texture coordinates. You could use the remaining parts of the texture to hold other sub images that are mapped to other panels (through the appropriate texture coordinates). In general, you want to optimize the way textures are used because they are eating up graphics memory and/or moving across the bus. This may seem like a lot of work for a blit, but it has a lot to do with the way new cards are optimized for 3D (like it or not). Besides, putting some thought into how you are moving large chunks of memory around the system is never a bad idea. But I'll get off my soapbox.
注意纹理会被拉伸和缩短以适合矩形。你可以通过调整纹理的坐标来调整这些。例如,如果你把u=1.0那行改为u=0.5,那么只有一半的纹理被使用,而剩下的另一半不会被压缩。所以,如果你有一个640X480的图片,你想把它放到一个640X480的窗口中去,你应该将640X480大小的图片放到一个1024X512大小的纹理中去然后指定纹理坐标为0.625,0.9375。你可以使用纹理中剩余的部分来放置那些会被映射到其他的面板中去的子图片(通过相应的纹理坐标)。通常,你会想优化纹理被使用的方式,因为它们吃光了图片内存并且在总线中移动。这看上去很象blit中的大量的工作,但是很多都会被新式的为3D进行优化的显卡处理掉了。此外,多多考虑如何在系统中移动大块的内存决不是一个坏的想法。但是我还是开始我的演说吧。
Let's see where we are so far. At one level, we've written a lot of code to blit a simple bitmap. But, hopefully you can see some of the benefit and the opportunities for tweaking. For instance, the texture coordinates automatically scale the image to the area we've defined by the geometry. There are lots of things this does for us, but consider the following. If we had set up our ortho matrix to use a percentage based mapping, and we specified a panel as occupying the lower quarter of the screen (for a UI, let's say), and we specified a texture with the correct texture coordinates, then our UI would automagically be drawn correctly for any chosen window/screen size. Not exactly cold fusion, but it's one of many examples. Now that we have the texture working well, we have to get back to talking about transparency.
让我们来看看我们走了多远。首先,我们写了很多代码来blit一个简单的位图。但是,希望你能够看到一些好处和机会。例如,纹理坐标自动缩放图片以适应我们的几何定义。这为我们做了很多工作,但是考虑到后面。如果我们设置使用一个基于百分比映射的垂直矩阵,并且,我们指定一个占据屏幕底部四分之一位置的面板(让我们说它是UI吧),而且我们也用正确的纹理坐标来指定它的纹理,这样,我们的UI在任何选定的窗口/屏幕大小下都会被自动的正确绘制出来。(Not exactly cold fusion),但这只是很多例子中的一个。现在我们已经让纹理可以工作的很好了,我们回过头来谈论一下透明。
Transparency
透明
As I said before, one easy way of adding transparency is to specify a color key value in the call to D3DXCreateTextureFromFileEx. Another is to use an image that actually has an alpha channel. Either way, specify a texture with some transparency (either with an alpha channel or a color key value) and run the app. You should see no difference. This is because alpha blending is not enabled. To enable alpha blending, add these lines to PostInitialize:
象我以前所说的,加入透明的一个简单的方法就是在调用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);
The first line enables blending. The next two specify how the blending works. There are many possibilities, but this is the most basic type. The last line sets things up so that changing the alpha component of the vertex colors will fade the entire panel by scaling the texture values. For a more in depth discussion of the available settings, see the SDK. Once these lines are in place, you should see the proper transparency. Try changing the colors of the vertices to see how they affect the panel.
第一行允许了混合。下两行指定混合如何工作。这会有很多的可能性,不过这是最基本的类型。最后一行进行一些设置以致当改变顶点颜色的alpha成分时会缩放纹理值来减弱整个面板。关于可使用设置的更深层讨论,请参见SDK。一旦这些行被加入进来,你会看到正确的透明。试着改变顶点的颜色来看看它将如何影响面板。
Moving the Panel
移动面板
By now our panel has many of the visual properties we need, but it's still stuck in the center of our viewport. For a game, you probably want things to move. One obvious way is to relock the vertices and change their positions. DO NOT do this!! Locking is very expensive, involves moving data around, and is unnecessary. A better way is to specify a world transformation matrix to move the points. For many people, matrices may seem a bit scary, but there are a host of D3DX functions that make matrices very easy. For example, to move the panel, add the following code to the beginning of Render2D:
现在我们的面板已经有了很多我们需要的视觉属性,但它还只是粘在我们的视口中央。在游戏中,你可以想让一些东西移动起来。一个显而易见的方法是重新锁定顶点,然后改变它们的位置。千万不要这样做!锁定是很昂贵的操作,它包括数据的移动,并且这是不必要的。一个更好的方法是指定世界变换矩阵来移动这些点。对于很多人来说,矩阵看上去是有一点吓人的,但是在D3DX中有一大群函数让矩阵使用起来非常简单。例如,为了移动面板,在Render2D函数的开头加入下面的代码:
D3DXMATRIX Position;
D3DXMatrixTranslation(&Position, 50.0f, 0.0f, 0.0f);
g_pd3dDevice->SetTransform(D3DTS_WORLD, &Position);
This creates a matrix that moves the panel 50 pixels in the X direction and tells the device to apply that transform. This could be wrapped into a function like MoveTo(X, Y), which I won't actually give the code for. Earlier I said to remember the fact that the vertex code specified the vertices around the origin. Because we did that, translating (moving) the position moves the center of the panel. If you are more comfortable with moving the upper left or some other corner, change the way the vertices are specified. You could also create different coordinate systems by correcting the parameters sent to the MoveTo function. For example, our viewport currently goes from -100 to +100. If I wanted to use MoveTo as if it were going from 0 to 200, I could simply correct for it in my call to D3DXMatrixTranslation by subtracting 100 from the X position. There are many ways to quickly change this to meet your specific needs, but this will provide a good basis for experimentation.
这里创建了一个可以在X方向移动面板50个象素的矩阵,然后告诉设备应用这个移动。这可以被装封到一个象MoveTo(X,Y)的函数里去,不过我没有实际给出这样的代码。前面,我说过要记住顶点是相对于原点来定义的。因为我们是这样做的,所以,平移(移动)移动了面板的中心位置。如果你认为移动左上角或是其他的角会更加适合,请改变顶点定义的方式。你也可以通过传递正确的参数到MoveTo函数来创建不同的坐标系统。例如,我们的视口当前是从-100到100。如果我想将视口认为是从0到200那样来使用MoveTo函数,我可以简单的在我调用D3DXMatrixTranslation时从X坐标中减去100来进行更正。有很多方法可以很快的改变以使你能看到你所想要的效果,但是,作为实验这将提供一个好的基础。
Other Matrix Operations
其他的矩阵操作
There are many other matrix operations that will affect the panel. Perhaps the most interesting are scaling and rotation. There are D3DX functions for creating these matrices as well. I'll leave the experimenting up to you, but here are a few hints. Rotation about the Z-axis will create rotation on the screen. Rotating about X and Y will look like you are shrinking Y and X. Also, the way you apply multiple operations is through multiplication and then sending the resulting matrix to the device:
有很多其他的矩阵操作可以影响面板。最有趣的可能是缩放和旋转了。有一些D3DX函数可以很好的创建这些矩阵。我将把这些实验留给你来做,不过这儿有一些提示。关于Z轴的旋转将会在屏幕上旋转。而关于X和Y轴的旋转将看上去象是Y轴和X轴在收缩。另外,应用多个操作的方法是通过乘法,然后将结果矩阵送给设备:
D3DXMATRIX M = M1 * M2 * M3 * M4;
g_pd3dDevice->SetTransform(D3DTS_WORLD, &M);
But, remember that the product of matrix multiplication is dependent on the order of the operands. For instance, Rotation * Position will move the panel and then rotate it. Position * Rotation will cause an orbiting effect. If you string together several matrices and get unexpected results, look closely at the order.
不过,记住矩阵乘法的结果是依赖于操作数的顺序的。例如,Rotation*Position将移动面板然后旋转它。Position*Rotation将导致一个沿轨道而行的效果。如果你排列了几个矩阵在一起,但是得到了并不期待的结果,请仔细的看看排列的顺序。
As you become more comfortable, you may want to experiment with things like the texture matrix, which will allow you to transform the texture coordinates. You could also move the view matrix to affect your coordinate system. One thing to remember: locks are very costly, always look to things like matrices before locking your vertex buffers.
当你变得更加轻松时,你可以想去试试象纹理矩阵这样的东西,它将允许你移动纹理坐标。你也可以移动观察矩阵来影响你的坐标系统。记得一点:锁定是非常昂贵的,在你锁定你的顶点缓冲之前,总是先看看象矩阵这样的东西。
Wrapping Up
装封
Looking at all the code listed here, this is really a long, drawn out way to do a blit, but the nice thing is that most of this can be wrapped into tidy functions or classes that make all this a one time cost for long term benefit. Please remember that this is presented in a very bare bones, unoptimized way. There are many ways to package this to get maximum benefit. This method should be the optimal way to create 2D applications on current and coming hardware and also will pay off in terms of the effects that you can implement very easily on top of it. This approach will also help you blend 2D with 3D because, aside from some matrices, the two are the same. The code was easily adapted from 2D work that I did in OpenGL, so you could even write abstract wrappers around the approach to support both APIs. My hope is that this will get people started using DX8 for 2D work. Perhaps in future articles I will talk about more tricks and effects.
看看这儿列出的所有的代码,为了进行blit我们走了很长的一段路,但是好事是很多都可以被装封到一些小的函数或是类中去,这样我们就可以一劳永逸了。请注意,这儿是使用一种非常梗概的而且没有优化的方法来表示的。有很多方法可以将这些包装起来以获得最大的收益。这可能是在当前的和以后的硬件上创建2D应用程序的最佳方法,而且你也可以获得在硬件上可以很简单的实现那些效果的好处。这种方法也可以帮助你在3D中混合进2D元素,因为在矩阵面前,它们是一样的。这些代码也可以简单的适合在OpenGL中进行2D工作,所以你甚至可以写一个抽象装封来支持两种API。我的希望是这可以让人们使用DX8来做2D工作。可能在以后的文章中我会讨论更多的技巧和效果。
posted on 2011-11-28 10:33 Hibernate4 阅读(188) 评论(0) 编辑 收藏 举报