Game Programming Tutorials
dogonfire.dk Index Tutorials Hardwater Forum Register FAQ

 Rotating Cube 

Introduction

In our last tutorial we drew a 2D triangle. Thats all nice and good, but this time we are going to create our very first 3D object... a cube! And to show that it really is 3D, we are going to rotate it about the x, y and z axis. Remember that all 3D objects are made up from polygons, normally triangles. So for our cube, we use 12 triangles (2 for each face).

3D World Space

In the last tutorial we saw how the Left-Handed 3D Coordinate System worked. This is the coordinate system that we will use from now on. We will specify in our code (below) that the y axis points up.

What is the difference between left and right-handed coordinate systems? In left-handed coordinates, the positive z axis points away from you. In right-handed coordinates, the positive z axis points towards you. In both cases the positive y axis points up and the positive x axis points to the right. You can remember this by holding up your left hand so that the palm is facing up (y axis) and your fingers point to the right (x axis). Your thumb represents the positive z axis (points away). If you did the same with your right hand, your thumb would point towards you, hence left and right-handed coordinate systems. We will always use the left-handed system, this is what DirectX uses.

Backface Culling

What is Backface Culling? Backface Culling is a pretty simple concept. Basically, it is a process where all of the polygons that are "facing" away from the user are not rendered. For example, I have created a square with one side red and the other blue. Let's say that I defined the polygons so that the red side was "facing" the user and then started rotating the square. With Backface Culling enabled, the user would only see the red face and would never see the blue face. Why is this useful? Well, if we are creating a closed 3D object (like a cube), we do not need to render the inside faces because they are never seen anyway. This makes the rendering of a cube more efficient.
How do I specify which face is "facing" the user and which face to cull (not render)? It's all in the order that you specify your vertices. Below are two diagrams showing the order in which to define a "Clockwise" polygon. If you created a polygon in this way, the polygon would be rendered as shown, but if you were to flip the polygon (rotate), it would not be rendered. You can define which faces are culled, clockwise or anti-clockwise. By default, DirectX will cull anti-clockwise polygons.



How to make a cube

Below are two diagrams showing how our cube is going to be made up. Here we use three triangle strips, one for the top of the cube, one for the sides and one for the bottom. The diagram below shows the vertices and polygons for each triangle strip (Fig 3.3). The vertices are numbered from 0 to 17, this is the order that we must specify our vertices in the vertex buffer. Under that is a diagram that shows our cube (Fig 3.4). Notice where each of the vertices are in relation to each other. Also, look at how the vertices are always in a clockwise direction (except the bottom). This is because we have enabled "Backface Culling" (see above).




Matrices

What is a Matrix? First of all, a matrix is NOT guys in shades running around in black clothes! Matrices are a quite advanced mathematics topic, but for our use I will only give you a brief introduction. A matrix can be thought of as a grid of numbers that can be applied to the coordinates of a vertex to change their values. In short, matrices are used to transform points/vectors in different ways. For example, you can use matrices in DirectX to translate, rotate and scale objects (vertices). In 3D terms, a matrix is a 4x4 grid of numbers. There are three basic types of matrices: World, View and Projection Matrix

World Matrix

You use the world matrix to rotate, scale and translate objects in 3D space (World Space) by transforming their vertices. All of these transformations will be performed about the origin (0, 0, 0). You can combine transformations by multiplying them together, but be aware that it is important which order you perform the multiplication. Matrix1 x Matrix2 is not the same as Matrix2 x Matrix1. When you perform a world matrix transformation, all subsequent vertices will be transformed by this matrix. To rotate two objects, one about the x axis and one about the y axis, you must perform the x axis transformation first, then render object 1. Next, perform the y axis transformation, then render object 2.

View Matrix

The view matrix defines the camera (or eye). The camera has a position in world space and also has a "look at" position. For example, you can place the camera above an object (camera position) and point it at the centre of the object (look at position). You can also specify which way is up, in the example below we will specify that the positive y axis is up.

Projection Matrix

The projection matrix can be thought of defining the camera lens. It specifies the field of view angle, aspect ratio and near/far clipping planes. For the time being at least, we will keep these settings the same throughout our examples.

Here is the code for this tutorial. It is the same as the code from the last tutorial, with some new modifications:

#include <d3dx9.h>
#pragma comment(lib,"d3d9.lib")
#pragma comment(lib,"d3dx9.lib")
#pragma comment(lib,"winmm.lib")

