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