Experiments In Game Programming
Main
 Home
XNA - C#
 Coming Soon!
 Level Editing - GTK
DirectX 9 - C++
 Downloads
 Disclaimer
 Introduction
Part 1 - DirectX
 1 - Breakout
 2 - Create DX
 3 - 2d Images
 4 - 3d Models
 5 - Cameras & Lights
 6 - Animation Timing
 7 - Keyboard/Mouse
 8 - Sound
Part 2 - Breakout
 1 - Art and Sounds
 2 - The Menu
 3 - Starting Breakout
 4 - The Level
 5 - The Paddle
 6 - The Ball
 7 - Finishing Touches

Untitled Document

Chapter 4

 

Drawing in 3d

 

 

At the core of DirectX is Direct3d. Direct3d has an overwhelming number of functions, procedures, properties and constants. Most books and tutorials start covering direct3d by addressing the very basics, drawing points, and lines and triangles. Then creating vertex buffers to draw lists of points and triangles, etc. However, when I started programming direct3d, I wanted to load and draw a model, not single points or triangles. I am not down playing the importance of learning to use the basics, after all a model is just a list of points and triangles. We are going to start with the model.

Topics Covered in this Chapter:
  • Creating our 3d Model Class
  • Setting up A Simple View and Light
  • Rendering A 3d Model with our Model Class

DirectX is kind enough to provide us with a model format that it is capable of loading without us having to write our own. The X format is the directX model format, and there are many export plugins available for popular 3d modeling programs. In this chapter we are going to write a class that uses the built in functionality to load an X format model, and then render it to our display. Because of the built in functionality setting up a model class is only going to be slightly more complicated than the 2d surface class we created in the last chapter. Setting up to display our model however does take a little more work, and will not be covered in detail until Chapter 5.

Let's take a look at the model class from the chapter 4 code. The first function we come to after the constructor/deconstructor is loadModel. loadModel takes 2 parameters: the device, and the filename of the model to load. The first thing it does is set a default scale, position, and rotation vector. This holds respectively the x,y,z scale, position, and rotation of the model. To load the model we call D3DXLoadMeshFromX. D3DXLoadMeshFromX takes 8 parameters, you can see what they are by looking at the source below. Important to us are the filename, the materialBuffer and the materialCount. Often models are made up of many components, components that have different color and texture data. The materialBuffer is loaded with the material information for each component, and the materialCount returns us a count of how many materials / components the model has. Once we have loaded our model, we need to store the material information for each component, because we need it to render the model. We do this by creating 2 arrays to store our material and texture information. Then we loop through each material, and save the material into the material array. If there is also a texture file associated with the material D3DXCreateTextureFromFile is called to load the texture file, and we save the loaded texture into the texture array. When this is all finished, we can release the materialBuffer because it is no longer needed.
bool Model::loadModel(LPDIRECT3DDEVICE9 device, std::string filename){
     HRESULT hr;
     mesh = NULL;
     scale = D3DXVECTOR3(1.0f, 1.0f , 1.0f); //set our scale variable to 1
     position = D3DXVECTOR3(0.0f, 0.0f , 0.0f); //set the position to 0,0,0
     rotation = D3DXVECTOR3(0.0f, 0.0f , 0.0f); //set the rotation, to nothing.
    
     //load the model, and its material information
     hr = D3DXLoadMeshFromX(filename.c_str(), //the file we wish to load
     D3DXMESH_SYSTEMMEM, //Location to store our model
     device, //The d3d Device
     NULL,
     &materialBuffer, //a pointer to the materials loaded by the mesh
     NULL,
     &materialCount, //number of material sets
     &mesh);

     if FAILED(hr){
         return false;
     }

     //get the pointer to the material buffer
     D3DXMATERIAL* modelMaterials = (D3DXMATERIAL*)materialBuffer->GetBufferPointer();
    
     //Create arrays to hold the textures and materials
     materials = new D3DMATERIAL9[materialCount];
     textures = new LPDIRECT3DTEXTURE9[materialCount];
    
     // for each of our materials load the material and texture into our arrays.
     for(DWORD i = 0; i < materialCount; i++)
     {
        
         //store the material in our array
         materials[i] = modelMaterials[i].MatD3D;

         // Load the texture
         hr = D3DXCreateTextureFromFile( device, modelMaterials[i].pTextureFilename,          &textures[i] );
         // If there is no texture, set to NULL
         if FAILED(hr){
             textures[i] = NULL;
         }
     }

     // Delete the material buffer
     if (materialBuffer){
         materialBuffer->Release();
     }

     return true;
}