LPDIRECT3D9 g_pD3D = NULL;
LPDIRECT3DDEVICE9 g_pD3DDevice = NULL;
LPDIRECT3DVERTEXBUFFER9 g_pVertexBuffer = NULL; // Buffer to hold vertices

struct MYVERTEX
{
 FLOAT x, y, z;
 DWORD color;
};

#define D3DFVF_MYVERTEX ( D3DFVF_XYZ | D3DFVF_DIFFUSE )


#define SafeRelease(pObject) if(pObject != NULL) {pObject->Release(); pObject=NULL;}

HRESULT InitDX(HWND hWnd)
{
 // First of all, create the main D3D object. If it is created successfully we
 // should get a pointer to an IDirect3D8 interface.
 g_pD3D = Direct3DCreate9(D3D_SDK_VERSION);
 if(g_pD3D == NULL)
 {
  return E_FAIL;
 }

 // Get the current display mode
 D3DDISPLAYMODE d3ddm;
 if(FAILED(g_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm)))
 {
  return E_FAIL;
 }

 // Create a structure to hold the settings for our device
 D3DPRESENT_PARAMETERS d3dpp;
 ZeroMemory(&d3dpp, sizeof(d3dpp));

 // Fill the structure.
 // We want our program to be windowed, and set the back buffer to a format
 // that matches our current display mode
 d3dpp.Windowed = TRUE;
 d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
 d3dpp.BackBufferFormat = d3ddm.Format;

 // Create a Direct3D device.
 if(FAILED(g_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
                                   D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &g_pD3DDevice)))
 {
  return E_FAIL;
 }
    
 // Turn on back face culling. This is becuase we want to hide the back of our polygons
 g_pD3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);

 // Turn off lighting becuase we are specifying that our vertices have colour
 g_pD3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE);

 return S_OK;
}

HRESULT InitVertexBuffer()
{
 VOID* pVertices;
    
 // Store each point of the cube together with it's colour
 // Make sure that the points of a polygon are specified in a clockwise direction,
 // this is because anti-clockwise faces will be culled
 // We will use a three triangle strips to render these polygons (Top, Sides, Bottom).
 MYVERTEX Vertices[] =
 {
  // Top Face
  {-5.0f, 5.0f, -5.0f, D3DCOLOR_XRGB(0, 0, 255),},  // Vertex 0 - Blue
  {-5.0f, 5.0f, 5.0f, D3DCOLOR_XRGB(255, 0, 0),},   // Vertex 1 - Red
  {5.0f, 5.0f, -5.0f, D3DCOLOR_XRGB(255, 0, 0),},   // Vertex 2 - Red
  {5.0f, 5.0f, 5.0f, D3DCOLOR_XRGB(0, 255, 0),},    // Vertex 3 - Green

  // Face 1
  {-5.0f, -5.0f, -5.0f, D3DCOLOR_XRGB(255, 0, 0),}, // Vertex 4 - Red
  {-5.0f, 5.0f, -5.0f, D3DCOLOR_XRGB(0, 0, 255),},  // Vertex 5 - Blue
  {5.0f, -5.0f, -5.0f, D3DCOLOR_XRGB(0, 255, 0),},  // Vertex 6 - Green
  {5.0f, 5.0f, -5.0f, D3DCOLOR_XRGB(255, 0, 0),},   // Vertex 7 - Red

  // Face 2
  {5.0f, -5.0f, 5.0f, D3DCOLOR_XRGB(0, 0, 255),},   // Vertex 8 - Blue
  {5.0f, 5.0f, 5.0f, D3DCOLOR_XRGB(0, 255, 0),},    // Vertex 9 - Green
        
  // Face 3
  {-5.0f, -5.0f, 5.0f, D3DCOLOR_XRGB(0, 255, 0),}, //Vertex 10 - Green
  {-5.0f, 5.0f, 5.0f, D3DCOLOR_XRGB(255, 0, 0),}, //Vertex 11 - Red

  // Face 4
  {-5.0f, -5.0f, -5.0f, D3DCOLOR_XRGB(255, 0, 0),}, //Vertex 12 - Red
  {-5.0f, 5.0f, -5.0f, D3DCOLOR_XRGB(0, 0, 255),}, //Vertex 13 - Blue

  // Bottom Face
  {5.0f, -5.0f, -5.0f, D3DCOLOR_XRGB(0, 255, 0),}, //Vertex 14 - Green
  {5.0f, -5.0f, 5.0f, D3DCOLOR_XRGB(0, 0, 255),}, //Vertex 15 - Blue
  {-5.0f, -5.0f, -5.0f, D3DCOLOR_XRGB(255, 0, 0),}, //Vertex 16 - Red
  {-5.0f, -5.0f, 5.0f, D3DCOLOR_XRGB(0, 255, 0),}, //Vertex 17 - Green
 };

 // Create the vertex buffer from our device.
 if(FAILED(g_pD3DDevice->CreateVertexBuffer(18 * sizeof(MYVERTEX),
                                               0, D3DFVF_MYVERTEX,
                                               D3DPOOL_DEFAULT, &g_pVertexBuffer, NULL)))
 {
  return E_FAIL;
 }

 // Get a pointer to the vertex buffer vertices and lock the vertex buffer
 if(FAILED(g_pVertexBuffer->Lock(0, sizeof(Vertices), &pVertices, 0)))
 {
  return E_FAIL;
 }

 // Copy our stored vertices values into the vertex buffer
 memcpy(pVertices, Vertices, sizeof(Vertices));

 // Unlock the vertex buffer
 g_pVertexBuffer->Unlock();

 return S_OK;
}


