Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
114 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

Loading and displaying .X files without DirectX


Introduction

I like the .X file format: its organisation and structure suits me. But why would I need to load and display X files without DirectX? My needs were simple:

  • I often use Borland C++ 5.01 compiler. The dxguid.lib cannot simply be converted from COFF to OMF without losing much information and rendering the DirectX SDK almost useless.
  • I wanted to be platform independent.
  • I wanted to understand how DirectX was rendering meshes and what was hidden in the helper functions.

The code samples use Freeglut and were written using DevCpp 5.0 (4.9.9.1) and minGw 3.4.2. This article is designed to be read while referencing the sample source code.

1. Loading and displaying the Mesh from an X File

Before getting into any kind of development, we need to define first what a mesh is and how it is stored within the X file. We will then derive and implement a design.

1.1. Design

1.1.1. Descriptions

A polygonal mesh is a structured list of points (called vertices) connected together to describe a surface. In our case, the mesh will define the surface of our model.

To texture the polygonal mesh, we associate texture coordinates with each vertex to know which part of an associated bitmap to draw on the mesh (e.g.: drawing tiny's jacket on the mesh part modelling the jacket).

Each face of a mesh can also be associated with a material. A material describes the colour reflected by an illuminated model. The colour is often defined as a set of Red, Green, Blue and Alpha components.

Let's now look into the X File format.

The .X File format can be either in plain text or in binary. This is given in the header of the file. We will look into the text format. The binary format will be addressed in the fifth chapter.

The X file format is structured into blocks describing elements of the model. You can browse [MSDN] to find descriptions of all the different blocks. A pair of braces delimits each block. An opening brace must always be paired with a closing brace. There is a hierarchical relationship between the blocks since some may contain others. The table below will outline the hierarchies non-exhaustively:

BlockContains
Frame FrameTransformMatrix
Frame
Mesh
Mesh MeshNormals
MeshTextureCoords
MeshMaterialList
SkinMeshHeader
SkinWeights
MeshMaterialList Material
Material TextureFileName
AnimationSet Animation
Animation AnimationKey

We are interested in the Mesh Block. This block can be either directly accessible or embedded within a frame. We may find many meshes within the same X file. For example, there are two meshes in tiny_4anim.x. The mesh block holds all the necessary information for its display:

  • MeshNormals: a list of normal vectors used in case the model is illuminated.
  • MeshTextureCoords: Texture coordinates for each Material.
  • MeshMaterialList: the list of Materials for the mesh. This block also associate a Material index to each face of the model.

Since we want to have a working demo, we need to support the following:

  • Recognise the type of the format of the file (text or binary),
  • Extract all the meshes and their associated blocks into a model,
  • Concatenate the meshes of a model to simplify the use of the model,
  • Create a mesh subset for each material used,
  • Calculate the bounding box coordinates of the mesh,
  • Display the model mesh under OpenGL.

Whew! Though that list is small, there is much work to do before we are able to display a mesh.

1.1.2. Code Design

The graph below sums up the class hierarchy for the Model object derived from the descriptions above:

The Model class has a single method to concatenate meshes.

The Mesh class maintains static lists of vertices, texture coordinates and normal vectors.

The Material class holds a colour and the Bitmap texture.

Wait a minute! How are we going to draw the model?

We could use the Model Object and add a Draw method. But doing this will cause some problems down the road: when we get into mesh animation, we will calculate a new set of vertices from the original mesh vertices at each animation frame. If we want to display the same model but with different poses (say like in a mob of monsters), we would have to calculate the model mesh for each monster. Instead we will create an Object3D class which will be used to perform all calculations on a model mesh. This Object3D class will be initiated with a Model class and will contain the methods to draw and calculate the bounding box of the mesh.

Why don't we have a Load method within the Model object?

There is a simple answer. There are many 3D-model file formats. We would need to implement a load method for each existing file format plus a function to get the correct loading function from the file extension. This would transform our code into some ungainly spaghetti.

We will use a loading interface for ease of implementing future loading functionality. From this loading interface, we will derive our X File loading class.

1.2. Implementation

At last! We can now begin coding. But before diving into the implementation of our design, I will quickly describe the framework into which our code will be embedded. You can find the framework in the file Sample0.zip.

1.2.1. Quick description of the framework (file Sample0.zip)

This framework is built on top of Glut. All the screen manipulations are encapsulated within a Screen object. This Screen object is also responsible for Font manipulation and texture loading and registration under OpenGL.

The Sample0 example shows how the OpenGL logo texture is loaded and selected before being displayed on the screen.

There is also a timer class used to calculate the elapsed time since the last call. This class is based on the Win32 function GetTickCount. This can easily be replaced by the function glutGet(GLUT ELAPSED TIME)).