The next function is render. Render only requires the device as a parameter, and as you would expect handles rendering the model onto the backbuffer. Before it can render, however, it needs to set the position, the scale, and the rotation of the object. This information is stored in matrices, directx provides us with functions to create each of the translation, scale, and rotation matrices. They are D3DXMatrixTranslation, D3DXMatrixScaling, and D3DXMatrixRotationYawPitchRoll respectively. Once we have created our matrices we multiply them together with D3DXMatrixMultiply to create one matrix that contains all of our models translation information. We then call SetTransform, with D3DTS_WORLD as the first parameter, and our complete translation information as the second. This positions our model in world space for rendering. Now that the model positioning is complete, we loop through each of our model components, set the material, and or texture associated with that component, and draw the component subset.
void Model::render(LPDIRECT3DDEVICE9 device)
{
     DWORD i;
     //set the scale matrix
     D3DXMatrixScaling(&scaleMatrix, scale.x,scale.y,scale.z);

     // store the position information into the translation matrix
     D3DXMatrixTranslation(&transMatrix, position.x, position.y, position.z);

     // set the rotation matrix
     D3DXMatrixRotationYawPitchRoll(&rotationMatrix,rotation.x,rotation.y,rotation.z);

     // Multiply the translation matrix by the rotation matrix, store the result      in the translation matrix
     D3DXMatrixMultiply(&transMatrix, &rotationMatrix, &transMatrix);
     // Multiply the translation matrix by the scale matrix, store the result in      the translation matrix
     D3DXMatrixMultiply(&transMatrix, &scaleMatrix, &transMatrix);
    
     // Position the object for rendering based on the position, rotation and scaleing.
     device->SetTransform(D3DTS_WORLD, &transMatrix);

     //render the object
     for(i = 0; i < materialCount; i++ )
     {
         // Set the material
         device->SetMaterial( &materials[i] );
         // Set the texture if we have one
         if (textures[i]!= NULL){
             device->SetTexture( 0, textures[i] );
         }
         // Draw the subset
         mesh->DrawSubset( i );
     }
}

There are 4 relatively simple functions left in the model class, setPosition, changePosition, setScale, and setRotation. Each function takes as a parameter, a D3DXVECTOR3 which is a vector with 3 components, x, y, and z. setPosition sets the position of the model in world space, changePosition changes the position relative to the model's current position, setScale sets the x, y, and z scale of the model, and setRotation sets the x,y, and z rotation of the model (rotation is in radians).

Now we have a model class, however, we are not quite ready to draw a model onto the screen. First we need to setup a view, to tell directx what part of the world space to render. We also need to create a light, otherwise our model will render entirely black. Setting up lights and cameras is the topic of the next chapter, but for now we are going to add a function called quickAndDrityViewSetup to our dxMgr class. This will setup a simple view and light so we can test our model class.

