DirectXTutorial.com
The Ultimate DirectX Tutorial
Sign In
Lesson 4: Drawing a Triangle
Lesson Overview

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.

Flexible Vertex Formats

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.

A Vertex Format Containing All Possible Data

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:

A Flexible Vertex Format Goes Faster

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.

FVF Codes

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).

FlagDescriptionTypes Included
D3DFVF_XYZIndicates 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_XYZRHWIndicates 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_DIFFUSEIndicates that the vertex format contains a 32-bit color code for a vertex, used for the color of diffuse lighting.DWORD
D3DFVF_SPECULARIndicates 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

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.

Creating Vertices

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.

Vertex Buffers

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.

UINT Length,

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).

DWORD Usage,

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.

DWORD FVF,

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.

D3DPOOL Pool,

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.

ValueDescription
D3DPOOL_DEFAULTThis 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_MANAGEDThis indicates that the buffer will be located in the video RAM.
D3DPOOL_SYSTEMMEMThis 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_SCRATCHThis 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.

LPDIRECT3DVERTEXBUFFER9 ppVertexBuffer,

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.

HANDLE* pSharedHandle

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 v_buffer;

d3ddev->CreateVertexBuffer(3*sizeof(CUSTOMVERTEX),
                           0,
                           CUSTOMFVF,
                           D3DPOOL_MANAGED,
                           &v_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.

UINT OffsetToLock, UINT SizeToLock,

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.

VOID** ppbData,

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.

DWORD Flags

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

v_buffer->Lock(0, 0, (void**)&pVoid, 0);    // locks v_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:

v_buffer->Unlock();    // unlock v_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 vertices[] =
    {
        { 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 v_buffer, which is created globally
    d3ddev->CreateVertexBuffer(3*sizeof(CUSTOMVERTEX),
                               0,
                               CUSTOMFVF,
                               D3DPOOL_MANAGED,
                               &v_buffer,
                               NULL);

    VOID* pVoid;    // the void pointer

    v_buffer->Lock(0, 0, (void**)&pVoid, 0);    // lock the vertex buffer
    memcpy(pVoid, vertices, sizeof(vertices));    // copy the vertices to the locked buffer
    v_buffer->Unlock();    // unlock the vertex buffer
}

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.

Drawing the Primitive

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.

SetFVF()

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);

SetStreamSource()

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, v_buffer, 0, sizeof(CUSTOMVERTEX));

DrawPrimitive()

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:

ValueDescription
D3DPT_POINTLISTShows a series of points.
D3DPT_LINELISTShows a series of separated lines.
D3DPT_LINESTRIPShows a series of connected lines.
D3DPT_TRIANGLELISTShows a series of separated triangles.
D3DPT_TRIANGLESTRIPShows a series of connected triangles.
D3DPT_TRIANGLEFANShows a series of triangles with one shared corner.

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, v_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);
}

Before looking at the whole program, let's look at one last step that is required.

Releasing Vertex Buffers

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)
{
    v_buffer->Release();    // close and release the vertex buffer
    d3ddev->Release();    // close and release the 3D device
    d3d->Release();    // close and release Direct3D
}

Now let's take a look at the whole program to see what we have.

The Finished Program

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.

[Main.cpp]     

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:

The Drawn Triangle

The Drawn Triangle
Transforming Vertices

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!