Finally, there are some tracing macros defined in ToolBox\MyTrace.h.:

  • DECLARE_TRACE: declares the tracing object.
  • START_TRACE: initialises the tracing file in the directory Debug.
  • END_TRACE: removes and deletes the tracing object.
  • MYTRACE: prints the data in the Glut console window and in the tracing file.

All code modifications explained in this article will be signalled within the source by the following tags:

/***********************************************
 NEW-NEW- NEW- NEW- NEW- NEW- NEW*/
…
Code modification
…
/***END***************************************/

All right! Let's first look at the code in the file Sample1.zip.

1.2.2. Parsing the file (file Sample1.zip)

The loading interface is defined in framework\Frm_IO.h. This is a template interface with two protected methods to help users to convert text to floating point numbers and to remove all occurrences of a character from a string.

The X File loading class is defined in files ToolBox\IOModel_x.h and cpp.

This is what happens in pseudo-code:

    Open the file
    Check the file Header
    Grab a reference to the Model Object to fill in
    Enter the main processing loop:
      While we have not reached the end of file
      Read the block name (ProcessBlock)
        If the block name is recognised, process it.
        Else avoid the block (AvoidTemplate)

The file Header is checked by comparing the value read from the file with macros defined in the file XfileStructs.h and given by Microsoft in [MSDN]. These macros are important since they can also be used to process binary files.

