Navigation: DirectX 9.0

Tutorial 3 - Loading an .X File


Overview

Getting a 3D object rendered to the screen for the first time is an exciting time. I hope. DirectX 9 supports the loading of .x files, which are files that store geometric data, vertices, texture coordinates and other data related to 3 Dimensional models. .X files can store animation data as well as static non-moving meshes so you can load animated characters into an application. I have written a detailed article on animated characters for your interest but I have also supplied a single class to handle this for you if you are not interested in the details of the workings of bones and skin in code and just want to get a character into your game. This tutorial is about loading a simple cube from an x file. Also once you can load a cube you can load any other .x file that contains a static mesh such as a castle or a house. I'll give you a simple chair(the kind you sit on I mean) that you can load so you can simply call .Load(L"Chair.x") instead of .Load(L"Cube.x") to see how to load 3d models of different kinds.

The Toolkit

The toolkit I have put together is a set of .h and .cpp files that define objects such as Camera, Mesh and Sprite. If you have been able to run the tutorial programs from previous tutorials, there is nothing extra you need to be able to use the toolkit; Only the toolkit itself, which you can download here - Download Toolkit.

ID3DXEffect

The lighting of a 3D scene can be done by setting device render states like m_d3dDevice->SetRenderState(D3DRS_LIGHTING, true); or lighting can be done using an HLSL effect file. HLSL stands for High Level Shading Language. Device render states can also be set in an HLSL file but for these tutorials we will not use render states for lighting. The basic idea behind an effect file is that we pass certain information to the HLSL effect created by the file like vertex positions(input), the effect does some calculations we have defined in the .fx file, and then outputs pixels to the back buffer(output). In this way we have a good level of control through HLSL to decide what pixels get written to the screen. We can perform Lighting for example using HLSL.

I won't be going into great detail about the HLSL language for these tutorials because we will be using an existing .fx file. All you really need to know at this point is that we compile an .fx file to get an effect instance and communicate with the effect using the members of ID3DXEffect.

You will need to add the following file to your existing project that we created in the previous tutorial (See Figure 4.):

HLSL1.fx - Right Click, Save Link As. or right click Save Target As.


Figure 4.



Ok, so it is time to declare an ID3DXEffect pointer variable. An ID3DXEffect object is also a COM(Component Object Model) object like the IDirect3DDevice9 object we have been using. You use it in the same way like other pointers to DirectX objects. Let's declare our effect pointer variable as follows:

class Game
{
public:
	~Game();
	bool InitGame(HWND render_window);
	void UpdateGame(float deltaTime);
	void RenderWorld();

private:
	bool InitializeDirect3D();
	void FreeAllResources();
	void OnDeviceGained();
	void OnDeviceLost();

private:
	//Member Variables have m_ prefix
	HWND m_mainWindow;
	IDirect3DDevice9* m_d3dDevice;
	D3DPRESENT_PARAMETERS m_present_parameters;
	int m_deviceStatus;
	ID3DXEffect* m_pEffect;

private:
	//Helper Functions.
	D3DPRESENT_PARAMETERS CreatePresentParameters(HWND render_window);
};


Now we create the effect instance with the following code:

if(FAILED(D3DXCreateEffectFromFile(m_d3dDevice, 
				L"HLSL1.fx", 
				NULL, 
				NULL, 
				D3DXSHADER_DEBUG, 
				NULL, 
				&m_pEffect, NULL)))
	{
		return E_FAIL;
	}


A good place to put the code above is in the InitializeDirect3D() function.

Once that's done we can now use the effect in our program with m_pEffect.

Also, as with all, or most DirectX COM objects, we also have to free any memory that is used by the effect instance at the end of the program, so add this line to Game::FreeAllResources():
void Game::FreeAllResources()
{
	SAFE_RELEASE(m_pEffect);
	SAFE_RELEASE(m_d3dDevice);
}

View, Projection and World Matrices

Before we continue, there is some more theory you might need to know about 3D applications. In order to construct a 3 Dimensional view of a world in a 2 Dimensional viewport(the rectangle that you see the world in), you need to provide DirectX for example with mathematical data that describes how the world should appear to the viewer. We describe such data with a Matrix, or set of matrices. For example, in the real world we live in, everything you see comes to a vanishing point in the distance. Consider looking at a bit of paper flat on (See Figure 5.):


Figure 5.

The paper is a rectangle but it comes to a vanishing point into the far distance. If you represented each of the four corners of the paper as 3D points, then you would have to transform each of the points so that they end up in the right positions to construct a 2d image of the 3D world from where you are looking. We call this kind of view a perspective view. And we can use a perspective Matrix to transform all the points, vertices, in a 3D scene to produce this kind of vanishing point effect.

A Matrix is simply an array of numbers (See Figure 6.):


Figure 6.

And they can be used to transform points or geometry. For example rotate a 3d model. I will not go into the details of the mathematics of matrices but I will demonstrate how to use them programmatically because they are extremely useful.

