A 2D Guide To DirectX 8 Graphics - Using 2D graphics in a 3D Environment
by (13 June 2001)
|Return to The Archives|
Back in the day when we worked with DirectDraw, it was easier to write to the screen
because there was a drawing surface that could be accessed. This was more efficient
because we could access the video memory directly and save time (just like in the days of
MS-DOS graphics where you had to access the video memory directly.) With the release of
DirectX 8, Microsoft changed the API. DirectDraw and Direct3D has been combined into a
new component called the DirectX Graphics component. Yes the old DirectDraw interface is
still there, but it is there for backward compatibility.|
My first thought was how I could still draw lines and points on the screen without having to mess with any of the Direct3D 3D configurations (such as Z-Buffers, Lighting, and Back-Face Culling). I began by setting up the Direct3D Interface like it showed in the SDK Help docs, and then read that the front-buffer (the display screen) cannot be accessed directly but only through back-buffers (well it can be accessed directly, except that option is for testing purposes only). So the next idea was to lock the back buffer and draw to that. Unfortunately, accessing the back buffer by locking it is a performance cut for some video cards. The method that I ended up with is plotted out below. I described every step from setting up the interface to drawing and displaying it. If you are used to using DirectDraw and have never used Direct3D (like me), you should read this tutorial thoroughly because the DirectX API has changed quite a bit and it could be confusing.
This tutorial will demonstrate how to draw 500 lines all over the screen (it will be a full screen app) until the user presses Escape. I chose lines instead of rectangles to show a general form of drawing in 2d (after reading you should understand how to draw points or rectangles).
The first thing that you must do before you do any coding is make sure that you have setup
the DirectX compiler directories and linker libraries correctly. (nothing will work until
you have them setup right)|
The header files that will be needed are:
windows.h, windowsx.h (general Win32 headers)
d3d8.h (general Direct3D header)
dxerr8.h (for debugging purposes).
WIN32_LEAN_AND_MEAN will throw out the rarely used Win32 stuff.
The last two constants will define the screen width and height that Direct3D will operate using. You can change these values to whatever resolution you desire to work in, just be sure that you test to make sure the display adapter will support it.
After everything is perfect, the coding will commence...
A Short Lesson On Debugging In DX 8
One of the most important tools in the DirectX 8 SDK is the debug function, DXTrace(). This
function can display a message box when one of your interface methods fail and you need
information about why it failed. Optionally it can display a message in the debugger window if you
are working in Full Screen and can't see the message box. The prototype for DXTrace():|
To demonstrate this code, I've used the CreateDevice() method from the IDirect3D8 Interface as a model.
Setting Up The Data Structures
The first thing I'm going to create is a structure for each point. Each point will need an x, y,
and z coordinate, and a reciprocal-homogeneous w component. (The w component is used
when dividing the x, y, and z coordinates to determine depth. Stepping ahead we will assign
it as 1 so the x and y components are uneffected.)
Each component will be of the float data type, and the color will be unsigned long. I'm
going ahead and defining the lineList global variable. Since we will be generating 500 lines
there are going to be 1000 points. |
The #define preprocessor following the structure sets up the flags Direct3D will need to process our custom vertex points. D3DFVF_XYZRHW is the flag that describes it as being a transformed vertice (since it has the RHW) and the D3DFVF_DIFFUSE flag says that it uses a diffuse color component.
I figured the best way to demonstrate the Direct3D interfaces would be to create a class, that way the tutorial is easier to follow. So the DirectX Graphics class is going to be called DXGraphics. Here it is in all it's glory, i'll explain the implementation of it as we move along...
Let's begin by talking about the private members of the DXGraphics class...
The Little Demons Of Direct3D 8
The first object that is necessary for using Direct3D is the Direct3D object (D3DObject). This
object is actually a pointer to the IDirect3D8 interface and holds the key to all the functionality
of Direct3D. D3DDevice is the pointer to the IDirect3DDevice8 interface that performs all the
rendering on the screen -- most of the time our code will be using methods from this interface.
Finally, D3DVertexBuffer is a pointer to the IDirect3DVertexBuffer interface. This interface
manages the Vertex Buffer which holds the vertices of our lines. When the lines are ready to be
displayed the Direct3D renderer will read the vertices from this buffer. The last two
structures aren't as important right now, but they will be needed later.|
The first two member functions I'll talk about are the Constructor and Destructor. In the Constructor, the three pointers need to be assigned to NULL. That way we don't access a part of memory that isn't supposed to be touched accidently.
In the Destructor we need to clean up the Direct3D pointers and release the memory, plus decrement the reference counter (the reference counter counts the number of things that are depending on the interface). If it isn't released it will cause a memory leak. At the beginning you check to see if the pointer is pointing to NULL. If it isn't NULL, it has been used and it needs to be released. Starting with the latest pointer you go down the list until you finally get back to the D3D Object.
The next section will talk about gaining access to Direct3D...
Setting Up Direct3D's Main Object
We want to start up Direct3D so we need to create the Direct3D object. The function
Direct3DCreate8() will load the interface and then send us a pointer so we can operate it.
Here's the prototype for it:|
It's really easy. For the SDKVersion, you must use the D3D_SDK_VERSION flag so that it will be built with the right header files (In case something changes in a header file this will signal that it needs to be recompiled.) If something goes wrong the function will return NULL. So here is how we'll use it...
Alright, now we have a pointer to the IDirect3D8 interface. The next step is to get the Display Adapter settings from Windows. An IDirect3D8 interface method called GetAdapterDisplayMode() will perform this task. And here's the prototype:
The first parameter asks which adapter to query because there might be multiple video cards. The primary display adapter can be found by sending in the D3DADAPTER_DEFAULT flag. The last parameter will return the current display mode settings. The D3DDISPLAYMODE structure is needed to get the results and that was already defined in the class definition. So all together here's how DXGraphics::createD3DObject() is implemented:
At the beginning of the function, the handle to the main window (where Direct3D will operate from) is passed to the member variable hMainWnd. This is important because hMainWnd will be used in the next function that will be discussed.
Note: This will be the only time I will use the DXTrace() feature. In the rest of the tutorial I will be using the FAILED() and SUCCEEDED() debug functions.
Creating The Direct3D Device
The Direct3D Device Interface is where all the power lies. Once you call the function that
creates the Device, it will set everything up that you asked for. It will setup the Front Buffer (
the primary display screen ) and then the number of Back Buffers you specify. You can
change the resolution higher or change the bit depth from 16-bit to 32-bit ( just as long as the
display adapter supports these modes ;) ). To specify these settings a structure named
D3DPRESENT_PARAMETERS will need to be initialized and cleaned out ( using ZeroMemory() ).|
The presentation parameters that I will talk about are not the only ones available. Look in the DirectX 8 SDK for the others and their descriptions. The parameters that are going to be used (from D3DPRESENT_PARAMETERS structure) are:
The next part is where DirectX 8 is now a lot easier to handle than it was the previous versions. Instead of calling QueryInterface(), like in the past, all that needs to be done in this version is a call to D3DObject's interface method, CreateDevice(). There will be several parameters to this method. Here's the prototype of IDirect3D8's CreateDevice():
Here's how it would be implemented in our code with the DXGraphics class:
For the Adapter parameter we use the D3DADAPTER_DEFAULT flag again. For the device type I chose the hardware rasterization flag D3DDEVTYPE_HAL ( the other flags available for this parameter are for software rasterization.) The window handle we have saved in the DXGraphics class is used for the rendering window (hFocusWindow). Then we pass in our presentation parameters we defined and then when the device has been created successfully we recieve the pointer and assign it to D3DDevice.
The last three things that should be done now that the device has been created is to turn off the 3D render settings like Lighting, the Z-Buffer, and Back-face culling.
Finally, the complete createD3DDevice() function of our DXGraphics class:
Bringing The Monster To Life
All that's left to do now is explain how to render the graphics in Direct3D. This section might
become a little confusing so I'll explain it the best I can. The first few functions that need to
be defined are the line(), generateLines() and createD3DVertexBuffer() functions in our
Back at the beginning when I defined the structure for Point, I went ahead and defined the lineList global variable. I defined it as an array because of the way the Direct3D's Vertex Buffer works. The generateLines() will store each endpoint in the lineList array. They will be stored in order so the first two items in the array create the first line and the next two create the second line and so forth. The Vertex Buffer will read the points from that lineList array and then render them onto the screen by the primitive list I tell it to use (which will be a line list.) So here are the generateLines() and line() functions:
Now the Vertex buffer needs to be created. The CreateVertexBuffer() method is part of the IDirect3DDevice8 Interface. It will need to know how big the buffer will be, how it will be used so it can determine the type of memory it needs, and what the flags are for the vertices.
For Length, we need to get the exact amount in bytes for how big the buffer will be. So you take the sizeof() one Point and times it by 1000. The Usage flags that are to be chosen will determine what kind of memory and what extra memory will be needed for this buffer. I used the D3DUSAGE_DONOTCLIP, D3DUSAGE_DYNAMIC, and D3DUSAGE_WRITEONLY flags. We won't need a clipper for the 2d line drawing (since the lines are limited to the size of the screen). Because we are generating 500 new lines over and over again it will be useful to have it stored in AGP memory, (or if the system doesn't have AGP memory Direct3D will figure out a good place to put it) and having it as a writeonly buffer lets Direct3D choose the best place for the memory. The vertex flags (FVF) we had already defined at the beginning so just pass in the POINT_FLAGS.
The Pool parameter will decide where the buffer will be in the memory pool, just use D3DPOOL_DEFAULT so it is stored where it should be in video memory. Last we will get the pointer to our new Vertex Buffer and assign it to D3DVertexBuffer.
Watching The Monster Dance: Rendering In 2D
The final function that will be created is the renderIt() function of our DXGraphics class. This will
render the points on the screen and create a spectacular display of thousands of randomly
colored lines - well it isn't that spectacular but what can I say?|
The first line of the renderIt() function initializes the pBuffer pointer. Next the Vertex Buffer is locked. Why? When the buffer is locked you will be able to gain access to the certain spot in memory where we will place our list of vertices. Here's the prototype for the method Lock() from ID3DVertexBuffer8's interface:
The first parameter OffsetToLock is where we want the pointer to start at, use 0 to start from the beginning. SizeToLock is how much memory we will be using, so we'll need the same memory size of our list of lines to be taken out. Next is the pointer to the spot in memory that will be returned, that's going to be the job for pBuffer. The flags that are going to be used are D3DLOCK_DISCARD and D3DLOCK_NOSYSLOCK. The first flag will tell the Direct3D that when the buffer is done being rendered and a new list of lines has been generated, it should overwrite the old list that was in the buffer with the new list. The second flag prevents the system-wide critical section from being locked, so we can still move the mouse cursor and reset the computer if need be.
After the buffer has been locked you fill it with the lineList array. That is achieved with a simple memcpy().
Then after the buffer is filled, unlock it.
Now the buffer is set to go. First we call the IDirect3DDevice8's method BeginScene(). This has Direct3D confirm that all the internal settings and flags are ready. If it is successful ( and only if it is successful ) we can start rendering the lines. Here's what happens inside the BeginScene() code block:
First the stream source is set. The stream source is where the Direct3D device will be able to find the Vertex Buffer. We specify 0 for the first stream number and then send in the Vertex Buffer ( D3DVertexBuffer ). Last we define the stride, which is the size of the point structure.
Next we set the Vertex Shader. The Vertex Shader is the mechanism that reads and processes the points (for more info read the DirectX 8 SDK), all it needs is the definition flags ( POINT_FLAGS) of our custom vertex Point. At last we draw the lines. DrawPrimitive() asks what type of primitive it will be drawing, I've used the D3DPT_LINELIST flag for our lines. It needs to start from the beginning so 0 is used and we will be rendering 500 lines.
Finally end the scene and use Present() to flip the back buffer onto the front buffer. The first two NULL's mean that the entire back buffer will be copied (not just a section) and it will be copied onto the entire front buffer. The third NULL means that it will use the default window handle that we specified in the D3DPRESENT_PARAMETERS. The last parameter hasn't been implemented yet so it is obviously NULL. Here the complete renderIt() function:
That's All For Now
|Included with this tutorial is the full source code (article_guidedx82d.cpp) that is compatible with Visual C++ 6. I did not explain how the code would be worked into WinMain(), if you need to see how the class works in WinMain() download the source code ( I really don't feel like writing and explaining anymore ;) ). I hope you have good luck with this code. And this code is by no means optimitized, so once you figure it out see where you can make it run faster. Thanks for reading!|