Introduction to X file saving
This page describes how you can save your own mesh data in the .x file format maintaining a mesh hierarchy. If you are not interested in keeping the hierarchy you can simply use the D3DXSaveMeshToX function. The page is split into the following:
- The Objects
- Step 1 – Creating the X file object and registering templates
- Step 2 – Creating the save object
- Step 3 – Adding our top level data
- Step 4 – Adding mesh data
- Step 5 – Finishing off
These notes refer to the new X file interfaces first introduced in the December 2004 update of the SDK. The old interfaces still work but are now marked as deprecated.
Note: there is very little information on the Internet or in the DirectX SDK about loading and saving .x files. You may find some notes that use the old interfaces but as far as I can tell my notes are the only ones anywhere using the new interfaces. Since there is so little information in the DirectX SDK help I have had to do a certain amount of reverse engineering and trial and error so it is possible I may have done things incorrectly in places. However you can be sure the code below works fine. If more information becomes available I will of course update these notes.
The old legacy interfaces were:
- Pointer type: LPDIRECTXFILE
- Pointer type: LPDIRECTXFILEDATA
- Pointer type: LPDIRECTXFILEOBJECT
- Pointer type: LPDIRECTXFILESAVEOBJECT
- Pointer type: LPDIRECTXFILEBINARY
- Pointer type: LPDIRECTXFILEDATAREFERENCE
- Pointer type: LPDIRECTXFILEENUMOBJECT
The new interfaces are:
- Pointer type: LPD3DXFILE
- Pointer type: LPD3DXFILEDATA
- Pointer type: LPD3DXFILEENUMOBJECT
- Pointer type: LPD3DXFILESAVEDATA
- Pointer type: LPD3DXFILESAVEOBJECT
The pointer type is just used to ease typing since you generally use a pointer to these interface classes. For example LPD3DXFILE is defined like this:
typedef ID3DXFile* LPD3DXFILE;
In these notes I will not use these typedefs as I find they confuse people, instead I will use the full pointer declaration.
X file saving involves the use of a number of DirectX objects. These provide the interfaces to allow us to assemble the data to save. As with all of DirectX we get DirectX to create these objects for us and then use their interfaces. The objects required here are:
ID3DXFile – used to create other objects (ID3DXFileEnumObject and ID3DXFileSaveObject) and also to register templates. The .x file format is flexible allowing user defined data to be stored. For this reason it uses the concepts of templates to define the make up of the data. There are a number of default templates for the most common data provided with the SDK.
ID3DXFileSaveData – allows us to add data objects as children to a .x file data node.
ID3DXFileSaveObject – allows us to add data objects and templates to the .x file and also to save the .x file to disk.
The interfaces ID3DXFileEnumObject and ID3DXFileData are not used for saving but are used in loading .x files.
The first step is to obtain an instance of an ID3DXFile. As usual with DirectX we do this by declaring a pointer to this type and passing the address of the pointer to a Direct3D function. Direct3D will then create an instance and point our pointer to it. We then have access to all the required functions.
HRESULT hr=D3DXFileCreate(&xFile );
If this succeeds (always remember to check the HRESULT returned for error conditions, for clarity I miss these checks from the calls below but you would be wise to do them) the next step is to specify the templates we want to use in our .x file.
xFile->RegisterTemplates( (void*)D3DRM_XTEMPLATES,D3DRM_XTEMPLATE_BYTES ) ;
Here we are registering the standard templates for saving model data. These templates are defined in the header file rmxftmpl.h so this header will need including. More detail on what data these allow us to save is detailed later. In addition there are some other templates provided for skinning use:
There are also a set of templates that have been added more recently by Microsoft called XEXTENSIONS_TEMPLATES. As I mentioned earlier you can also create your own custom templates but this is beyond the scope of these notes.
The next step is to create a save object (ID3DXFileSaveObject) where we can start building our data. We use the ID3DXFile object to create this object:
xFile->CreateSaveObject(filename , flags, format, &xFileSave);
The first parameter is the filename you wish to use and the second is flags to determine the character set of the filename (D3DXF_FILESAVE_TOFILE or D3DXF_FILESAVE_TOWFILE for use of wide characters). Format is used to specify if you want to save the file in binary, text and/or compressed. The final parameter is the address of a ID3DXFileSaveObject pointer that, if the call is successful, will point to a new instance. So an example of creating a save object to save to ‘test.x’ in compressed binary would be:
xFile->CreateSaveObject(“test.x” , D3DXF_FILESAVE_TOFILE, D3DXF_FILEFORMAT_BINARY | DXFILEFORMAT_COMPRESSED, &xFileSave);
Now that we have our save object created we can start adding our data.
It is usual to write data to the file with a frame hierarchy (see the example of a hierarchy for a person here: x file animation). So we need to build our data by creating frames and adding child frames and mesh. For each frame we can specify a transformation. This describes how the frame is transformed relative to it’s parent frame.
What I like to do is add a root frame and then add a frame for each set of mesh data. The Microsoft example files do not always do this but I like this method as it eases the creation of a hierarchy.
It is easy to get confused between the file save data interface (ID3DXFileSaveData) and the file save object interface (ID3DXFileSaveObject) as they have similar functionality. Basically the ID3DXFileSaveObject interfaces control the adding of data to the .x file itself, this allows you to add many things at the top level. In practice I simply add to this a default frame with a transformation matrix and from then on add to a ID3DXFileSaveData. The ID3DXFileSaveData once attached to the save frame can be added to using it’s interfaces.
So lets create our top level frame to hang everything else from:
hr = xFileSave->AddDataObject(TID_D3DRMFrame, “Scene_Root_Frame”, NULL, 0, NULL, &xFileSaveRoot);
The AddDataObject method provided by both ID3DXFileSaveData and ID3DXFileSaveObject allows different types of data to be added. Since the .x file format is meant to be flexible and allow additions all types of data are described via the use of templates. In the first step above we registered the default templates which provide a number of common types including: frames, matrix and mesh. Each type is referred to using a globally unique identifier (GUID) e.g.
- TID_D3DRMFrame – data describing a frame
- TID_D3DRMFrameTransformMatrix- a transformation matrix
- TID_D3DRMMesh – mesh data
- TID_D3DRMMeshNormals – mesh normal data
- TID_D3DRMMeshTextureCoords – mesh texture co-ordinates
- TID_D3DRMMeshMaterialList – material list
- TID_D3DRMMaterial – one material
- TID_D3DRMTextureFilename – a texture filename
Note: there are more of these, these are just the most commonly used ones. Look in rmxfguid.h for a full list. You can also create your own custom ones if required.
The first parameter of AddDataObject is where we specify the data template we want to use. The second parameter is a name. You can specify this or not – it is up to you. The third parameter is where we can specify a GUID for the data object itself or NULL if it does not have one (normally the case). The next two parameters specify the data itself. Since AddObject can work with any type of data we simply specify the size of the data in bytes and a void pointer to the data. The last parameter is the return from the function. Here we provide the address of a new object that will be created from the call. For both the ID3DXFileSaveData call and the ID3DXFileSaveObject the returned object is a ID3DXFileSaveData object.
We can now forget about the ID3DXFileSaveObject until the final step of writing to disk as we have added our root ID3DXFileSaveData to it and everything else will be added to this (or to it’s children).
So far we have just created a top level frame, we now want to build on this. If you want to specify the transformation of the exported data you will want to add a transform matrix in here. We add this object to our newly created xFileRootObject.
The data for a transform matrix is held in 16 floats. I use the following simple code to fill it from a D3DXMATRIX called matWorld:
size_t *pbData=new float;
for (int y=0;y<4;y++)
for (int x=0;x<4;x++)
We can now add our transformation data:
ID3DXFileSaveData *pRootTransform = NULL;
“Root_Transform_Matrix”, NULL, size, pbData,&pRootTransform);
I normally create a new frame per mesh. It may be that you only have one mesh in your data but if you have more you may wish to set a transformation matrix per frame. For each mesh I am going to save I will add a root frame and transformation matrix.
Once we have written our .x file we can load it into mesh viewer (comes with the SDK) to check it is correct. You can examine the hierarchy and for what I am describing it should look like this:
If I save more than one mesh per 3D object it looks like this (without the mesh frames expanded):
Below is the code I use to add the root frame for each mesh:
// Loop for each mesh (I am holding them in a vector here)
// create a unique name for each mesh frame
hr = xFileSaveRoot->AddDataObject(TID_D3DRMFrame, buf, NULL, 0, NULL, &xFileMeshData);
We have now created a uniquely named frame to which we can add all our mesh data e.g. positions, normals, texture co-ordinates.
Writing the mesh position data
The pattern of adding objects carries on as already described. You have to be a bit careful to make sure your data is laid out correctly and that data sizes are correct etc. below is the calculation I use to determine size when saving mesh data with vertex position and indices:
DWORD byteSize=sizeof(DWORD)+ // num verts
3*sizeof(float)*m_numVerts+ // the verts themselves
sizeof(DWORD)+ // num faces
m_numTris* // each tri
(sizeof(DWORD)+ // number of verts per tri
3*sizeof(DWORD)); // the three vertex index
I then create some room for it all and stream the data into it in the following order:
- Number of vertices
- For each vertex: the vertex position
- Number of triangles
- For each triangle the number of indices (3) and then each index
I then add this data
ID3DXFileSaveData * meshDataObject = NULL;
HRESULT hr = xFileMeshData->AddDataObject(TID_D3DRMMesh, “Mesh_Data”,NULL,byteSize,pbData, &meshDataObject);
Important: the normals, material lists and textures should all be added to this new meshDataObject.
Writing the mesh normal data
Normal data is added to the meshDataObject. As well as streaming the normal vectors we also need to specify which faces the normals belong to. Normal data should be in the following format:
- Number of normals
- For each normal: the normal x,y.z values
- Number of triangles
- For each triangle: how many normals (3) and then each vertex index
As usual add this to meshDataObject using the AddDataObject method. You will need to provide a new ID3DXFileSaveData object but after adding the data you have no further use of it (so release it).
Writing the materials
We can write a list of materials used by our triangles. We specify for every triangle which material it uses. The following data is written:
- Number of materials
- Number of faces
- For each face which material it uses
After adding this data object using TID_D3DRMMeshMaterialList we can then add a description of each material. Note: we add these as children of the returned material list AddObject object.
For each material we write:
- Diffuse colour
- Specular colour
- Emissive colour
Note: there is no ambient colour hence the normal process of assigning of the diffuse to the ambient component when loading a .x file.
After adding this material to our material list object we get back a material object. If we wish to specify a texture file for the material we add this to the material object e.g.
ID3DXFileSaveData * textureObject = NULL;
hr = materialObject->AddDataObject(TID_D3DRMTextureFilename, NULL, NULL, byteSize, charStr, &textureObject);
Where charStr is a character string and byteSize is calculated as:
byteSize = sizeof(char)*(strlen(charStr)+1);
We have now finished writing out our materials so can release the material list object, the material objects and the texture objects.
Writing the texture co-ordinates
Texture co-ordinates are quite straight forward you specify:
- Number of texture co-ordinates
- Each coordinate (2 floats)
I have described above the main data types you will want to write to a .x file but note that there are many more that are beyond the scope of these notes. Once you have finished adding objects the last thing to do is to save to disk, you use your original ID3DXFileSaveObject:
X files are unfortunately far from simple to use. This is mainly because the format is meant to be flexible and so is based on the use of templates (the Microsoft documentation is not exactly helpful either). Hopefully these notes will prevent others from going through the struggle of understanding x files that I had to!