• 07/13/03 08:14 PM
    Sign in to follow this  

    Implementing Immediate Mode in Direct3D

    Graphics and GPU Programming

    Myopic Rhino

    [b]Introduction[/b]
    When 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.


    [b]The Concept[/b]
    It's important to realize that I am [b]not[/b] 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.


    [b]The Interface[/b]
    I 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 [b]C++, not C![/b] 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.


    [b]The Functions[/b]
    For 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.

    [b]d3dBegin( D3DPRIMITIVETYPE Type, IDirect3DDevice8* pDevice );[/b]
    In d3dBegin, we want to set up the data structures we will be using and prepare to store the vertices. We'll also store a pointer to the device. It's important to realize that this function will not call pDevice->BeginScene(). I've chosen not to call it for flexibility; it shouldn't be a problem to call BeginScene() just before the call to d3dBegin() if you really want to. Since no actual rendering or vertex buffer creation will occur at this stage, we don't really need the device pointer. We could have passed pDevice into d3dEnd. You can move it if you like; it's a semantic decision on my part. If you do leave it here, you must not forget to put pDevice->AddRef()! (You will see the implementation later; I'm just reminding you about it.) Lastly, it would be really helpful to remember whether or not d3dBegin has been called. If d3dEnd gets called first, all sorts of wacky stuff could happen, so we need to make sure things work out correctly.

    [b]d3dEnd();[/b]
    This is the function with the most code. When d3dEnd is called, we will have a list of vertices with their associated normals, diffuse colors, and texture coordinates. We will create a vertex buffer for these vertices, transfer the vertices to the vertex buffer, and render the vertices. There will also be a couple of calculations to be made, like the number of faces to be drawn. As with d3dBegin(), this function will not call pDevice->EndScene()! You can do it just after the call if you need to.

    [b]d3dVertex( float x, float y, float z );[/b]
    The Vertex function is, of course, the one that adds vertices to our list of vertices. Also, it will be assigned the duty of resizing the list if it has run out of space. This brings us to the problem of the best way to represent our list of vertices. I will discuss it in a few paragraphs; let's first define what our functions are supposed to do.

    [b]d3dNormal3f( float x, float y, float z );[/b]
    Here, we define the normals for the vertex. This might seem a bit tricky because the normal is not actually applied to a vertex until the d3dVertex3f call. Furthermore, subsequent calls to d3dVertex3f should use the same normal, unless d3dNormal3f is called again. We will therefore need some sort of variable to keep track of the current normals.

    [b]d3dColor4f( float a, float r, float g, float b );[/b]
    This function will define the diffuse color for the vertices. As with d3dNormal3f, we will need to store these values between calls. Also notice that I rearranged the parameters from RGBA (used in the OpenGL standard) to ARGB. This is to conform to the Direct3D standard, which in this case opposes the OpenGL standards.

    [b]d3dTexCoord2f( float tu, float tv );[/b]
    This defines the texture coordinates for the vertices. Once again, we will need to store these values between calls.


    [b]Vertex Structures and Lists[/b]
    Before 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.


    [b]The Array Method[/b]
    Remember 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:

    Adding a vertex to the list:
    Resizing the list
    Copying the list to a vertex buffer:
    O(1)
    O(n)
    O(1) 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.


    [b]The Linked List Method[/b]
    The 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.

    Adding a vertex to the list:
    Resizing the list:
    Copying the list to a vertex buffer: O(1)
    O(0) (no time required because we never resize)
    O(n) 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().


    [b]The Static Variables[/b]
    We'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:

    [b]static IDirect3DDevice8 *pD3DDevice = NULL;[/b]
    A pointer to the Direct3D device. We'll hold this between the d3dBegin() and d3dEnd calls, and access it during d3dEnd().

    [b]static BOOL bRenderBegun = FALSE;[/b]
    We need to make sure that we have begun before d3dVertex3f() or d3dEnd() is called. This variable will become true at d3dBegin() and false at d3dEnd().

    [b]static float CurNX = 0.0, CurNY = 0.0, CurNZ = 0.0;[/b]
    The normals need to be held in-between calls to d3dVertex3f(). In addition, we need to be able to set the normal before d3dBegin is called (not so common with normals and texture coordinates, but OpenGL coders do it frequently with glColor*).

    [b]static D3DCOLOR CurDiffuse = 0xff000000;[/b]
    As with the normals, we need to hold on to the diffuse color. We'll put the color in this variable in between calls. Also notice that while the default color is black, we still set the alpha channel to 0xff so that we draw opaque polygons.

    [b]static float CurTU = 0.0, CurTV = 0.0;[/b]
    And of course, we need to keep the current texture coordinates.

    [b]static ListVertex *pVertList = NULL;[/b]
    Like I've explained before, this is a pointer to a dynamic array of vertices.

    [b]static long MaxArraySize;[/b]
    This holds the current maximum size of the array. We'll check NumVerts against this at every call to d3dVertex3f() to make sure that there is enough array space. If not, we'll expand the array.

    [b]static long NumVerts = 0;[/b]
    This holds how many vertices have actually been created by calls to d3dVertex3f(). It's also the logical size of the array, and the index of the next vertex to be added.

    [b]static D3DPRIMITIVETYPE PrimitiveType;[/b]
    When it comes time to actually draw the vertices, we will need to know what kind of primitives to draw. Since this is passed in as a parameter to d3dBegin() but not used until d3dEnd(), we need to keep track of its value.


    [b]Implementing the Functions[/b]
    The 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.


    [b]Writing the Header File[/b]
    Now 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 //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 [url="/reference/programming/features/imind3d/D3DVertices.zip"]sample program[/url] that might give you a feeling of deja vu as it puts your first triangle back up on screen ;). You can get it [url="/reference/programming/features/imind3d/D3DVertices.zip"]here[/url].


    [b]Adding More Functions[/b]
    It'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.


    [b]Performance[/b]
    While 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.


    [b]Conclusion[/b]
    I 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 [email="imind3d@zgx.cjb.net"]imind3d@zgx.cjb.net[/email].

    This article is Copyright (C) 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.



      Report Article
    Sign in to follow this  


    User Feedback

    Create an account or sign in to leave a review

    You need to be a member in order to leave a review

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now

    There are no reviews to display.