Let's face it, Windows code is a pain! Who wants to piece together hundreds of lines of archaic WinAPI code just to initialize a window. Wouldn't it be wonderful if it were possible to initialize a window, setup Direct3D 9.0, render some primitives to the screen, and construct a basic input handling system without having to touch a single line of obfuscated WinAPI code?
Well, guess what...with Sam Lantinga's SDL library, it can, and with less than 100 lines of code at that! SDL is a powerful cross platform library which allows you to perform windowing, complex graphic operations, music, input handling, and multithreading while compiling with the same code on a myriad of operating systems including Windows, Linux, and Macintosh. In this tutorial however, we will be using SDL for its elegant windowing and input. Despite your initial hopes and inclinations, since we are using Direct3D the code will not be cross platform. Sam is brilliant, but I don't think he is
that brilliant! :) With that brief and informative history, let's get started!
Assuming you have the DirectX 9.0 SDK installed all you need to do is head over to
http://www.libsdl.org, the official website for SDL, and download the source. If it makes you more comfortable, skim the very informative DOC project to get a general feel for how SDL works, although this is not really necessary. For this project we will be including the following headers:
windows.h, d3d9.h, d3dx9.h, and
sdl.h. Additionally we will link to the following libraries:
sdl.lib, sdlmain.lib, d3d9.lib, and
d3dx9.lib.
Due to the simplicity and direct nature of this program, the only function defined is the main one, main(), so let's define it and declare all the variables this program will be using. I have commented the purpose of each variable and hope the meanings are somewhat conceptually clear in an abstract sense.
// this is the type used to describe a single vertex
// each vertex has x,y,z coords and a color associated with it
struct D3DVERTEX
{
float fX,fY,fZ;
DWORD dwColor;
};
int main( int argc, char* argv[] )
{
SDL_Event event; //used to poll for events and handle input
LPDirect3D9 Direct3D_object; //used to create a direct 3d device
LPDirect3DDEVICE9 Direct3D_device; //basic rendering object
D3DPRESENT_PARAMETERS present_parameters; //stores the important attributes and
D3DXMATRIX projection_matrix; // properties your Direct3D_device will have
LPDirect3DVERTEXBUFFER9 tri_buffer = NULL; //data buffer which the Direct3D_device can draw from
VOID* pData; //pointer to beginning of vertex buffer
//actual data to be fed to the vertex buffer
D3DVERTEX aTriangle[ ] = {{-2.0f,1.0f,10.0f,0xffff0000},
{-3.0f,-1.0f,10.0f,0xff00ff00},
{-1.0f,-1.0f,10.0f,0xff0000ff}};
Now you may be wondering why a win32 app would use a console style main() function. The answer is that as part of SDL's cross platform code, it takes care of the WinMain() function and layers main() over it. So effectively your entry point is main() just as it in a console program.
With that confusion settled it's time to discuss the basic Direct3D initialization process.
- initialize a window
- create a Direct3D object
- set the important parameters for the Direct3D device you want to make
- using the Direct3D object, parameters, and a handle to your window
- create the Direct3D device
- establish projection matrix, lighting, culling, depth buffer, and vertex format
- load in primitive data
When all those steps are taken care of, you want to generate your central game loop which will render graphics each frame as well as handle messages and input.
This will be covered in the 7[sup]th[/sup] and final step.
Initialize a Window
When programming with the Windows API, making windows is a pain in the arse. You have to register a window class, define callback procedures, and set a million parameters. All of this when you only want a simple window to serve as a rendering context. Fortunately for us, SDL can do all of this for us. The following lines of code will initialize the SDL_VIDEO subsystem, and create an 800 by 600 video mode. Of course you are free to adjust the resolution to your needs.
if( SDL_Init( SDL_INIT_VIDEO ) < 0 || !SDL_GetVideoInfo() )
return 0;
SDL_SetVideoMode( 800, 600, SDL_GetVideoInfo()->vfmt->BitsPerPixel, SDL_RESIZABLE );
That's it! A window has been created, and to get a handle to it all you need to do is call GetActiveWindow().
Create a Direct3D object
This couldn't be any easier...
Direct3D_object = Direct3DCreate9(D3D_SDK_VERSION);
if( Direct3D_object == NULL )
{
MessageBox(GetActiveWindow(),"Could not create Direct3D Object","D3D_OBJ ERR",MB_OK);
return 0;
}
Direct3D Device Parameters
ZeroMemory(&present_parameters, sizeof(present_parameters));
present_parameters.Windowed = false;
present_parameters.SwapEffect = D3DSWAPEFFECT_DISCARD;
present_parameters.EnableAutoDepthStencil = true;
present_parameters.AutoDepthStencilFormat = D3DFMT_D16;
present_parameters.hDeviceWindow = GetActiveWindow();
present_parameters.BackBufferWidth = 800;
present_parameters.BackBufferHeight = 600;
present_parameters.BackBufferFormat = D3DFMT_R5G6B5;
present_parameters.MultiSampleType = D3DMULTISAMPLE_NONE;
Explaining each and every parameter would be overkill for a beginner's tutorial. Essentially they allow you to define the core properties of your Direct3D device, like the dimensions of the screen buffer, the format of the screen buffer, the way the buffer is swapped each frame, the format of the stencil buffer, and so on...
Creating the Direct3D Device
Just like the object, creating the device is an easy task once you have the window and parameters filled in:
if( FAILED(Direct3D_object->CreateDevice(D3DADAPTER_DEFAULT,D3DDEVTYPE_HAL,
GetActiveWindow(), D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&present_parameters,&Direct3D_device)))
{
MessageBox(GetActiveWindow(),"Could not create Direct3D Device","D3D_DEV ERR",MB_OK);
return 0;
}
Setting States
Next, we need to establish the projection matrix, lighting, culling, depth buffer, and vertex format. Sounds like a lot, doesn't it? Actually the Direct3D device can do each of these with one to two lines of code respectively, at least for our purposes.
For this projection matrix I have set the near and far planes to 1 and 1000 respectively, and used the ratio of the screen height to the screen width as the aspect ratio. (Make sure to adjust this ratio accordingly if you modified the height and width.) In the other lines of code lighting is disabled, backface culling is disabled, the depth buffer is set, and a vertex format is set. (D3DFVF_XYZ | D3DFVF_DIFFUSE) means that a vertex's data is organized in linearly in memory as its x, y, and z coords followed by its color.
D3DXMatrixPerspectiveFovLH(&projection_matrix, D3DX_PI / 4.0f,(float) 800/600,1, 1000);
Direct3D_device->SetTransform(D3DTS_PROJECTION,&projection_matrix);
Direct3D_device->SetRenderState(D3DRS_AMBIENT,RGB(255,255,255));
Direct3D_device->SetRenderState(D3DRS_LIGHTING,false);
Direct3D_device->SetRenderState(D3DRS_CULLMODE,D3DCULL_NONE);
Direct3D_device->SetRenderState(D3DRS_ZENABLE,D3DZB_TRUE);
Direct3D_device->SetFVF((D3DFVF_XYZ | D3DFVF_DIFFUSE));
Loading Primitive Data
First off we need to create our vertex buffer object. It obviously needs to be as large as our triangle data is, and for now we only want to write to the buffer so let's specify that as well since the device can optimize if it knows we won't be reading from it. Also we need to specify the vertex format the buffer will use, and pass a flag telling it to manage the data. We finally pass the address of the object which will become the vertex buffer. The last parameter has its uses, but we do not need it for now so it needs to be NULL.
Direct3D_device->CreateVertexBuffer(sizeof(aTriangle),D3DUSAGE_WRITEONLY,
(D3DFVF_XYZ | D3DFVF_DIFFUSE),
D3DPOOL_MANAGED,&tri_buffer,NULL);
The following functions set pData as the main pointer to the vertex buffer, and copy the vertex data to the memory pointed by pData.
tri_buffer->Lock(0,sizeof(pData),(void**)&pData,0);
memcpy(pData,aTriangle,sizeof(aTriangle));
tri_buffer->Unlock();
Final Step
Now that everything is initialized, we need only to make our main game loop, which will render to the buffer each frame and also process incoming messages. Each frame we need to clear out the buffers. Meanwhile all data has to be drawn in between a BeginScene() and EndScene() as illustrated below.
The input as handled by SDL is fairly straightforward as well. In this case it will respond to a user hitting the escape key by the freeing Direct3D object and device, and then quitting.
while( 1 )
{
Direct3D_device->Clear(0,NULL,D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
D3DCOLOR_XRGB(0,0,0),1.0f,0);
Direct3D_device->BeginScene();
Direct3D_device->SetStreamSource(0,tri_buffer,0,sizeof(D3DVERTEX));
Direct3D_device->DrawPrimitive(D3DPT_TRIANGLELIST,0,1);
Direct3D_device->EndScene();
Direct3D_device->Present(NULL,NULL,NULL,NULL);
while( SDL_PollEvent( &event ) )
switch( event.type )
{
case SDL_KEYDOWN:
if ( event.key.keysym.sym == SDLK_ESCAPE )
{
Direct3D_device->Release();
Direct3D_device = NULL;
Direct3D_object->Release();
Direct3D_object = NULL;
return 0;
}
break;
}
}
return 0;
}
I hope this tutorial was helpful. you may also want to consult the full source code which is attached to this article. Suggestions, complaints, or complements can be emailed to me at [email="mconway@tulane.edu"]mconway@tulane.edu[/email].
Acknowledgements
I want to thank Victor Saars for his wonderful tutorials at
http://www.riaz.de/tutorials/d3d.html. This tutorial is based off of the knowledge I gained from his excellent explanations.
Biography
Michael Conway is a student attending Tulane University and is currently majoring in computer science and mathematics. He has been programming since his senior year of high school, but has played video games his entire life. Naturally the two coincide to game development :). He has experience in c/c++, OpenGL, SDL, PHP, VisualBasic, and is currently working on a small scale online 3D adventure game. With the advent of this tutorial, he has now embarked on a journey to learning Direct3D in the hopes of widening his horizons in the 3D graphics domain and seeing what it is like on the other side.