In DirectX applications we typically use the D3DXMATRIX structure to work with matrices. I believe in newer versions of DirectX, they've done away with the d3dx library and it is no longer used, but I really like d3dx. It provides a really nice way to produce the various kinds of matrices we need like rotation matrices, scaling matrices and other types of transformation matrices that can be useful. For example D3DXMatrixTranslation(&m, x, y, z); produces the matrix "m" that can be used to change the position of a 3d model, translate it. m is defined as D3DXMATRIX m; and the D3DXMatrixTranslation() function here would simply update the values stored in m. D3DXMATRIX matrices are all 4x4 matrices. The structure itself just stores an array but the reason we use it is because it has overloaded operators, for example the multiplication operator * let's us multiply two matrices together and get a resulting D3DXMATRIX, M1 * M2 = M3.

There are 3 matrices we need to produce a 2 Dimensional image of a 3D world. These are a projection matrix, view matrix and world matrix.

The projection matrix as I talked about turns 3d points in a scene into a perspective view. The view matrix transforms points in a scene into positions that are relative to our point of view, for example, a view matrix is defined by an eye position, a point that we are looking at, and an up direction. By constructing a view matrix with such properties, it allows us to transform every vertex in the world so that it is positioned relative to our point-of-view or the point of view of a camera. Eye, LookAt, UpDirection. Once we have transformed points relative to our point-of-view, we can then apply the perspective(proj) transformation to transform them into a perspective view. Both transformations can be applied at the same time by multiplying the view matrix by the perspective matrix - View * Proj;

Here is an example of how we would construct a view matrix:

D3DXMATRIX view;
D3DXVECTOR3 Eye(0.0f,0.0f,-100.0f);
D3DXVECTOR3 LookAt(0.0f,0.0f,0.0f);
D3DXVECTOR3 UpDirection(0.0f,1.0f,0.0f);
D3DXMatrixLookAtLH(&view, &Eye, &LookAt, &UpDirection);

This would position the camera at -100.0f along the z axis.
The camera would be looking at the origin of the world 0,0,0.
And the +Y direction would be the "up" direction of the world.
This is because D3DXVECTOR3 holds an x, y and z value.

The World matrix is a matrix used to transform 3d models by any transformations we may require. For example we could rotate a castle by a world matrix, then apply the view*proj to the castle to get it's final transformation. Then we could have a different world matrix for another 3d model, transform it by the world matrix, then apply the view*proj to get it's final transformation.

Once we have the world matrix, the view matrix and the projection matrix, we pass them to the effect and the HLSL effect does it's magic and produces the pixels that we see on the screen, which is our 3d world.

Loading a Mesh into the Program

I think I've explained enough now for you to at least be able to use the Mesh and the Camera classes in the toolkit(Don't Worry, I will explain how to use them), provided you understand vectors a little(See Tutorial 4). There is some theory behind meshes, 3d models, such as texture coordinates that might be useful for me to explain but I think it's about time we got some results so I want to show you how to use Mesh and Camera to load a textured cube into your program.

I have supplied a stripped down version of the Camera and Mesh classes that you can download from the following link: Download as Zip

The Zip file contains the following files:
  • Mesh.h
  • Camera.h
  • Mesh.cpp
  • Camera.cpp
  • Cube.x
  • Chair.x
  • BARK5.jpg
  • dxlogo.png
  • HLSL1.fx
or feel free to use the full versions of the files from the toolkit itself. However, the full versions of the files depend upon other files in the toolkit, just to make you aware, so you will have to include all the files in the toolkit or at least the ones such as Collision.h, Collision.cpp that Mesh depends on.

Add the files listed above to your window project.

Note: For a Debug Build, you will have to put Cube.x, Chair.x, BARK5.jpg and dxlogo.png in the same directory as the .cpp files in your project.

Ok, now we are going to set up a camera and load a .x mesh into the program.

Include Camera.h and Mesh.h in the main.cpp file.
#include "Camera.h"
#include "Mesh.h"
Declare a Camera instance and a Mesh instance in the Game class:
private:
	Camera camera1;
	Mesh model1;

In the Game::InitGame() function we will set the position of the camera and load the Cube.x file using the Mesh instance, model1.

Modify the Game::InitGame() function as follows:
bool Game::InitGame(HWND render_window)
{
	this->m_mainWindow = render_window;

	if(!InitializeDirect3D()){
		return false;
	}

	camera1 = Camera(D3DXVECTOR3(0,30,-100.0f), D3DXVECTOR3(0,0,0));

	model1.Load(L"Cube.x", m_d3dDevice);
}

What's happening here?

We set the position of the camera with the vector D3DXVECTOR3(0,30,-100.0f) and we set the position the camera is looking at with the vector D3DXVECTOR3(0,0,0). The camera sets the up axis to be +Y because that's how I've implemented it. For example camera1.GetViewMatrix() will give us the view matrix that has a positive Y axis as the up direction.