There are two main utility functions:

  • ProcessBlock: This function checks the current character to identify the start of a block name and avoid comments (which start by the characters # or //). If a valid character is detected, this function reads in the string until it finds a space, then calls the utility function Block. This second function will look up in the list of XOF_TEMPLATEID structure (this structure pairs a block name with a token identifier). If it recognises the string as a valid Block Name, it will return the corresponding Token Identifier else this function returns a X_UNKNOWN Token.

  • AvoidTemplate: This function will avoid all the data enclosed between a matched pair of braces. It consumes an opening brace character then checks each successive character. If it finds another opening brace character, this function will call itself recursively. This function returns whenever it finds a closing brace.

These utility functions are invaluable to process a text X File since they help us narrow down the blocks we want to process. If a block is contained within another one like in the Frame structure, it will suffice to duplicate the processing loop inside a specialised processing function for the frame structure (see the function ProcessBone(void)).

We found a mesh! Hallelujah! Now we have to process it. This is the task of the specialised function ProcessMesh. Here is what happens in pseudo-code:

    Create an empty Mesh object and initialise it.
    Read in the name of the mesh.
    If there is no name, assign a name to that mesh.
    Read in the number of vertices.
    Load the list of vertices into the Mesh object.
    Read in the number of faces
    Load the list of faces into the Mesh object.
    Enter the mesh processing loop:
      While we have not reached the end of the block
      Read the block name (ProcessBlock)
        If the block name is recognised, process it.
        Else avoid the block (AvoidTemplate)
    Once the Mesh block is fully processed we add the Mesh object to the Model's list of Meshes.

Why do we need a mesh name? The X file format either declares the mesh or only references the mesh name within the block that is concerned by it. To be able to trace what happens and check that the mesh is correctly associated, we need a unique mesh name. If there are no names, we need to create a unique name (this is done by the utility function SetUID).

Next we process the Mesh block data (see [MSDN] for the description of that data). Then we enter a loop to process all embedded blocks.

The Texture Coordinates block is very simple to process: we read in the number of texture coordinates, and then we load in the list of texture coordinates. The block is processed.

The Mesh Normal Vectors block isn't any more difficult. We read in the number of vectors, and then we load in the list of vectors. Next we load in the list of vector indices per face: this gives us the vertex normals for each face allowing for correct illumination of the model.

Material list blocks are a little trickier. Here is the pseudo-code:

    Read in the number of Materials used by that mesh.
    Read in the material index for each face
    Enter the material list processing loop:
    While we have not reached the end of the block
      Read the block name (ProcessBlock)
        If the block name is recognised, process it.
        Else avoid the block (AvoidTemplate)

All that is left is to process each Material description block within the Material list. Here we go:

    Read in the face colour.
    Read in the emissive power.
    Read in the specular colour.
    Read in the emissive colour.
    Enter the material description processing loop
    While we have not reached the end of the block
      Read the block name (ProcessBlock)
        If the block is TextureFileName, we read in the bitmap name.
        Else avoid the block (AvoidTemplate)
    Once the material is processed, we add it to the mesh's list of materials.

And that's it! All the meshes are loaded into the model object.

1.2.3. Concatenating meshes and creating subsets

We will now look into the files framework\Frm_Mesh.h and cpp.

We can't yet display the meshes loaded within the model. First, we want to concatenate the meshes because:

  • This simplifies the mesh maintenance (only one material list, one normal vectors list and one list of vertices and faces).
  • This simplifies the drawing step further down the line: we remove a loop through a list of meshes and we only draw a single vertex array.

If you look closely at the Mesh block parsing code, you see at the beginning the initialisation of a series of values for the mesh: these values are the sum of the previous meshes indexes (number of vertices, number of faces, … ). These values will be used:

  • To deduce the final size of the concatenated mesh.
  • To increment all index references by their starting values to have a correctly displayed mesh.

Now let's have a look at the pseudo-code:

    Create a new mesh and retrieve its dimension from the last mesh in the list.
    Check the new mesh dimensions and resolve all discrepancies.
    Create all the new mesh arrays.
    Process each mesh from the model list
      For each mesh increment the index references.
      Copy each mesh data into the new mesh.
      Move each mesh material into the new mesh material list.
    Delete the model mesh list.
    Add to the model mesh list the new concatenated one.

When we calculate the new mesh dimensions, we need to take care of differences between mesh descriptions. One mesh may use textures and thus have texture coordinates while another may just be coloured and have no texture coordinates. To solve that problem, we duplicate the vertex array size to initialise the texture coordinates array. If we didn't do that, the face list would be divided between indexed faces with colour information and indexed faces with texture coordinates.

Now that we have concatenated our meshes, there is one step left: we need to create subsets. Let me explain: we have created a mesh with multiple materials. We want to divide our array of faces into a list of faces for each material used. The aim is to have only one call to set a material before drawing our mesh subset.

The code is very straightforward:

    For each Material in the material list
      Count the number of occurrences of that material in the FaceMaterial list
      Initialise the subset
      For each occurrence of that material in the face material list copy the face data to the subset
    Add the subset to the list of subsets

1.2.4. Displaying the result

At last, we have parsed our X File, we have concatenated our Meshes and our Model Object is ready for display. Only one part of our design is left for implementation: the Object3D class that will be in charge of all the calculations based on the original mesh.

Let's look at the file Sample1.cpp. During the initialization of the Glut demo, we call our specialised loader object to parse the file tiny_4anim.x into our Model instance. If the Model was successfully loaded, we concatenate the meshes. We load up into OpenGL all bitmap textures declared within the Meshes material list.

Now we enter the meat of our subject: we initialise an instance of Object3D with our loaded Model. This initialisation keeps a pointer to the Model, gets a pointer to the first mesh of the Model Mesh list and initialises an empty vertex array with the same size as the Model Vertex Array. Then we call the Object3D Update method, which copies the Model Vertex Array into its own array. Last but not least we compute the bounding box coordinates and deduce the centre of the bounding sphere.

Let's display our Object3D! First, we calculate our camera position with the centre of the bounding sphere, and then we call the draw method with our Screen object as a parameter. This draw method will parse the mesh material and subset lists. It will set up each material and draw the corresponding subset until there are no more materials to process.

In the Idle function, we clear the Object3D vertex array and call back the Update function. That's all there is to it. Whew! We finally made it.

Let's increase the complexity. Time for us to look into tiny's skeleton and skin her.





Bone hierarchy and skinning


Contents
  Introduction
  Bone hierarchy and skinning
  Animating the model

  Printable version
  Discuss this article