void SetRotation()
{
 // Here we will rotate our world around the x, y and z axis.
 D3DXMATRIX matWorld, matWorldX, matWorldY, matWorldZ;
    
 // Create the transformation matrices
 D3DXMatrixRotationX(&matWorldX, timeGetTime()/400.0f);
 D3DXMatrixRotationY(&matWorldY, timeGetTime()/400.0f);
 D3DXMatrixRotationZ(&matWorldZ, timeGetTime()/400.0f);

 // Combine the transformations by multiplying them together
 D3DXMatrixMultiply(&matWorld, &matWorldX, &matWorldY);
 D3DXMatrixMultiply(&matWorld, &matWorld, &matWorldZ);

 // Apply the tansformation
 g_pD3DDevice->SetTransform(D3DTS_WORLD, &matWorld);
}

void SetCamera()
{
 // Here we will setup the camera.
 // The camera has three settings: "Camera Position", "Look at Position" and "Up Direction"
 // We have set the following:
 // Camera Position: (0, 0, -30)
 // Look at Position: (0, 0, 0)
 // Up direction: Y-Axis.
 D3DXMATRIX matView;
 D3DXMatrixLookAtLH(&matView, &D3DXVECTOR3(0.0f, 0.0f,-30.0f), //Camera Position
                              &D3DXVECTOR3(0.0f, 0.0f, 0.0f),  //Look At Position
                              &D3DXVECTOR3(0.0f, 1.0f, 0.0f)); //Up Direction

 g_pD3DDevice->SetTransform(D3DTS_VIEW, &matView);
}

void SetPerspective()
{
 // Here we specify the field of view, aspect ration and near and far clipping planes.
 D3DXMATRIX matProj;
 D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI/4, 1.0f, 1.0f, 500.0f);
 g_pD3DDevice->SetTransform(D3DTS_PROJECTION, &matProj);
}


void Render()
{
 if(g_pD3DDevice == NULL)
 {
  return;
 }

 // Clear the backbuffer to black
 g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
    
 // Begin the scene
 g_pD3DDevice->BeginScene();
    
 // Setup the rotation, camera, and perspective matrices
 SetRotation();
 SetCamera();
 SetPerspective();

 // Rendering our objects
 g_pD3DDevice->SetStreamSource(0, g_pVertexBuffer, 0, sizeof(MYVERTEX));
 g_pD3DDevice->SetFVF(D3DFVF_MYVERTEX);
 g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);  // Top
 g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 4, 8);  // Sides
 g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 14, 2); // Bottom

 // End the scene
 g_pD3DDevice->EndScene();
    
 // Flip the back and front buffers so that whatever has been rendered on the back buffer
 // will now be visible on screen (front buffer).
 g_pD3DDevice->Present(NULL, NULL, NULL, NULL);
}

void CleanUp()
{
 SafeRelease(g_pVertexBuffer);
 SafeRelease(g_pD3DDevice);
 SafeRelease(g_pD3D);
}

void GameLoop()
{
 MSG msg;
 BOOL fMessage;

 PeekMessage(&msg, NULL, 0U, 0U, PM_NOREMOVE);
    
 while(msg.message != WM_QUIT)
 {
  // Check if any messages
  fMessage = PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE);

  if(fMessage)
  {
   // Process message
   TranslateMessage(&msg);
   DispatchMessage(&msg);
  }
  else
  {
   // No message to process, so render the current scene
   Render();
  }
 }
}