Then we load the Cube.x file with model1.Load(L"Cube.x", m_d3dDevice);

We must free any memory that the Mesh object uses. Objects in the toolkit that don't require us to free memory do not have a Release() function but since Mesh uses DirectX objects to load a mesh I have provided a Release() function. If you look at an object and it has a Release() function this means you have to call Release() when you are done with it. Otherwise, if there is no Release() function then you do not have to worry about freeing resources.

Modify the FreeAllResources() function as follows:
void Game::FreeAllResources()
{
	model1.Release();
	SAFE_RELEASE(m_pEffect);
	SAFE_RELEASE(m_d3dDevice);
}

Lastly, to get a cube rendered to the window, we need to pass the view and projection matrix to the effect instance and call model1.Render(). Here is the complete implementation of the Game::RenderWorld() function that does just that:
void Game::RenderWorld()
{
	if(m_deviceStatus == DEVICE_LOST_OR_OPERATIONAL)
	{
		//TODO: Add Device lost handling.
		m_d3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xff0000ff, 1.0f, 0);

		if(SUCCEEDED(m_d3dDevice->BeginScene()))
		{
			//Call UpdateMatrices() to recalculate the view and
			//projection matrices with current camera position etc.
			//We may have moved the camera or resized the window, 
			//so we need to recalculate
			//the view and projection matrices with UpdateMatrices()

			camera1.UpdateMatrices(m_mainWindow);
			D3DXMATRIX view = *camera1.GetViewMatrix();
			D3DXMATRIX proj = *camera1.GetProjMatrix();

			m_pEffect->SetMatrix("matVP", &(view * proj));
			D3DXVECTOR4 lightPos(0.0f, 0.0f, -50.0f, 0.0f);
			m_pEffect->SetVector("lightPos", &lightPos);

			model1.Render("Lighting", m_d3dDevice, m_pEffect);

			m_d3dDevice->EndScene();
		}

		// Swap buffers.
		m_d3dDevice->Present(NULL, NULL, NULL, NULL);
	}
}
And that's it!

You may notice "Lighting" in model1.Render("Lighting"...). An HLSL effect can be used to achieve different effects and these effects can be defined by a technique. If you examine the HLSL file you will notice there is a technique called Lighting. In our case we want to use simple lighting to light our models, but there might be other techniques we want to use for different models, so I have made it so you can choose the technique you wish to use by passing it's name as a parameter e.g. "Lighting".

If you have managed to implement the code covered in this tutorial up until now, and you run the program, you will see a textured cube rendered to the window (See Figure 7.).

Figure 7.

Finishing Up

Nothing is moving, and it would be good if you could transform different 3d models in various ways. So to finish up and put what we have leanred about matrices into practice, we will make the cube rotate over time.

Here we will rotate the cube around the Y axis using a rotation value YRotation.

Add the YRotation variable to the Game class:
private:
	Camera camera1;
	Mesh model1;
	float YRotation;

Make sure it has a sensible value when the game starts, so we will set it to 0.0f. If you do not set the value of a variable such as floats or ints, they could have any random value.
bool Game::InitGame(HWND render_window)
{
	this->m_mainWindow = render_window;

	if(!InitializeDirect3D()){
		return false;
	}

	camera1 = Camera(D3DXVECTOR3(0,30,-100.0f), D3DXVECTOR3(0,0,0));

	model1.Load(L"Cube.x", m_d3dDevice);
	YRotation = 0.0f;
}

The last modification is to increase the rotation value over time and rotate the cube.

Note: Often you have to convert degrees to radians when working with rotations. If you forget to do this you might get unexpected results. For example D3DXToRadian(degrees).

The rotation functions of Mesh I have provided take degrees as arguments:
void Game::UpdateGame(float deltaTime)
{
	//Handle device lost event.
	HRESULT coop = m_d3dDevice->TestCooperativeLevel();

	if(coop != D3D_OK)
	{
		if(coop == D3DERR_DEVICELOST)
		{
			if(m_deviceStatus == DEVICE_LOST_OR_OPERATIONAL)
				OnDeviceLost();		
		}
		else if(coop == D3DERR_DEVICENOTRESET)
		{
			if(m_deviceStatus == DEVICE_NOTRESET)
				OnDeviceGained();
		}
	}

	//Update game objects here.
	YRotation += deltaTime*0.1f;
	model1.Rotate(YRotation);
}

I did say we'd put what you have learned about matrices into practice, so you could achieve the same result by updating the rotation matrix of the Mesh:
YRotation += deltaTime*0.1f;
D3DXMatrixRotationY(&model1.rotation, D3DXToRadian(YRotation));

Loading a Chair

If you want to load a chair instead of the cube, just replace "Cube.x" with "Chair.x":
model1.Load(L"Chair.x", m_d3dDevice);

The Source

Here are the source files for this tutorial(Including resources):
Download as Zip