Below is the quickAndDrityViewSetup added to the dxMgr class, the details will be covered in the next chapter, for now you can read the comments and get a good idea of what's going on.
void dxMgr::quickAndDrityViewSetup(){
     D3DXMATRIX viewMatrix; //the view matrix
     D3DXMATRIX projectionMatrix; // the projection matrix
     D3DXVECTOR3 position = D3DXVECTOR3(0.0f, 0.0f, 30.0f); //the position of our      camera
     D3DXVECTOR3 target = D3DXVECTOR3(0.0f, 0.0f, 0.0f); //the lookat target of      our camera

     float aspect = 1.333f; // the aspect ratio of the screen
     float nearClip = 1.0f; //nearest point at which the objects stop rendering     
     float farClip = 1000.0f; //farthest point at which the objects stop rendering     

     //Setup the Projection Matrix
     D3DXMatrixPerspectiveFovLH(&projectionMatrix, D3DX_PI / 4.0f, aspect,      nearClip, farClip);
     //Set the Projection
     pd3dDevice->SetTransform(D3DTS_PROJECTION, &projectionMatrix);

     //Setup the View
     D3DXMatrixLookAtLH(&viewMatrix,
     &position, //camera's position
     &target, //camera's target
     &D3DXVECTOR3(0.0f, 1.0f, 0.0f));// the up direction
     //Set the View
     pd3dDevice->SetTransform(D3DTS_VIEW, &viewMatrix);
    
     //Turn on lighting
     pd3dDevice->SetRenderState(D3DRS_LIGHTING, TRUE);

     //Create a new Light
     D3DLIGHT9 light;
     light.Type = D3DLIGHT_POINT; //point light, lights in every direction.
     light.Diffuse.r = light.Diffuse.g = light.Diffuse.b = 1.0f;
     light.Specular.r = light.Specular.g = light.Specular.b = 0.0f;
     light.Ambient.r = light.Ambient.g = light.Ambient.b = 0.3f;
     light.Position = D3DXVECTOR3( 0.0f, 10.0f, 25.0f );
     light.Attenuation0 = light.Attenuation1 = light.Attenuation2 = 0.0f;
     light.Range = 60.0f;

     //Set the light
     pd3dDevice->SetLight(0, &light );
     //Turn on the light
     pd3dDevice->LightEnable(0, TRUE );

}

Now that we have a way to create a view and setup some light, we can test our model class. So let's add a model test to the GameMain.

First, we add the Model class to the header, and create a variable to store our test model.
#pragma once
class dxMgr;
class dxText;
class Surface;
class Model;
.....

private:

dxMgr* dxManager;
dxText* textManager;
Surface* testSurface;
Model* testModel;
};


Next we add calls to create the model, and load sphere.x (our test model) to the init function. As well as a call to our quick and dirty view setup.
bool GameMain::init(HWND wndHandle)
{
     ...

     //for testing purposes create a new 3d model
    
     testModel = new Model();
     testModel->loadModel(dxManager->getD3DDevice(),"sphere.x");
     if (!testModel){
         return false;
     }
    
     dxManager->quickAndDrityViewSetup();


     return true;
}

Lastly, we add a render call in GameMains render loop. I have added 2 sets of calls with examples of the position and scale commands. 
void GameMain::update(void)
{
     //call our update function
     // begin rendering
     dxManager->beginRender();
     //game render calls go here

     ....

     testModel->setScale(D3DXVECTOR3(1.0f, 1.0f , 1.0f));
     testModel->setPosition(D3DXVECTOR3(5.0f, 0.0f , 0.0f));
     testModel->render(dxManager->getD3DDevice());


     testModel->setScale(D3DXVECTOR3(1.0f, 2.0f , 1.0f));
     testModel->setPosition(D3DXVECTOR3(-5.0f, 0.0f , 0.0f));
     testModel->render(dxManager->getD3DDevice());

    
     //end render
     dxManager->endRender();

}

That's it, compile and run the project. The output should now include 2 spheres one stretched funny in the y direction.



Summing Up - Chapter 4

In this chapter we created a 3d Model class for loading, and rendering 3d models in the X file format. The model class will load materials and texture files associated with the model. We had to create a test view before we could view our model because direct3d needs to know what area of world space to render. Before moving on, play with loading your own models if you have the tools to make them. Milkshape 3d has an X file export and is available free on the internet. You could also try out a trial version of 3d Studio Max, in combination with the panda directx exporter plugin (also available free). If your model doesn't show up try shrinking the scale, often your models are larger than you think.
 ©2008 David Whittaker