Implementing Immediate Mode in Direct3D
IntroductionWhen I first began building a DirectX graphics engine, I ran into an almost ironic problem. I couldn't find out how to implement the features I wanted to write in Direct3D. It was particularly bad with certain features such as BSP rendering; all the examples I found were in OpenGL. I've ported OpenGL code to Direct3D before, but problems arose when people decided to use the glVertex* family of commands. There just isn't anything like them in Direct3D. It got even worse when I needed to write some sort of culling or polygon reduction algorithm and the easiest way to do it was vertex by vertex, face by face. I never found any examples that had used OpenGL's vertex array commands, which have Direct3D counterparts. Recently, I came up with a rather clever idea. I figured that it should be possible to write the OpenGL vertex commands in Direct3D so that the vertices could be specified one by one, but they would still be sent down the graphics pipeline in a vertex buffer. The overhead incurred would be minimal, and it would be extremely useful. Porting code from OpenGL to Direct3D would be simplified greatly; it would be easier to write multi-API code. The ConceptIt's important to realize that I am not going to develop a Direct3D wrapper using OpenGL commands. I am essentially implementing a new set of commands for the Direct3D API. We will follow the OpenGL naming conventions for our new commands and approximately the same argument format. In this particular article, I will show how to implement the following commands: d3dBegin( D3DPRIMITIVETYPE Type, IDirect3DDevice8* pDevice ); d3dEnd(); d3dVertex3f( float x, float y, float z ); d3dNormal3f( float x, float y, float z ); d3dColor4f( float a, float r, float g, float b ); d3dTexCoord2f( float tu, float tv ); I decided that these were the core set of commands necessary for the implementation. Once you see the functions, it will be very easy to add more versions (e.g. d3dColor4ub) of these functions, as well as whole new functions. The next step, though, is to determine exactly what each of these functions should do. The InterfaceI considered whether or not to write these as regular C functions, or to build a C++ class out of them. While a C++ class would have some advantages, primarily that it would allow multiple sets of vertices, it's not really all that useful. In addition, I'm trying to follow OpenGL's model as closely as possible, so we'll write them in regular C style. (NOTE: This code is C++, not C! We'll be using C++ to avoid the painfully long calls involved with coding DirectX in C.) In order to write these functions in C style, we will need a number of static variables in the CPP file along with the function implementations. The FunctionsFor the definitions of these functions, I've stayed as close as reasonably possible to the definitions for their OpenGL counterparts. There are only two changes. First of all, we need to get the device pointer somewhere; I decided to get it in the call to d3dBegin(). Second, OpenGL usually takes its colors in RGBA, but Direct3D likes them in ARGB format. I changed d3dColor4f() to use ARGB color order. It's not so important for that particular call, but it will become important if you ever write d3dColor4fv, which takes a pointer to the color locations. d3dBegin( D3DPRIMITIVETYPE Type, IDirect3DDevice8* pDevice );
d3dEnd();
d3dVertex( float x, float y, float z );
d3dNormal3f( float x, float y, float z );
d3dColor4f( float a, float r, float g, float b );
d3dTexCoord2f( float tu, float tv );
Vertex Structures and ListsBefore going on, we need to make some structural decisions. If you look at the description for d3dVertex3f again, you'll notice that it adds a vertex to the vertex list. Before we can decide how to create a vertex list, we need to determine what exactly a vertex is. We'll use a standard D3D vertex structure, since there is actually no other way to do it. Looking back at the function calls, we see that our vertex structure must support a position, normals, diffuse color, and texture coordinates. That gives us the following structure: struct ListVertex { float x, y, z; float nx, ny, nz; D3DCOLOR Diffuse; float tu, tv; } And its corresponding FVF definition: #define FVF_LISTVERTEX D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE | D3DFVF_TEX0 Now that we've got our vertex structure, we can decide how to make a list for it. There are two major ways to structure the list: as an array or as a singly linked non-cyclic list. The Array MethodRemember that we don't know how many vertices we're going to get ahead of time. This means that our array will have to be dynamic, and will also have to be resized occasionally. We get the following performance from the array:
I've assumed memcpy() to be O(1) for the above times (don't worry if you don't understand these notations; it's an analysis of the time required for the operations). I can assume this because on modern systems, moving data around really doesn't take too much time. I'm guessing that at most, this code will need to shift ~1MB of data. Resizing the list involves creating a larger memory space, copying the old data to the new list, and changing the array pointer to the new memory location. That will have to be done approximately every few thousand vertices. Copying the list involves a single memcpy(), since the vertices are all in a contiguous memory block. Adding a vertex (assuming there is still space in the array) simply involves changing the data for the vertex involved. The main drawback here is that depending on how much the array is expanded at each resize, we could waste quite a lot of memory. The Linked List MethodThe advantage to the linked list is that we do not need to resize the list at any point. It's not difficult to implement, and not too hard to traverse.
The serious drawback here is that moving the vertices into the buffer is O(n), not O(1)! Because we cannot guarantee that any vertices are next to each other in memory, we have to memcpy() the vertices one at a time, not all at once. This is a serious performance hit! We'll use the array method. The O(n) time for copying the linked list is too high, and array resizing does not take so much time that it justifies using the linked list(the time for the array resize is O(n/ExpandAmount), whereas the linked list copy time is just O(n) ). A single static pointer represents our list: static ListVertex *pVertList = NULL; At this point we need to make a decision about the size to start the list at and how much to expand it at each resize. We could hard code it in, but that wouldn't be good coding practice. Instead, we'll put a #define statement in the header. Also, we'll encase it in an #ifndef...#endif block. This way, you can define it as something else before you include the header and easily change the value. To simplify things, there will be just one #define which will be used both as the start array size and as the amount to expand at each time: #ifndef MIN_VERTEX_LIST_SIZE #define MIN_VERTEX_LIST_SIZE 2048 #endif We'll access this define during d3dBegin() and d3dVertex3f(). The Static VariablesWe've already seen one variable: the vertex list. If you go back through the function descriptions, you'll notice that I mention several other variables, but that I have not actually specified what they are. I'll do that here: static IDirect3DDevice8 *pD3DDevice = NULL;
static BOOL bRenderBegun = FALSE;
static float CurNX = 0.0, CurNY = 0.0, CurNZ = 0.0;
static D3DCOLOR CurDiffuse = 0xff000000;
static float CurTU = 0.0, CurTV = 0.0;
static ListVertex *pVertList = NULL;
static long MaxArraySize;
static long NumVerts = 0;
static D3DPRIMITIVETYPE PrimitiveType;
Implementing the FunctionsThe most complex of the functions will be d3dVertex3f() and d3dEnd(). We'll go through those two last. Let's start off with d3dBegin(): void d3dBegin( D3DPRIMITIVETYPE Type, IDirect3DDevice8* pDevice ) { //Initialize all our vars if( pDevice == NULL ) return; pD3DDevice = pDevice; pD3DDevice->AddRef(); PrimitiveType = Type; pVertList = new ListVertex[MIN_VERTEX_LIST_SIZE]; NumVerts = 0; MaxArraySize = MIN_VERTEX_LIST_SIZE; bRenderBegun = TRUE; } Most of the lines are simply initializing the variables to their default values. Notice that the color, texture coordinates, and normals are not initialized; this is so that you can make calls to these functions before calling d3dBegin(). We create a new list with the starting size as well. I haven't checked for a possible memory out error for brevity, but you can add it if you wish. Another detail is that I haven't zeroed the newly allocated memory. This is not necessary because the memory used will not contain junk data by the time we actually read it. The memory that is not set to a meaningful value will never be used either. We then AddRef() on the device pointer to make sure that we don't lose the device between now and d3dEnd(). Lastly, we set bRenderBegun to TRUE so that the other functions know that we're ready. Next, we'll look at d3dNormal3f(), d3dColor4f(), and d3dTexCoord2f(): void d3dColor4f( float a, float r, float g, float b ) { CurDiffuse = D3DCOLOR_COLORVALUE( r, g, b, a ); } void d3dNormal3f( float nx, float ny, float nz ) { CurNX = nx; CurNY = ny; CurNZ = nz; } void d3dTexCoord2f( float tu, float tv ) { CurTU = tu; CurTV = tv; } The reason I'm putting up all three functions at the same time is because they are all basically the same. We don't modify any vertices because these functions are not supposed to modify the vertices. Remember that the settings have to be applied to all of the vertex calls that follow. The values don't change unless these functions are called again. Instead of modifying the current vertex, we simply store these values. They'll be applied to a vertex in d3dVertex3f(), which we'll look at now: void d3dVertex3f( float x, float y, float z ) { //If we haven't begun, then there's no array to add to! :o if( !bRenderBegun ) return; //If we're out of space in the array, add more space if( NumVerts == MaxArraySize ) { //Expand the array by MIN_VERTEX_LIST_SIZE MaxArraySize += MIN_VERTEX_LIST_SIZE; ListVertex* pTemp = new ListVertex[MaxArraySize]; memcpy( pTemp, pVertList, NumVerts * sizeof(ListVertex) ); delete[] pVertList; pVertList = pTemp; } //Alias the current vertex to spare a bit of typing ListVertex* CurVert = &(pVertList[NumVerts]); CurVert->x = x; CurVert->y = y; CurVert->z = z; CurVert->nx = CurNX; CurVert->ny = CurNY; CurVert->nz = CurNZ; CurVert->Diffuse = CurDiffuse; CurVert->tu = CurTU; CurVert->tu = CurTV; NumVerts++; } This function has two parts to it. The first one checks if we are out of space in the array. Remember that NumVerts is 1 for the first vertex, but that vertex's index in the array is 0. This means that when d3dVertex3f() is called, pVertList[NumVerts] is the next vertex. We don't increment NumVerts until the very end. Back to the point, if we are out of space, we create a new array with more space, copy the old list to the new one, and delete the old list. Finally, we change the pointer to the new list, since the old one doesn't exist. The second part of the function simply sets the values for the vertex from the parameters, and also from the current values that we stored from d3dColor4f(), d3dNormal3f(), and d3dTexCoord2f(). After some calls to d3dVertex3f(), the client will want to render with d3dEnd(): void d3dEnd() { HRESULT r = 0; if( !bRenderBegun ) return; int NumPrimitives = 0; switch( PrimitiveType ) { case D3DPT_POINTLIST: NumPrimitives = NumVerts; break; case D3DPT_LINELIST: NumPrimitives = NumVerts / 2; break; case D3DPT_LINESTRIP: NumPrimitives = NumVerts - 1; break; case D3DPT_TRIANGLELIST: NumPrimitives = NumVerts / 3; break; case D3DPT_TRIANGLESTRIP: NumPrimitives = NumVerts - 2; break; case D3DPT_TRIANGLEFAN: NumPrimitives = NumVerts - 2; } //Create a vertex buffer and fill it IDirect3DVertexBuffer8* pVB = NULL; r = pD3DDevice->CreateVertexBuffer( sizeof(ListVertex) * NumVerts, D3DUSAGE_WRITEONLY, FVF_LISTVERTEX, D3DPOOL_DEFAULT, &pVB ); if( FAILED( r ) ) { //Don't forget that there are things to do before bailing! pD3DDevice->Release(); delete[] pVertList; bRenderBegun = FALSE; return; } void* pVertexData = NULL; r = pVB->Lock( 0, 0, (BYTE**)&pVertexData, 0 ); if( FAILED( r ) ) { pVB->Release(); pD3DDevice->Release(); delete[] pVertList; bRenderBegun = FALSE; return; } memcpy( pVertexData, pVertList, sizeof(ListVertex) * NumVerts ); pVB->Unlock(); pD3DDevice->SetStreamSource( 0, pVB, sizeof(ListVertex) ); pD3DDevice->SetVertexShader( FVF_LISTVERTEX ); r = pD3DDevice->DrawPrimitive( PrimitiveType, 0, NumPrimitives ); //release stuff, delete memory, etc. pVB->Release(); pD3DDevice->Release(); delete[] pVertList; pVertList = NULL; bRenderBegun = FALSE; } First, we make a quick check to insure that d3dBegin() has been called. Second, we take the number of vertices and work out how many primitives we have to draw. Why Direct3D can't do this by itself, I don't know. We just use a simple switch statement and a formula depending on the type of primitive requested. We then create a vertex buffer for the vertices and copy the vertex list into the buffer using a simple Lock(), memcpy, Unlock() sequence. We render the vertex list by passing the necessary parameters to DrawPrimitive. Don't forget however, that we need to set the stream source and vertex shader. If nothing is showing up on the screen when you know something should, you probably forgot to set one of these. Lastly, we release the vertex buffer and device, and delete the memory used for the vertex list. Don't forget that since the render has now ended, we need to mark the bRenderBegun flag as FALSE. If you forget to set that flag, you could run into serious trouble. Writing the Header FileNow that we've finished implementing our functions, we need to prototype them in a header file so that our programs can actually use them. In addition, we want to add that one #define for the list size. Here's the header in full: #ifndef _D3DVERTS_H_ #define _D3DVERTS_H_ #include <d3d8.h> //The start vertex list array size, and also how much to expand the array each time //To change the value, simply define it before including the header #ifndef MIN_VERTEX_LIST_SIZE #define MIN_VERTEX_LIST_SIZE 2048 #endif //Function prototypes for the vertex functions void d3dBegin( D3DPRIMITIVETYPE Type, IDirect3DDevice8* pDevice ); void d3dVertex3f( float x, float y, float z ); void d3dNormal3f( float nx, float ny, float nz ); void d3dTexCoord2f( float tu, float tv ); void d3dColor4f( float a, float r, float g, float b ); void d3dEnd(); #endif That's all there is to it. At this point, you can include the CPP and H file in your program and test them out. I've also written a sample program that might give you a feeling of deja vu as it puts your first triangle back up on screen ;). You can get it here. Adding More FunctionsIt's a good bet that you'll want to add some more versions of these functions. It simply involves adding the new functions to the CPP and H files, and implementing them. With some of the functions you might need to change their arguments to the types that D3D wants. Just to show you a sample, here's the code I wrote for d3dColor3ub: void d3dColor3ub( unsigned char r, unsigned char g, unsigned char b ) { CurDiffuse = D3DCOLOR_XRGB( r, g, b ); } It simply takes 3 byte values and uses the convenient D3DCOLOR_XRGB macro to change the 3 bytes into a single DWORD. Here's some code for d3dVertex3fv, which is also quite simple: void d3dVertex3fv( const float* pVector ) { d3dVertex3f( pVector[0], pVector[1], pVector[2] ); } This function just dereferences the pointer and passes the values on to the normal d3dVertex3f() function. Since the major framework is already written, most of the new functions will be a couple of lines at most. The sample program has many more new functions that I've written. PerformanceWhile this solution runs quite fast, it hasn't been optimized at all. Amongst other things, many of these functions could be inlined. A fast call directive might help under MSVC 6.0 as well. I've omitted these directives for brevity and to not distract from the topic at hand. ConclusionI hope that this article and source code helps you out. I've already found that these vertex commands are incredibly useful, and they've only been working for a day. If you happen to use this source code, a modified version of it, or just the idea, please add a reference to me. This could be just a note mentioning my name as the originator of the idea and/or source code. If you have any questions or comments, you can email me at imind3d@zgx.cjb.net. This article is Copyright © 2003. Although you are free to save or print out this article for your own use, under no condition may you distribute it to others without written consent of the author. Discuss this article in the forums
See Also: © 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|