// The windows message handler
LRESULT WINAPI WinProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
 switch(msg)
 {
  case WM_DESTROY:
         PostQuitMessage(0);
       return 0;
       break;
 
  case WM_KEYUP:
       switch (wParam)
       {
        case VK_ESCAPE:
        // User has pressed the escape key, so quit
        DestroyWindow(hWnd);
        return 0;
        break;
       }
       break;
 }

 return DefWindowProc(hWnd, msg, wParam, lParam);
}

// Application entry point
INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR, INT)
{
 // Register the window class
 WNDCLASSEX wc = {sizeof(WNDCLASSEX), CS_CLASSDC, WinProc, 0L, 0L,
                     GetModuleHandle(NULL), NULL, NULL, NULL, NULL,
                     "Tutorial 3", NULL};
 RegisterClassEx(&wc);

 // Create the application's window
 HWND hWnd = CreateWindow("Tutorial 3", "www.dogonfire.dk :: Tutorial 3",
                           WS_OVERLAPPEDWINDOW, 50, 50, 500, 500,
                           GetDesktopWindow(), NULL, wc.hInstance, NULL);

 // Initialize Direct3D
 if(SUCCEEDED(InitDX(hWnd)))
 {
  // Show our window
  ShowWindow(hWnd, SW_SHOWDEFAULT);
  UpdateWindow(hWnd);

  // Initialize Vertex Buffer
  if(SUCCEEDED(InitVertexBuffer()))
  {
   // Start game running: Enter the game loop
   GameLoop();
  }
 }
    
 CleanUp();

 UnregisterClass("Tutorial 3", wc.hInstance);
    
 return 0;
}

You should finish up with a window with a black background and a multi-coloured cube rotating in the middle of the screen as shown below.



So, what have we added/changed:

Include and lib files

We have a new header file, which replaces the old one: "d3dx9.h"
We also have two new lib files to link with in our project. They are: "d3dx9.lib" and "winmm.lib".

CUSTOMVERTEX

We have changed our custom vertex to only include x, y, z and colour values. These will allow us to specify a point in 3D space and a colour.

D3DFVF_CUSTOMVERTEX

To match our custom vertex structure, we have modified our FVF. Here we use the flags D3DFVF_XYZ and D3DFVF_DIFFUSE.

InitDX

We want to enable Backface Culling as explained above, so we use the SetRenderState function to do this. Notice that we use the D3DCULL_CCW flag to specify that we want DirectX to cull the anti-clockwise faces.
We also use the SetRenderState function to specify that we want to disable lighting, this is because we have given a color value to each of our vertices.

InitVertexBuffer

Here we have set the values for our 18 vertices. Each vertex has a comment next to it with it's number, these numbers match the diagrams above (Fig 3.3 and 3.4). The cube is centred about the origin (0, 0, 0) and is 10 units wide, high and deep.

SetRotation

SetRotation is a new function. Here we call the functions D3DXMatrixRotationX, D3DXMatrixRotationY and D3DXMatrixRotationZ to generate rotation matrices and store them in three D3DXMATRIX structures. Next, we multiply the three matrices together to form one world matrix, we call the SetTransform function to apply the transformation to our vertices.
(Dont worry too much about the details behind matrix operations for now, I will explain that more deeply in Tutorial 5.)

SetCamera

SetCamera is a new function. Here we set the camera position and rotation. We set it's position in 3D space to be (0, 0, -30) and point it at the origin (0,0,0). We center our cube around the origin. We also specify that the y axis points up. We use the D3DXMatrixLookAtLH to generate the view matrix and then use the SetTransform function to apply the transformation.

SetPerspective

SetPerspective is another new function. Here we set the camera's lens. We have decided to have a field of view of 90 degress or PI/4 radians (normal human field of view) and an aspect ratio of 1. We have decided to set the near clipping path to 1, this means that polygons closer than one unit to the camera will be cropped. We have also decided to set the far clipping path to 500, this means that polygons more than 500 units away will be cropped.

Render

In the Render function, we call the three new functions SetupRotation, SetupCamera and SetupPerspective. These functions are called before we render the polygons.
We render the polygons by using three triangle strips, one for the top, one for the sides and one for the bottom.

Summary

So that's it. In this tutorial we learnt about Backface Culling, Matrices, 3D World Space and how a cube is made up using triangle polygons. In the next tutorial, we will rearrange our code into classes and make our program render in full screen mode.

Tutorial 3 Source Code
  Presented by DogOnFire Interactive Updated 2005