Introduction to Direct3D (DX7)
Andreas Jönsson, May 2000
In this tutorial we will expand our simple window, and add some Direct3D to it. You will learn how to initialize Direct3D with the help of Direct3DX and then render a rotating colored triangle. This tutorial is only meant as an introduction to Direct3D and Direct3DX.
Includes and libraries
As we still need the window we will include windows.h this time as well. In addition to that we will include d3dx.h and mmsystem.h. d3dx.h makes sure we have all the declarations of the Direct3D functions we need and mmsystem.h has the declaration of timeGetTime() that we'll use later on.
// Don't include rarely used code to speed up compilation #define WIN32_LEAN_AND_MEAN // We want to use the overloaded operators for common D3D types #define D3D_OVERLOADS #include <windows.h> #include <mmsystem.h> #include <d3dx.h>
As you can see I have defined D3D_OVERLOADS before including d3dx.h. This let's us use the overloaded operators for the common Direct3D datatypes, thus making it easier to work with them.
As before we need to link with some libraries to get the implementation of the functions that we'll use for this program. We'll need user32.lib and gdi32.lib for the window functions, ddraw.lib and d3dx.lib for the DirectX functions and winmm.lib for the timeGetTime() function.
Initializations
After the window has been created as before with Create(), we can initialize Direct3D. For this purpose I have implemented a new function, named InitializeD3D(), which we will take a look at right now.
HRESULT InitializeD3D() { // Begin by initializing D3DX HRESULT hr; if( FAILED(hr = D3DXInitialize()) ) return hr; // Create the D3DX Context // D3DX creates everything we need here hr = D3DXCreateContext(D3DX_DEFAULT, // Best acceleration 0, // Windowed mode g_hWnd, // The render window D3DX_DEFAULT, // Same width as window D3DX_DEFAULT, // Same height as window &g_pD3DX); // The context if( FAILED(hr) ) return hr; // In order to render with Direct3D // we'll need the Direct3DDevice g_pD3DDev = g_pD3DX->GetD3DDevice(); if( g_pD3DDev == 0 ) return E_FAIL; // We will not use any lighting in // this tutorial so we turn it off g_pD3DDev->SetRenderState(D3DRENDERSTATE_LIGHTING, FALSE); // Let's use another background color g_pD3DX->SetClearColor(D3DRGB(0.5f,0.5f,0.5f)); // Success g_bDXReady = true; return S_OK; }
As we are using the utility library, Direct3DX, to help with the initialization we need to call D3DXInitialize() before any other Direct3D calls. The SDK documents doesn't say what this function does, but a logical guess would be that it checks to see if the user has the correct version of DirectX installed and perhaps even enumerating available display devices on the system.
After D3DX has been initialized we create a ID3DXContext with D3DXCreateContext(). With this single call everything we need has been created for us: DirectDraw object, primary buffer, backbuffer, Direct3D object, Direct3DDevice object, etc. With previous versions of DirectX all of this had to be created one at a time which would make a considerable amount of code.
D3DXCreateContext() isn't very flexible, you can choose the 3D device to use, fullscreen or windowed mode, and fullscreen resolution, but that's about it. For most application you don't need anything else, but if you do you can use D3DXCreateContextEx() which have a lot more settings. If that isn't enough your only option left is to initialize Direct3D the old fashioned way. The best thing with D3DX library is that you can use whatever parts you need and simply ignore the rest.
After the context has been created we need to retrieve a pointer to the Direct3D device so we can do some rendering, this is done with ID3DXContext::GetD3DDevice(). From the context we can also get a pointer to DirectDraw and Direct3D if need be, but in this simple tutorial we wont use those objects.
One more thing is important in this function and that is that I turn off the Direct3D lighting. If it is on, which it is by default, and there are no enabled lights all triangles rendered will come out black. This is something that is new for DX7 so if you are converting from DX6 or earlier versions you may experience this problem. The lighting is turned of with a call to IDirect3DDevice7::SetRenderState().
An important advice
It is always important to be ready to catch any error that may arise when programming, and with DirectX most functions return an HRESULT to indicate success or error. Because there may be more than one error code, and in some cases more than one success code, you should always check for error with the macro FAILED() or SUCCESS().
First after the error has been detected should you try to determine the exact error. This usually means a large switch case where all the possible errors are tested, but with D3DX there is an easier way. And that is to use D3DXGetErrorString() to get a textstring, readable by humans, and print it out to the user. In this tutorial I do this with ReportError() which is called from WinMain(), when any of the functions return an error code.
void ReportError(HRESULT hr) { // Get a readable translation of the error char szError[256]; D3DXGetErrorString(hr, 256, szError); // Show the error message to the user MessageBox(NULL, szError, "Fatal Error", MB_OK|MB_ICONERROR|MB_SYSTEMMODAL); }
As you can see I use MessageBox(), to show the errors, some alternatives would be to print it to the debug output with OutputDebugString() or to print it to a logfile.
Running the application
We need to change the function Run() in order to take advantage of the idle time between messages. Previously we used GetMessage() to retrieve our messages, this time we'll use PeekMessage() instead. If there are no messages to be retrieved we render a frame and then check again.
int Run() { // Check messages until we receive a WM_QUIT message MSG Msg; Msg.message = ~WM_QUIT; // Make sure it is not WM_QUIT while( Msg.message != WM_QUIT ) { // We will use PeekMessage() so we can // render the triangle during idle times if( PeekMessage( &Msg, NULL, 0, 0, PM_REMOVE ) ) { // Dispatch the message to the window procedure DispatchMessage( &Msg ); } else { // There were no messages in the queue // so we can proceed with our rendering if( FAILED(IdleAction()) ) PostQuitMessage(-1); } } return (int)Msg.wParam; }
I don't think there is much more to say about Run(). Instead we'll take a look at the window procedure, as there are some changes there as well.
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { HRESULT hr; switch( uMsg ) { case WM_SIZE: // If the window is resized we need to resize the // renderbuffers as well otherwise Direct3D will // stretch them to fit the window giving heavy // pixelating effects if( g_bDXReady ) { if( FAILED(hr = g_pD3DX->Resize(LOWORD(lParam), HIWORD(lParam))) ) { ReportError(hr); g_bDXReady = false; PostQuitMessage(-1); } } return 0; case WM_DESTROY: // Terminate the Direct3D objects before the window is deleted TerminateD3D(); // Tell the message loop to quit PostQuitMessage(0); return 0; } // Let the default window procedure handle // any message that we don't care about return DefWindowProc( hWnd, uMsg, wParam, lParam ); }
As you can see there are only two window messages that are treated specially. The most important of those is the WM_DESTROY message as it is there we uninitialize and realease the DirectX objects. I suppose it can be done after the window has been deleted in WinMain() but I prefer to do the cleanup process in the reverse order of the initialization.
The other message that is treated is WM_SIZE that is sent to our window after its size has changed. As the window has changed you should also change the size of the renderbuffer to reflect this. Again, with the help of D3DX this is very easy. A call to ID3DXContext::Resize() is what we'll make, with the dimensions taken from the message's lParam. As I programmed this tutorial I first forgot to treat this message, and I made an interesting discovery. If the size of the renderbuffer and the window doesn't match D3DX stretches the surface to fit the window with bilinear filtering. This usually doesn't look good, but it can maybe be used in certain situations to an advantage.
Rendering with Direct3D
Now we finally come to the fun part about Direct3D, rendering scenes. As previously stated I will only give an introduction to the rendering here by showing you how to render a rotating triangle, hopefully it will be enough to spark your interest to find out more by yourself.
The first thing that will be done is to clear the screen of any residue from previous renderings, this is done by calling ID3DXContext::Clear(). This fills the screenbuffer with the color previously set with ID3DXContext::SetClearColor(). It also optionally clears the depthbuffer and stencilbuffer.
After the buffer has been cleared we can render the scene. To tell any available hardware that we are beginning a new scene we call IDirect3DDevice7::BeginScene(). This gives the device the opportunity to process all polygons before rendering them, for example doing a depth sort so that they don't have to use a depthbuffer. When the scene is finished we report this to the device with a call to IDirect3DDevice7::EndScene().
When the scene has been successfully rendered we need to put it on the screen so that it will be visible to the user. With D3DX we do this with a call to ID3DXContext::UpdateFrame(). In this tutorial I quit the program if the call fails, but you don't have to be so drastic. If the call fails it usually means that your surfaces has been lost due to another program taking over the device for a while. Most of the time these surfaces can be restored so that the program can resume its execution, but I will not go into how that is done here.
Let's take a look at IdleAction(), where all this takes place.
HRESULT IdleAction() { if( !g_bDXReady ) return E_FAIL; // Clear the framebuffer and zbuffer HRESULT hr; hr = g_pD3DX->Clear(D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER); if( FAILED( hr ) ) return hr; // Begin a new scene hr = g_pD3DDev->BeginScene(); if( SUCCEEDED( hr ) ) { // Let's rotate the triangle at a steady rate D3DXMATRIX Rot; D3DXMatrixRotationZ(&Rot, timeGetTime()/1000.0f); g_pD3DDev->SetTransform(D3DTRANSFORMSTATE_WORLD, (D3DMATRIX*)&Rot); // Render the like sided triangle Vertex V[3]; V[0].v = D3DVECTOR(-1,-0.577f,1); V[0].c = D3DRGB(1,0,0); V[1].v = D3DVECTOR( 0, 1.155f,1); V[1].c = D3DRGB(0,1,0); V[2].v = D3DVECTOR( 1,-0.577f,1); V[2].c = D3DRGB(0,0,1); g_pD3DDev->DrawPrimitive(D3DPT_TRIANGLELIST,FVF_VERTEX,V,3,0); // The scene is finished hr = g_pD3DDev->EndScene(); } if( FAILED(hr) ) return hr; // Show the scene if( FAILED(hr = g_pD3DX->UpdateFrame(0)) ) return hr; return S_OK; }
With IDirect3DDevice7::SetTransform() we tell Direct3D how the vertices we send to it should be transformed. Sligthly simplified the vertices are first transformed by the world matrix which puts them into world coordinates, then they are transformed by the view matrix into camera coordinates. And finally they are transformed by the projection matrix that projects them onto the screen. In this tutorial I only work with the world matrix to rotate the triangle, I leave the other at their default settings.
Computing the matrices can either be done by hand or using one of the numerous helper functions that the D3DX utility library has. I prefer to use the helper functions myself as it allows me to utilize any special optimization that Microsoft might have added to the code, not that I know if there are any. This time I only use D3DXMatrixRotationZ() to rotate the triangle around the z axis, which is the direction the we are facing.
Before the triangle can be rendered we need to setup its vertices. If you have programmed with earlier versions of Direct3D you may have heard about D3DVERTEX, D3DLVERTEX, and D3DTLVERTEX. You should forget about them, they are still supported but it is better to define your own format with the flexible vertex format that Direct3D uses. Our triangle vertices will only include position and color, since we don't want to light them or texture the triangle. The most important thing to remember with the flexible vertex formats is that you must have the right order for the attributes. There are also certain attributes that cannot be combined in the same vertex format, for example untransformed and transformed coordinates.
#define FVF_VERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE) struct Vertex { D3DVECTOR v; D3DCOLOR c; };
Now that we know what attributes to use in the vertices we can setup the vertices and render the triangle with DrawPrimitive(), as is done in the function above.
I will take this opportunity to tell you a bit about timekeeping. I see a lot of upcoming developers that move their scene objects a fix amount each frame with no regard for how much time that has passed since the last frame. This may work just fine on their own system, but unfortunately with todays market everyones systems will be next to unique and the update frequency will differ between systems. The solution to this problem is to take the time into account when updating the scene, so that if less time has passed since last update the objects are moved a less amount. For this tutorial I use timeGetTime(), that returns the time in milliseconds since the Windows was started, to keep the triangle rotating at a constant speed.
Termination
Obviously the user don't want to leave our program running forever so we need some code for shutting it down as well. Preferably this should be done without unnecessary errors. When we receive a WM_DESTROY message we call TerminateD3D() to clean up our Direct3D code. In this function we must release all our objects by calling Release() on them then when all objects have been released we unitialize D3DX with D3DXUnitialize().
void TerminateD3D() { // Release our DirectX objects if( g_pD3DDev ) { g_pD3DDev->Release(); g_pD3DDev = 0; } if( g_pD3DX ) { g_pD3DX->Release(); g_pD3DX = 0; } // Let D3DX do some final uninitializations D3DXUninitialize(); }
Conclusion
And that concludes this tutorial, hopefully you have seen the advantages of D3DX and that isn't that hard to make a Direct3D application. As always, if you have any questions lingering feel free to send me an e-mail.