There is an easy way to learn DirectX. Unfortunately, that easy way has a
lot of limits to its capabilities, and even if it didn't, it is quite a hassle to
use in large games. The easier way to program is, of course, harder to learn,
and this lesson will cover that easier-to-program way, hopefully without making
the learning part too hard.
In this lesson you will learn to draw a triangle on the screen. We will build
this triangle by creating a series of vertices and having the Direct3D device draw
them on the screen.
First we will cover the theory of how this all works, then we will go over the code
itself and build the program.
If you went through Lesson 3 in any great detail, you will recall the definition
of vertex: the location and properties of an exact point in 3D space.
The location simply consists of three numerical values which represent the vertex's
coordinates. The properties of the vertex are also defined using numerical
values.
Direct3D uses a technology called a Flexible Vertex Format (or FVF). A vertex format
is the layout of the data containing the location and properties of a vertex.
A flexible vertex format would be a format of data that you can modify and set according
to your needs. Let's take a look at how this works exactly.
A vertex is made of a struct, which contains the data pertinent to creating whatever
3D image it is made for. To display the image, we will copy all the information
to the Video RAM and then order Direct3D to copy the data to the back buffer.
However, what if we were forced to send all the data that could possibly be wanted
for a vertex? This would happen.

Image 4.1 - A Vertex Format Containing All Possible Data
Of course, you may
not see right away what the problem is here, but let's say we
only needed two of these blocks of information. We could send it to the Video
RAM much faster by doing it like this:

Image 4.2 - A Flexible Vertex Format Goes Faster
This is what happens when we use a flexible format. We select which information
we want to use, and send just that, enabling us to send many more vertices between
each frame.
In Direct3D, each vertex is made from a pre-set vertex format. As the title
claims, this format is flexible, and is built using certain elements Direct3D provides.
The elements are set using specific flags which, when logically ORed together, create
a vertex definition, or a code that tells Direct3D the vertex format.
Let's take a look at how this is done. Let's say we want to include the location
and the diffuse color of our vertices. We would build a code that looked like
this:
#define CUSTOMFVF (D3DFVF_XYZRHW | D3DFVF_DIFFUSE)
Later, when we are working with the vertices, we will simply use CUSTOMFVF, rather
than type out the entire FVF code each time. We'll see an example of this
in a minute.
We can add all kinds of flags into this expression here. Following is a table
of flags we will use throughout this tutorial, and a description of what they do
(although don't go plugging them in randomly just yet).
[Table 4.1 - FVF Code Flags]
|
Flag |
Description |
Types Included |
|
D3DFVF_XYZ |
Indicates that the vertex format includes the X, Y and Z coordinates of an untransformed
vertex. Untransformed means that the vertex has not yet been translated into
screen coordinates.
|
float, float, float
|
|
D3DFVF_XYZRHW |
Indicates that the vertex format includes the X, Y and Z coordinates as well as
an additional RHW value of a transformed vertex. This means that the vertex
is already in screen coordinates. The Z and the RHW are used when building
software engines, which we will not get into. |
float, float, float, float |
|
D3DFVF_DIFFUSE |
Indicates that the vertex format contains a 32-bit color code for a vertex, used
for the color of diffuse lighting. |
DWORD |
|
D3DFVF_SPECULAR |
Indicates that the vertex format contains a 32-bit color code for a vertex, used
for the color of specular highlighting. |
DWORD |
D3DFVF_TEX0
through
D3DFVF_TEX8 |
Indicates that the vertex format contains the coordinates for any textures that
will be applied to a model. |
float, float |
|
[Close Table] |
There are, of course, more things to put in, and they are all covered in the DirectX
documentation. However, we will only be needing these flags for this
tutorial.
Now we need to create the vertices using our new format. We don't use any
new function or anything like that; we do it by building a simple struct containing
the variables we included in the FVF code.
For instance, we used both the D3DFVF_XYZRHW and D3DFVF_DIFFUSE flags in the example
above, and to go with it, we should build the following struct:
struct CUSTOMVERTEX
{
FLOAT x, y, z, rhw; // from the D3DFVF_XYZRHW flag
DWORD color; // from the D3DFVF_DIFFUSE flag
}
As you can see, the first four FLOATs are values represented by the D3DFVF_XYZRHW
flag, while the DWORD is represented by the D3DFVF_DIFFUSE flag. If you look
at the above table, you will find which variable types go with which FVF code flags.
Now let's build an actual vertex using our new CUSTOMVERTEX struct. We could do it like this:
CUSTOMVERTEX OurVertex = {320.0f, 50.0f, 1.0f, 1.0f, D3DCOLOR_XRGB(0, 0, 255)};
Of course, we could also make an array of vertices like this:
CUSTOMVERTEX OurVertices[] =
{
{320.0f, 50.0f, 1.0f, 1.0f, D3DCOLOR_XRGB(0, 0, 255),},
{520.0f, 400.0f, 1.0f, 1.0f, D3DCOLOR_XRGB(0, 255, 0),},
{120.0f, 400.0f, 1.0f, 1.0f, D3DCOLOR_XRGB(255, 0, 0),},
};
This results in a triangle, which we will see drawn on the screen
shortly.
This is just one example of a Flexible Vertex Format. We will go over how
to build more complex vertex formats later, but this one will do for now.
Now we have acomplished two things. First, we have built an FVF code.
Second, we have constructed a triangle. Now we need to get that triangle ready
for Direct3D to use. To do this, we create what is called a vertex buffer.
A vertex buffer is simply an interface that stores a section in memory (either Video RAM or system memory) to holds information about the vertices/models in your game. We create this interface by using the function CreateVertexBuffer(). The name is self-explanatory.
It's parameters, however, are less merciful. Here is the prototype:
HRESULT CreateVertexBuffer(
UINT Length,
DWORD Usage,
DWORD FVF,
D3DPOOL Pool,
LPDIRECT3DVERTEXBUFFER9 ppVertexBuffer,
HANDLE* pSharedHandle);
Let's take these parameters up one at a time.
This parameter contains the size of the buffer that will be created. We get
this number by multiplying the size of one vertex by the number of vertices that
will be stored in the buffer. For example, a triangle contains three vertices,
so the size of the triangle's buffer is: 3 * sizeof(CUSTOMVERTEX).
Sometimes there are special ways to use vertices which alter the DirectX handles
the vertices. We will not get into these in any great detail in this tutorial.
This parameter can contain flags indicating these special ways. As we won't
be using any yet, we'll just set it to 0 for now.
This is the FVF code we constructed earlier. We just fill it in with CUSTOMFVF.
If we went up to change the FVF code, this part would also change (hence the #define).
This is what tells DirectX what format the vertices are in, so it is important that
what you put here is accurate.
This parameter tells Direct3D where to create the vertex buffer and how. Following
is a table that describes the possible entries for this parameter. For this
tutorial, we will be using the flag D3DPOOL_MANAGED.
[
Table 4.2 - D3DPOOL
Values]
|
Value |
Description |
|
D3DPOOL_DEFAULT |
This flag indicates that the buffer should be created in the most appropriate memory
for what settings and resources are available. This however, imposes some
limits which are not always good for games. |
|
D3DPOOL_MANAGED |
This indicates that the buffer will be located in the video RAM. |
|
D3DPOOL_SYSTEMMEM |
This indicates that the buffer will be located in the system memory. Vertex
buffers located here cannot usually be accessed by the Direct3D Device, but can
be accessed by other, more advanced means. |
|
D3DPOOL_SCRATCH |
This also indicates the buffer will be located in system memory, however, there
is no way for the video RAM to access this. This type is useful for storing graphics
information that is not currently being used (but will be used later), such as graphics
belonging to other maps a player hasn't reached yet, but might in the near future. |
|
[Close Table] |
If you can't decode that, it's the pointer to the vertex buffer interface we're making.
We put the blank pointer in this parameter and the function simply
fills it in for us.
The documentation says this parameter is, and I quote, "Reserved. Set this
parameter to NULL". What this means I can't say for certain, but apparently
Microsoft want us to set it to NULL. So be it. We'll set it to NULL.
And with all that in mind, let's take a look at how this function appears in our
program:
LPDIRECT3DVERTEXBUFFER9 t_buffer;
d3ddev->CreateVertexBuffer(3*sizeof(CUSTOMVERTEX),
0,
CUSTOMFVF,
D3DPOOL_MANAGED,
&t_buffer,
NULL);
Now that you have created your vertex buffer, you need to load the vertices into
it. You do this using a simple call memcpy(). However, before you can
get access to the buffer, you need to lock it.
There are two reasons you need to lock the buffer. First, you need to tell
Direct3D that you need complete control of the memory. In other words, it
shouldn't be handled by any other process that might be going on. Second,
you need to tell the video hardware not to move it around. There is no guarantee
that the Video RAM will stay put. Locking tells the video hardware not to
mess around with the memory while you are working with it.
To lock a buffer, you use the Lock() function, which has four parameters, but is
actually quite simple:
HRESULT Lock(UINT OffsetToLock,
UINT SizeToLock,
VOID** ppbData,
DWORD Flags);
Let's go over these parameters.
If we only wanted to lock part of our vertex buffer, we would indicate that in these
two parameters. The first one indicates how far into the buffer, in bytes,
the lock should start. The second indicates how much should be locked, again
in bytes. We want to lock the entire buffer, so we'll set both of these to
0.
Unless you are at least intermediate with C++ this will probably make no sense to
you. Basically a void* is a pointer that points to no particular type of variable.
For example, a double* points a double, whereas an int* points to an int.
Each type of pointer has it's own format, and so it can't convert to another type
without loss of data.
A void* points to any of these, and can be converted without trouble.
Here we have a pointer to a void*. This pointer gets filled with the location
of the memory to contain our vertices. The vertex buffer interface will take
care of the details of this, but we need the pointer in the next step, so the Lock()
function will fill this pointer with the proper address. Have a look at the
example below to see how we fill this parameter.
This is an advanced parameter, and we won't get into it anywhere in this tutorial.
They basically provide special ways to handle the locked memory. If you are
truly interested, they can be researched in the DirectX documentation. For
now, we will just set it to 0.
Let's fill this function out and see how it looks:
VOID* pVoid; // the void* we were talking about
t_buffer->Lock(0, 0, (void**)&pVoid, 0); // locks t_buffer,
the buffer we made earlier
Next, we use a call to memcpy() to copy the vertices to the vertex buffer.
memcpy(pVoid, OurVertices, sizeof(OurVertices)); // copy vertices to
the vertex buffer
Lastly, we have a very complicated function: Unlock(). This function has no
parameters. What it does is tell Direct3D that we're all done with the memory,
and it doesn't have to be locked anymore. It looks like this:
t_buffer->Unlock(); // unlock t_buffer
Because of the number of commands we've just learned, we're going to stick them
all away into a single function of our own creation: init_graphics().
void init_graphics(void)
{
// create three vertices using the CUSTOMVERTEX struct built earlier
CUSTOMVERTEX t_vert[] =
{
{ 320.0f, 50.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(0, 0, 255),
},
{ 520.0f, 400.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(0, 255,
0), },
{ 120.0f, 400.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(255, 0,
0), },
};
// create the vertex and store the pointer into t_buffer, which is
created globally
d3ddev->CreateVertexBuffer(3*sizeof(CUSTOMVERTEX),
0,
CUSTOMFVF,
D3DPOOL_MANAGED,
&t_buffer,
NULL);
VOID* pVoid; // the void pointer
t_buffer->Lock(0, 0, (void**)&pVoid, 0); // lock
the vertex buffer
memcpy(pVoid, t_vert, sizeof(t_vert)); // copy the vertices
to the locked buffer
t_buffer->Unlock(); // unlock the vertex buffer
return;
}
At this point I would suggest going over this section a couple times to make sure
you thoroughly got it all. This is a rather key part of 3D programming, and
we'll be using it and modifying it throughout the rest of the tutorial.
Now we actually get to have something on the screen! We have three very simple
functions to talk about before this happens though. Each are called from the
Direct3D Device interface. Let's take a look at each one.
The first of these functions is SetFVF(). SetFVF() is a function that tells
Direct3D what FVF code we are using currently. We could, of course, have multiple
FVF codes and use them in two different parts of the 3D scene. Before we draw
anything, we need to tell Direct3D which one we are using. This function is
written out like this:
d3ddev->SetFVF(CUSTOMFVF);
Next we have the function SetStreamSource(), which tells Direct3D which vertex buffer
we are drawing from. This one has a couple parameters, so let's take a look
at the prototype:
HRESULT SetStreamSource(UINT StreamNumber,
LPDIRECT3DVERTEXBUFFER9 pStreamData,
UINT OffsetInBytes,
UINT Stride);
The first parameter is the number of the stream source. We'll get into how
this works later, but for now set it to 0, as we only have one vertex buffer.
The second parameter is the pointer to the vertex buffer we created earlier.
The third parameter is the number of bytes into the vertex buffer we should start
from. This will usually be 0.
The last parameter is the size of each vertex. We fill this with: sizeof(CUSTOMVERTEX).
Let's take a look at the function as it is used:
d3ddev->SetStreamSource(0, t_buffer, 0, sizeof(CUSTOMVERTEX));
Now that we have told Direct3D what kind of vertices we are using and where to get
them from, we tell it to draw the vertices we have built. This function draws
the primitives in the selected vertex buffer to the screen. Here is the prototype:
HRESULT DrawPrimitive(D3DPRIMITIVETYPE PrimitiveType,
UINT
StartVertex,
UINT
PrimitiveCount);
The first parameter is the type of primitive that is used. These were covered
in Lesson 3, but the codes used are here:
[
Table 4.3 - D3DPRIMITIVETYPE
Values]
|
Value |
Description |
|
D3DPT_POINTLIST |
Shows a series of points. |
|
D3DPT_LINELIST |
Shows a series of separated lines. |
|
D3DPT_LINESTRIP |
Shows a series of connected lines. |
|
D3DPT_TRIANGLELIST |
Shows a series of separated triangles. |
|
D3DPT_TRIANGLESTRIP |
Shows a series of connected triangles. |
|
D3DPT_TRIANGLEFAN |
Shows a series of triangles with one shared corner. |
|
[Close Table] |
The second parameter is the number of the first vertex we will put on the screen.
We could, if we wanted, start in the middle of the vertex buffer. However,
we want the whole buffer drawn, so we will put 0 here.
The third and last parameter is the number of primitives we want to draw.
If we draw a triangle, we put one here (there's only one triangle). If we
were using points, we would put 3, as there are three points. Lines would
also be 3.
Now let's take a look at the entire render_frame() function now that we have modified
it.
// this is the function used to render a single frame
void render_frame(void)
{
d3ddev->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0),
1.0f, 0);
d3ddev->BeginScene();
// select which vertex format we are using
d3ddev->SetFVF(CUSTOMFVF);
// select the vertex buffer to display
d3ddev->SetStreamSource(0, t_buffer, 0, sizeof(CUSTOMVERTEX));
// copy the vertex buffer to the back buffer
d3ddev->DrawPrimitive(D3DPT_TRIANGLELIST,
0, 1);
d3ddev->EndScene();
d3ddev->Present(NULL, NULL, NULL, NULL);
return;
}
Before looking at the whole program, let's look at one
last step that is required.
Just like the Direct3D Device and Direct3D itself, a vertex buffer must be released
before our program closes.
// this is the function that cleans up Direct3D and COM
void cleanD3D(void)
{
t_buffer->Release(); // close and release the
vertex buffer
d3ddev->Release(); // close and release the 3D device
d3d->Release(); // close and release Direct3D
return;
}
Now let's take a look at the whole program to see what we have.
Ok, let's see what a triangle looks like. If you've never seen one, this will
be an educational experience. If you have (please) you can see how one is
made in Direct3D.
Anyway, let's examine the final DirectX code. The new parts covered in this
lesson are in bold as usual.
[
Show Code]
// include the basic windows header files and the Direct3D header file
#include <windows.h>
#include <windowsx.h>
#include <d3d9.h>
// define the screen resolution and keyboard macros
#define SCREEN_WIDTH 640
#define SCREEN_HEIGHT 480
#define KEY_DOWN(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 1 : 0)
#define KEY_UP(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 0 : 1)
// include the Direct3D Library file
#pragma comment (lib, "d3d9.lib")
// global declarations
LPDIRECT3D9 d3d; // the pointer to our Direct3D interface
LPDIRECT3DDEVICE9 d3ddev; // the pointer to the device class
LPDIRECT3DVERTEXBUFFER9 t_buffer = NULL;
// the pointer to the vertex buffer
// function prototypes
void initD3D(HWND hWnd); // sets up and initializes Direct3D
void render_frame(void); // renders a single frame
void cleanD3D(void); // closes Direct3D and releases memory
void init_graphics(void); // 3D declarations
struct CUSTOMVERTEX {FLOAT X, Y, Z, RHW; DWORD COLOR;};
#define CUSTOMFVF (D3DFVF_XYZRHW | D3DFVF_DIFFUSE)
// the WindowProc function prototype
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
// the entry point for any Windows program
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
HWND hWnd;
WNDCLASSEX wc;
ZeroMemory(&wc, sizeof(WNDCLASSEX));
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = (WNDPROC)WindowProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
// wc.hbrBackground = (HBRUSH)COLOR_WINDOW; // not
needed any more
wc.lpszClassName = L"WindowClass";
RegisterClassEx(&wc);
hWnd = CreateWindowEx(NULL,
L"WindowClass",
L"Our Direct3D Program",
WS_EX_TOPMOST | WS_POPUP, // fullscreen values
0, 0, // the starting x and y positions should be 0
SCREEN_WIDTH, SCREEN_HEIGHT, // set the window to 640
x 480
NULL,
NULL,
hInstance,
NULL);
ShowWindow(hWnd, nCmdShow);
// set up and initialize Direct3D
initD3D(hWnd);
// enter the main loop:
MSG msg;
while(TRUE)
{
DWORD starting_point = GetTickCount();
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
render_frame();
// check the 'escape' key
if(KEY_DOWN(VK_ESCAPE))
PostMessage(hWnd, WM_DESTROY, 0, 0);
while ((GetTickCount() - starting_point) < 25);
}
// clean up DirectX and COM
cleanD3D();
return msg.wParam;
}
// this is the main message handler for the program
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_DESTROY:
{
PostQuitMessage(0);
return 0;
} break;
}
return DefWindowProc (hWnd, message, wParam, lParam);
}
// this function initializes and prepares Direct3D for use
void initD3D(HWND hWnd)
{
d3d = Direct3DCreate9(D3D_SDK_VERSION);
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.Windowed = FALSE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.hDeviceWindow = hWnd;
d3dpp.BackBufferFormat = D3DFMT_X8R8G8B8;
d3dpp.BackBufferWidth = SCREEN_WIDTH;
d3dpp.BackBufferHeight = SCREEN_HEIGHT;
// create a device class using this information and the info from
the d3dpp stuct
d3d->CreateDevice(D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp,
&d3ddev);
init_graphics(); // call the function to initialize
the triangle
return;
}
// this is the function used to render a single frame
void render_frame(void)
{
d3ddev->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0),
1.0f, 0);
d3ddev->BeginScene();
// select which vertex format we are using
d3ddev->SetFVF(CUSTOMFVF);
// select the vertex buffer to display
d3ddev->SetStreamSource(0, t_buffer, 0, sizeof(CUSTOMVERTEX));
// copy the vertex buffer to the back buffer
d3ddev->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
d3ddev->EndScene();
d3ddev->Present(NULL, NULL, NULL, NULL);
return;
}
// this is the function that cleans up Direct3D and COM
void cleanD3D(void)
{
t_buffer->Release();
// close and release the vertex buffer
d3ddev->Release(); // close and release the 3D device
d3d->Release(); // close and release Direct3D
return;
}
// this is the function that puts the 3D models into video RAM
void init_graphics(void)
{
// create the vertices using the CUSTOMVERTEX struct
CUSTOMVERTEX t_vert[] =
{
{ 320.0f, 50.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(0, 0, 255),
},
{ 520.0f, 400.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(0, 255,
0), },
{ 120.0f, 400.0f, 0.5f, 1.0f, D3DCOLOR_XRGB(255, 0,
0), },
};
// create a vertex buffer interface called t_buffer
d3ddev->CreateVertexBuffer(3*sizeof(CUSTOMVERTEX),
0,
CUSTOMFVF,
D3DPOOL_MANAGED,
&t_buffer,
NULL);
VOID* pVoid; // a void pointer
// lock t_buffer and load the vertices into it
t_buffer->Lock(0, 0, (void**)&pVoid, 0);
memcpy(pVoid, t_vert, sizeof(t_vert));
t_buffer->Unlock();
return;
}
Go ahead and update your program and let's see what we get. If you run this,
you should see the following on your screen:

Image 4.3 - The Drawn Triangle
Well done! You have made DirectX actually draw something. Of course,
there is a lot more we can do, but let's start with what we have. I'd recommend
doing the following short exercises before moving on, just so you get familiarity
with the program.
1. Change the colors of the triangle.
2. Get the triangle to change shape during runtime.
3. Get the colors to fade from one point to the next.
Of course, you may be disappointed to find that this triangle is not 3D yet, but
let's go on to the next lesson and make it 3D by rotating it, resizing it and moving
it in a 3D world.
Next Lesson: Transforming Vertices
GO! GO! GO!
© 2006-2009 DirectXTutorial.com. All Rights Reserved.