OpenFX is a great all round 3D modeller. I found it easy to use and very powerful. The main problem I had was exporting my model into a .x file. The only way I found was to export to a .3ds file and then use the conv3ds.exe tool that comes with the DirectX SDK to convert it to a .x file. The only problem was that it lost the textures when exporting to a .3ds file. There is also very limited help. So, OpenFX isn't the right tool for me.
So, I have decided to use MilkShape 3D because it is cheap, it can export to a .x file (via a free plug-in) and it is easy to use.
Once you have your 3D modelling package, the next thing to do is create a model. For this tutorial have created a simple spaceship. I used the tutorials http://xu1productions.com/3dstudio/tutorials.html to get me started with MilkShape. Fig 10.1 below, shows a screenshot of MilkShape 3D with my completed spaceship model.
Fig 10.1
Once your model is complete, you need to export it to a .x file. To do this with MilkShape, download and install the "DirectX 8.1 Exporter" plug-in by John Thompson from the MilkShape website. Then open your model in MilkShape and go to "File"→"Export"→"DirectX (JT)…". Select a location for your .x file, then select the options you require (normally the defaults) and press "OK". You are now ready to load your model into your DirectX program.
For this tutorial I have created a wrapper class called CMesh for loading and rendering meshes loaded from a .x file. Below is the main code for the CMesh class. The constuctor shows how to load a mesh from a .x file and store it in memory. The destructor shows how to free the memory that was used to store the mesh. The Render method shows how to render a mesh.
The CMesh constructor takes two parameters, the first is a pointer to the device and the second is string containing the path of the .x file to load. We use the D3DXLoadMeshFromX function to load the mesh into memory. Once this is done we create two arrays, one to hold the materials and one to hold the textures of our model. We then loop around and populate the two arrays from the loaded mesh. The last thing to do is make sure that the normals are set for each vertex of the mesh. We clone the mesh and use the D3DXComputeNormals function to set the normals.
In the destructor we release each texture and the mesh itself. We also have to delete the two array pointers that we created in the constructor.
The Render function is pretty straight forward, we simply loop through each subset of the mesh and render it with the appropriate texture and material.
CMesh::CMesh(LPDIRECT3DDEVICE8 pD3DDevice, LPSTR pFilename) {
LPD3DXBUFFER pMaterialsBuffer = NULL;
LPD3DXMESH pMesh = NULL;
m_pD3DDevice = pD3DDevice;
if (FAILED(D3DXLoadMeshFromX(pFilename, D3DXMESH_SYSTEMMEM, m_pD3DDevice, NULL, &pMaterialsBuffer, &m_dwNumMaterials, &pMesh))) {
m_pMesh = NULL;
m_pMeshMaterials = NULL;
m_pMeshTextures = NULL;
LogError("<li>Mesh '%s' failed to load", pFilename);
return;
}
D3DXMATERIAL* matMaterials = (D3DXMATERIAL*)pMaterialsBuffer->GetBufferPointer();
//Create two arrays. One to hold the materials and only to hold the textures
m_pMeshMaterials = new D3DMATERIAL8[m_dwNumMaterials];
m_pMeshTextures = new LPDIRECT3DTEXTURE8[m_dwNumMaterials];
for (DWORD i = 0; i < m_dwNumMaterials; i++) {
//Copy the material
m_pMeshMaterials[i] = matMaterials[i].MatD3D;
//Set the ambient color for the material (D3DX does not do this)
m_pMeshMaterials[i].Ambient = m_pMeshMaterials[i].Diffuse;
//Create the texture
if (FAILED(D3DXCreateTextureFromFile(m_pD3DDevice, matMaterials[i].pTextureFilename, &m_pMeshTextures[i]))) {
m_pMeshTextures[i] = NULL;
}
}
//We've finished with the material buffer, so release it
SafeRelease(pMaterialsBuffer);
//Make sure that the normals are setup for our mesh
pMesh->CloneMeshFVF(D3DXMESH_MANAGED, MESH_D3DFVF_CUSTOMVERTEX, m_pD3DDevice, &m_pMesh);
SafeRelease(pMesh);
D3DXComputeNormals(m_pMesh);
LogInfo("<li>Mesh '%s' loaded OK", pFilename);
}
CMesh::~CMesh() {
SafeDelete(m_pMeshMaterials);
if (m_pMeshTextures != NULL) {
for (DWORD i = 0; i < m_dwNumMaterials; i++) {
if (m_pMeshTextures[i]) {
SafeRelease(m_pMeshTextures[i]);
}
}
}
SafeDelete(m_pMeshTextures);
SafeRelease(m_pMesh);
LogInfo("<li>Mesh destroyed OK");
}
DWORD CMesh::Render() {
if (m_pMesh != NULL) {
for (DWORD i = 0; i < m_dwNumMaterials; i++) {
m_pD3DDevice->SetMaterial(&m_pMeshMaterials[i]);
m_pD3DDevice->SetTexture(0, m_pMeshTextures[i]);
m_pMesh->DrawSubset(i);
}
return m_pMesh->GetNumFaces();
} else {
return 0;
}
}
One extra thing to note is that if you scale the mesh you will also scale the normals. This will have the effect of making the object darker the more it is scaled. To fix this problem we need to enable the D3DRS_NORMALIZENORMALS render state. This is done with one call to the SetRenderState function as shown below.
m_pD3DDevice->SetRenderState(D3DRS_NORMALIZENORMALS, TRUE);
For this tutorial we will create three spaceships and rotate them each about a different axis. The final scene when rendered will look something like the screenshot below:
So now we can create any object we like, we are no longer limited to cubes and spheres. Remember, when you create your models, make sure they have a low number of polygons. The more polygons you have the more your frame rate will drop. In the next tutorial we will look at adding 2D elements to a 3D scene. That is useful when it comes to creating scores and energy bars.
DirectX Tutorial 11: 2D in 3D
In this tutorial we will add some 2D elements to a 3D scene. This will be useful for adding things like energy bars, timers, radars and so on. We will also look at how to do texture transparency so that your 2D elements can appear non-rectangular. You can download the full source code by clicking the "Download Source" link above.
The first and easiest thing to do is add some 2D text to your scene. For this, I have created a simple wrapper class called CFont. The code for this class is shown below.
CFont::CFont(LPDIRECT3DDEVICE8 pD3DDevice, LPSTR pFontFace, int nHeight, bool fBold, bool fItalic, bool fUnderlined) {