Loading and Animating MD5 Models with OpenGL


Bob with Lamp

Bob with Lamp

In this article, I will show how you can load and animate models loaded from the MD5 model file format.  In this article I will use OpenGL to render the models.  I will not show how to setup an OpenGL application in this article. If you need to get a quick introduction on setting up an OpenGVL application, you can follow the “Beginning OpenGL for Game Programmers” article [here].

Introduction

The MD5 Model format has been used by several commercial game projects including ID software’s Doom 3

Dependencies

A few dependencies are used by this project to ease the coding process and to make the material more readable.

  • OpenGL Utility Toolkit (GLUT): Is used for platform-independent window management and window event handling that simplifies the implementation of an OpenGL windowed application.
  • OpenGL Mathmatics Library (GLM): Used for vector classes, quaternion classes, matrix classes and math operations that are suited for OpenGL applications.
  • Simple OpenGL Image Library (SOIL): A library that provides functions to load textures from disc as easy as possible.  This library is ideal for loading images to be used by OpenGL.
  • boost (1.46.0): The boost::filesystem library is used to resolve paths, decompose paths, and open files in a platform independent way.

All of the dependencies are provided together with the source files and project files that are needed to build the demo in Visual Studio 2008 and Visual Studio 2010.

MD5 Model Format

A fully animated MD5 model asset consists of two different files.

  1. The .md5mesh file: describes the geometry and materials that are used to display the model.
  2. The .md5anim file: describes a single animation that can be applied to the model described in the .md5mesh file.

The two files must match the number and name of joints to be valid.

The .md5mesh File

The .md5mesh file is used to describe the geometry and materials that are used to display the model.  This file consists of a header, a single “joints” section and any number of “mesh” sections.

The format of the .md5mesh file is:

The .md5mesh Header

An example of the header is shown below:

The header consists of the MD5 version this file describes, a command-line argument that was used to generate the mesh file, the number of joints described in this file, and the number of meshes that this file defines.

For the model loader described in this article, the “MD5Version” tag must always be “10”.  I will not cover different versions of the MD5 file format in this article and will assume this value is always “10”.

The next line describes the command-line arguments that were used to export the mesh file from the Digital Content Creation (DCC) tool such as Maya, 3D Studio Max, or Blender.

The next two lines “numJoints“, and “numMeshes” describe how many joints and meshes that are defined in this file.

The “joints” section

An example of the “joints” section is shown below:

Immediately following the “numJoints” and “numMeshes” parameters is the “joints” section.  The “joints” section starts with the word “joints” and an open-brace ‘{‘ character followed by “numJoints” joint definitions. These joints define the skeleton of the model in the bind pose.

Each joint is defined on a single line and begins with the name of the joint enclosed in double-quotes.  The next parameter following the name of the joint is the index of the joint’s parent in the skeletal hierarchy.  The only joint that does not have a parent is the root joint, in which case the joint’s parent index will be “-1”.

After the parent’s index, the joint’s position and orientation are described as 3-component vectors enclosed in parenthesis “( x y z )”.  Each component of the vector is separated by a space.  The first vector is the position of the joint in object local space, and the second vector is the orientation of the joint in object local space.  The orientation is a quaternion which actually requires 4-components to be fully defined.  The w-component of the orientation quaternion will be computed manually which will be shown later.

The “mesh” section

An example of the “mesh” section is shown below:

Following the “joints” section, there is a “mesh” section for each of the meshes described in the model file.  The “mesh” section begins with the word “mesh” and an open-brace ‘{‘ character.

The first parameter in the “mesh” section is the “shader” parameter.  It’s value is the relative path to a texture which can be applied to the mesh.  Depending on the exporter, this path could be relative to the root folder of the archive where the mesh was loaded from, or it could be relative to the .md5mesh file, or it could also be an absolute path on the computer where the mesh was originally exported (in this case, you will need to edit the texture path manually before importing the mesh if the file path doesn’t exist in your environment).  The texture path may or may not have an extension.  Your model loader should account for this by adding the default texture extension to the file path before requesting the texture from the the texture manager. More on this will be handled later when I describe the source code for the loader.

Following the “shader” parameter is the vertex definitions.  The first parameter before the vertex array is the “numverts” parameter which defines how many vertices this mesh contains.

A single vertex definition consists of the word “vert” followed by the index of the vertex in the vertex array.  Immediately following the vertex index is a 2-component vector enclosed in parenthesis “( s t )” that defines the texture coordinates of the vertex.  Following the texture coordinate are two integer values that describe the start index of the weight, and the number of weights that are associated with this vertex.  Each vertex can be weighted to one or more joints in the skeletal hierarchy of the model.  The final position of the vertex is determined by summing the positions of the joints and the positions of the weights multiplied by the bias of the weight.  Weight definitions will be described later.

Following the vertex array is the triangle array.  A triangle is defined by a 3-tuple set of vertex indexes in the vertex array.  The triangle array starts with the “numtris” parameter which describes how many triangles this mesh defines.

Each triangle definition appears on a single line of the file.  The triangle definition starts with the word “tri” immediately followed by the index of the triangle in the triangle array.  The next three integers in the triangle definition describe the index of the vertices in the vertex array that make up this triangle.

Following the triangle definitions is the weights array.  Each weight describes how much of a single vertex is associated with each joint in the model’s skeleton.  The weights array starts with the “numweights” parameter which describes the number of weights that are to be read.

Each weight definition appears on a single line.  The weight definition starts with the word “weight” and is immediately followed by the index of the weight in the weight array.  Following the weight index is the joint index in the joints array that this weight is associated with.  The “weightBias” parameter is a ratio that determines how much of the joint’s orientation and position is applied to the vertex’s final position.  The “weightPosition” parameter is a 3-component vector which describes the position of the weight in joint-local space and must be rotated by the joint’s orientation and added to the joint’s position before being applied to the final vertex position.  This algorithm will be described in more detail when I show the code that builds the mesh’s vertex array.

The .md5anim File

The .md5anim file describes a single animation cycle that can be associated with a model.  The .md5anim file consists of several sections that are used to describe the animation.  The first section is the header which describes the content of the rest of the file.  following the header is the “hierarchy” section which describes the joints defined in this animation and must be consistent with the joints that are described in the .md5mesh file that this animation is associated with.  The next section is the “bounds” section which defines an axis-aligned bounding box of the mesh for each frame of the animation.  The “baseframe” section defines the default position and orientation of each joint in the skeleton. And finally there is a “frame” section for each frame that makes up the animation.

The “baseframe” section should not be confused with the model’s bind pose skeleton. The “baseframe” is only the joints default position and orientation before the “frame” data is applied to the joint. It is possible that the “baseframe” section contains all zeros because each “frame” section could define every component that is used to replace the position and orientation of a joint for that frame.

The model’s bindpose skeleton is defined in the model’s “joints” section.

The format of the .md5anim file is as follows:

The .md5anim Header

The first section of the .md5anim file is the file header.  The header describes the rest of the content that is contained in the animation file.  The header consists of the version of this file, the command line that was used to export this file from the DCC software, the number of frames that defines the animation, the number of joints in the skeletal hierarchy, the frame-rate of the animation, and the number of animated components that defines each frame section.

The “MD5Version” parameter defines the version of the file.  In this demo, I will assume this version number is always “10”.

The “commandline” parameter describes the command-line arguments that were used to export this animation from the DCC software. This value will be ignored in the demo application.

The “numFrames” parameter described the number of “frame” sections that this animation will define.

The “numJoints” parameter describes the number of joints that are described in the “hierarchy” section.

The “frameRate” parameter defines the number of frames per second that this animation was created with.  The actual amount of time between each frame can be calculated by taking the reciprocal of the frame-rate.

The “numAnimatedComponents” parameter defines the number of components that each “frame” section defines.  The frame components will be used later to describe the final position and orientation of the skeleton for each frame of the animation.

The “hierarchy” Section

The “hierarchy” section defines the joints of the skeleton in this animation.  The number of the joints and the name of the joints in the “hierarchy” section must match the number and names of the joints described in the model files’s “joints” section.

An example of the “hierarchy” section is shown below:

Each joint in the hierarchy appears on one line of the file.  The joint definition starts with the name of the joint as a string enclosed in quotes.  Following the string name is an index of the the joint’s parent in the joints array.  The root joint will be the only joint without a valid parent so it’s parent’s index will be “-1”.  Following the parent index is the “flags” value which describes how this joint’s position and orientation will be built-up based on the frame data described later.  The last parameter in the joint definition is the first index of the data array defined in the frame data.

Following the “hierarchy” section is the “bounds” section.  The “bounds” section describes an axis-aligned bounding box that defines the dimensions of the model for each frame of the animation.  An example of the “bounds” section is shown below.

Each line of the “bounds” section describes the bounding box’s minimum and maximum points that describe the bounding box of the model for a single frame.  Each of the min, and max points for the bounding box is 3-component vector described in object local space.

The “baseframe” section describes the default position and orientation of each joint before the frame data is applied.  Each position and orientation is described relative to the joint’s parent. To build the final skeleton joint in object-local space, you have to add the position and orientation of the joint’s parent.  An example of the “baseframe” section is:

Each line of the “baseframe” section describes a joint’s default position and orientation.  Since the orientation is defined as a quaternion, 4-components are required to describe the orientation.  The w-component of the quaternion will be calculated manually when the joint for the skeleton frame is built.  This algorithm will be shown later in the article.

Following the “baseframe” section is the “frame” sections.  There is one “frame” section for each frame of the animation defined by the “numFrames” parameter.  An example of the “frame” section is shown below:

Each “frame” section starts with the word “frame” followed by the frame number that this frame describes.  The frame numbers increase sequentially from 0 to (numFrames – 1).  The “frame” section consists of a series of floating-point values that describe the frame data.  The number of floating point values in each frame is determined by the “numAnimatedComponents” parameter read in the header.

Now that we’ve seen the format of the MD5 model and animation files, let’s see how we can create CPP class files to read-in and render the MD5 model at runtime.

The MD5Model Class

The MD5Model class is used to parse the .md5mesh files and to store the data at runtime.  It is also going to be responsible for holding the list of animations that are applied to the model.  In a production environment, it may be appropriate to have a global animation manager class that will store all the animations that can be applied to different MD5Model classes with the same skeleton.  For this demo, I am going to neglect these optimizations for the sake of clarity and ease of implementation.  The MD5Model class will also provide functionality to render the model in OpenGL.

The contents of the header file for the MD5Model class are shown below.

The header starts by including the MD5Animation class definition. This class will be shown later, but at this time you only have to know that this class will hold the information necessary to describe a single animation that is associated with the model.

On line 11, the class’s public functions are defined. The LoadModel function will load the model’s mesh data from a .md5mesh file. The LoadAnim function will load the animation data from a .md5anim file and store the animation data in the MD5Animation instance. The Update and Render methods will update the animation and render the animated model.

On line 17, types are defined for the position, normal, texture, and index buffers that are used to render the model’s meshes in OpenGL. Each mesh will have it’s own buffers that describe the mesh’s geometry.

Starting on line 22 structures are defined to store the information defined in the .md5mesh file that was described earlier. I won’t repeat what was said in the section that described the .md5mesh file format. The only addition to these structures is the Mesh structure that adds the members that are necessary the render the mesh in OpenGL at runtime and the Vertex structure that adds a member variable to store the vertex normal in the vertex’s bind pose in joint-local space. The final vertex normal of the animated mesh will be calculated based on the vertex’s bind pose normal. This is necessary to perform proper lighting calculations on the animated mesh’s vertices.

The PrepareMesh method is used to compute the mesh’s vertex positions based in the joint and weight information and to populate the position, and texture buffers.

The PrepareNormals method is used to pre-compute the mesh’s normals in the bind-pose as well as the normals defined in joint-local space that will be used to quickly calculate the new normals of the animated model.

The RenderMesh method will render a single mesh of the model using OpenGL.

The RenderNormals and RenderSkeleton methods are primarily used to debug the loaded joints and computed normals of the mesh. If the lighting doesn’t look right on the mesh, in most cases it’s because the normals are not computed correctly. The RenderNormals method can be used to determine if the normals are pointing in the right direction and are computed correctly.

The CheckAnimation method will make sure that the loaded animation is appropriate for this particular model file. If the animation skeleton hierarchy doesn’t match with this model’s joints array, the animation will be ignored and the model will simply appear in it’s bind pose.

Starting from line 90 a few private member variables will be defined that will be used to load and display the model.

The MD5Model::LoadModel Method

The MD5Model::LoadModel method is used to load the .md5mesh file and store the data in runtime structures. This method takes as its only parameter a string that describes the location of the .md5mesh file to be loaded. The method will return true if everything went okay, or false if the file could not be loaded.

In most cases this method will simply assert if the pre, or post conditions are not met. Ideally, a message should be logged to the console or to a log file stating what error occurred and the function should return false. This is left as an exercise to the reader.

The first thing this method does is to check the validity of the file name parameter passed to the function. It does this using the boost::filesystem library functions.

If the file exists and the file size is greater than zero, we will continue to parse the file.

The parent_path variable is used to prefix the texture path in the case the shader parameter points to a texture with a relative path. The param variable is used to store the current parameter in the parsed file and the junk variable is used to read unused data from the file stream.

Before we start loading the data, I want to make sure that the current joints and mesh arrays are empty so we don’t append more joints and meshes of a previously loaded model file.

On line 41, we’ll read-in the first parameter as a string and while we haven’t reached the end of the file, we’ll continue to parse the file.

The first section of the file we will parse is the header described earlier which includes the MD5Version parameter, the commandline parameter, the numJoints parameter, and the numMeshes parameter.

Since I will only handle MD5 files of version “10”, in this implementation, I simply assert if the version parameter is anything other than “10”. Ideally, you might want to log an error message and return false if the version is not “10”.

Since the commandline parameter will not be used in this demo, I use the IgnoreLine helper method to ignore the rest of the current line in the file.

In the case of numJoints or numMeshes parameter, I store the value in the appropriate member variable and reserve enough space in the arrays to store the input data.

After the header content has been read-in, the “joints” and “mesh” sections will be parsed. Let’s first look at the “joints” section.

The “joints” section begins with the open-brace ‘{‘ character followed by the joint definitions, one on each line. For each joint, the name of the joint is read in, followed by the joint’s parent ID, and then followed by the joint’s position and orientation in object local space.

Before we commit the joint to the joints array, the double-quotes around the name string will be removed and the w-component for the orientation quaternion will be computed. The ComputeQuatW helper function will be used to compute the w-component of the quaternion that was just read in. The ComputeQuatW assumes that the resulting quaternion is of unit length. With this assumption, the w-component of the quaternion can be computed as follows:

Once the joint has been parsed and the w-component of the orientation is computed, the joint is added to the end of the joints array. The “joints” section ends with a closing-brace ‘}’ character which is consumed on line 81.

After the joints have been read-in, the “mesh” sections can be parsed. There is one “mesh” section for each of the meshes contained in the model determined by the numMeshes parameter that was read in the header. Each mesh has several sub-sections: “shader“, “verts“, “tris“, and “weights“. Let’s first look at how the “shader” mesh parameter is parsed.

On line 88, the open-brace ‘{‘ character is read-in. The “mesh” section will be parsed until the next closing-brace ‘}’ character is read-in. The “shader” parameter will usually point to the base texture that is used to render this mesh. If the path to the texture does not have a parent path, the most likely it is a path that is relative to the model file. In this case, the parent path of the model file will be prefixed to the path so the texture loader can find the file relative to the current working folder. If the texture does have a parent path, then the texture is probably already relative to the working folder and the path will be used as-is. In some cases, the texture will not contain an extension. In such a case, I append the default file extension “.tga” to the file. This is the most common extension used for MD5 models but the extension might differ in your situation.

The shader may actually refer to a set of textures that have various post-fixes. In which case it might be the case that there are several textures that define the mesh’s material (such as a specular map, a height map, or a normal map). For the sake of simplicity, I will not elaborate on the handling of these additional textures.

On line 113, the texture data is loaded using the SOIL function and a texture ID is saved in the mesh’s m_TexID member variable.

Following the “shader” parameter is the vertex definition for the mesh. The vertex group starts with the “numverts” parameter which defines the number of vertices that must be parsed, one per line of the file.

Each vertex of the mesh starts with the word “vert” followed by the vertex index in the vertex array. Following the vertex index is the 2-d texture coordinate of the vertex, then the index of the first weight that will be applied to this vertex, and the total number of weights that will be applied to this vertex when the vertex is skinned to the model’s joints. The weights array for this mesh will be parsed later. Once the vertex has been parsed, it is added to the mesh’s m_Verts array. Since the texture coordinate will remain static during the animation, it can be added to the texture coordinate buffer and pretty much forgotten about until it’s time to render the mesh.

You probably noticed that the vertex normal is not being stored in the model file. The vertex normals are necessary to compute correct lighting on the mesh. The vertex normals will be computed manually in the MD5Model::PrepareNormals method which will be shown later.

After the vertex definitions comes the triangle definitions. The triangle definitions are nothing more than an index buffer that determines how the mesh’s vertices should be ordered when rendered. Each triangle consists of three indices into the vertex buffer that compose a single triangle of the mesh.

The “numtris” parameter determines how many triangle definitions this mesh contains. Each triangle of the mesh starts with the word “tri” followed by the index of the triangle in the triangle buffer. Since we’re not really concerned with the triangle array, except for rendering the mesh, we’ll just store the 3-tuple indices in the index buffer and forget about the index buffer until it’s time to render the mesh.

After the triangle array comes the weights array of the mesh. Each weight is assigned to exactly one joint defined in the model’s “joints” section.

The “numweights” parameter defines how many weights are defined for the mesh. Each weight is defined on a single line of the file and consists of the word “weight” followed by the index of the joint that this weight is assigned to. After the joint index, the bias of the weight is read. The bias of the weight determines how much of this weight influences the final position of the vertex. The bias is a floating point value and the bias of all the weights associated with a vertex should sum to 1.0. After the bias, the position of the weight in joint-local space is defined. To get the final position of the vertex, the position of each weight has to be converted to object local space, then added to the final vertex position multiplied by the weight bias. This algorithm will be shown later when I describe the MD5Model::PrepareMesh method.

On line 166-168, if we received any other parameter besides the one we expected, that line is ignored. After the mesh has been parsed and the data structures are filled, the MD5Mesh::PrepareMesh method will compute the vertex positions of the mesh in the bind-pose based on the model’s joints and the mesh’s weights array. The MD5Mesh::PrepareNormals method will pre-compute the normals of the mesh in the skeleton’s bind pose. Additionally, the normals of the mesh will be computed in joint-local space so that they can be easily recomputed for the animated mesh. These methods will be shown next.

The MD5Mesh::PrepareMesh Method

The MD5Mesh::PrepareMesh method will compute the mesh’s vertex positions in object-local space based on the model’s joints and the mesh’s weights array.

The MD5Model::PrepareMesh method takes a reference to a mesh as it’s only parameter and returns true if the mesh was successfully processed.

The method loops through the vertices of the mesh, resetting the current position and normal. Even though the vertex normal is not being computed here, setting the normal to zero here prepares it to be computed in the MD5Mesh::PrepareNormal method shown later.

The final vertex position is the sum of the weights positions in object-local space multiplied by the bias of the weight. Since the position of the weight is expressed in joint-local space, it must first be converted to object-local space by rotating the weight’s position by the joint’s orientation and adding it to the joint’s position value. This is shown on lines 248, and 250.

When all of the weights positions in object-local space have been summed, the final vertex position is added to the mesh’s position buffer to be rendered in OpenGL.

The MD5Mesh::PrepareNormals Method

The MD5Mesh::PrepareNormals method will compute the mesh’s normals in the skeleton’s bind pose based on the positions of the mesh’s vertices computed in the MD5Mesh::PrepareMesh method shown earlier.

The general algorithm for computing the mesh’s normals is as follows:

Let’s see how this looks in code:

The mesh’s triangles can be easily read from the mesh’s m_Tris member variable to get the vertices that make up a single triangle in the mesh. On line 298, the triangle normal is computed by taking the cross-product of two of the triangle’s edges and the normal is added to the vertex normal for each of the vertices that make up the triangle.

Once we have the summed normals for each vertex in the mesh, these normals need to be normalized in order to ensure the lighting for the vertex is computed correctly. Now we have the vertex normal in the mesh’s bind pose and it’s added to the mesh’s normal buffer.

To compute the animated normal, we can pre-compute the vertex’s normal in joint-local space by rotating the normal by the inverse of the joint’s orientation multiplied by the bias of the weight for each weight that is associated to the vertex. This is shown in lines 318-323 in the source code above.

The MD5Model::Render Method

The MD5Model::Render method will render each mesh of the model. For debugging purposes, this method will also render the model’s animated skeleton and the computed normals for each mesh. The MD5Animation::Render and MD5Model::RenderNormals methods will not be shown here, but you can refer to the class’s source code included at the bottom of this article.

First the world matrix of the model is concatenated with the current matrix. Each mesh of the model is then rendered with the MD5Model::RenderMesh method. Nothing special here. Let’s see how each mesh is rendered.

The MD5Model::RenderMesh Method

The MD5Model::RenderMesh method will render a single mesh of the model.

Before we can render the mesh in OpenGL with the buffers we specified earlier, we must first enable the client states for each buffer we will be sending to the GPU. For our meshes, we have a position buffer, a normal buffer, and a texture coordinate buffer. on lines 371-374, the pointer to the first element of our buffers are pushed into the display list and on line 376, the mesh is actually rendered by pushing the geometric elements to the GPU.

After the geometry has been rendered, we have to restore the OpenGL state so that another call to glDrawElements doesn’t behave unexpectedly.

The MD5Animation Class

The animation functionality has been separated into another class called MD5Animation. The main responsibility of the MD5Animation class is to load and parse the .md5anim file and animate the skeleton. Let’s first take a look at the class’s header file.

The LoadAnimation method is used to load and parse the animation data from a .md5anim file. The Update method is used to update the animation’s skeleton between frames and the Render method is used to render the debug skeleton in it’s animated pose.

Starting from line 18 a few structures are defined that will be used to store the skeletal information from the .md5anim file.

On line 74 the GetSkeleton method will be used to retrieve the animated skeleton by the MD5Model class in order to update it’s vertex positions.

The BuildFrameSkeleton method is used to build a pose’d skeleton for a single frame based on the FrameData that is read from the .md5anim file.

The InterpolateSkeletons method is used to compute the animated skeleton pose between two frames.

The MD5Animation::LoadAnimation Method

The MD5Animation::LoadAnimation method takes the path to the .md5anim file that defines the animations. This method will return true if the animation was successfully loaded.

The first thing this method does is check if the file exists and the file is not empty. If these tests pass, the current animation’s arrays are cleared to load the new animation.

The file path this method expects is either a file path that is relative to the current working directory (usually relative to the executable file or if you are running from Visual Studio, this will be relative to the project file).

If the file exists and isn’t empty, the file will be parsed. The .md5anim header information will first be read-in.

For this demo, I will only support MD5Version 10. If the file encounters any other file version, it will fail to load.

The commandline parameter is not used so if this parameter is encountered, it and everything that comes after it on that line is ignored.

The numFrames parameter store the number of frames that are used to define the animation and determines how many “frame” sections will be parsed later in the file.

The numJoints parameter determines the number of joints that are defined in the “hierarchy” section which will be parsed next.

The frameRate parameter stores the number of frames per second that are defined in this animation file. To determine how much time there is between frames, simply take the reciprocal of the frame-rate.

The numAnimatedComponents parameter determines how many components will appear in each “frame” section later.

Immediately following the header comes the “hierarchy” section. The “hierarchy” section defines the joints of the skeleton that are used by this animation.

The “hierarchy” keyword is immediately followed by the open-brace character ‘{‘. Each line in the “hierarchy” section defines a single joint which consists of the joint’s name enclosed in double-quotes followed by the index of the parent joint in the joint’s array, the flags bit-field and finally the index of the first element in the frame’s components array that is to be applied to the joint when the frame skeleton is built.

The joint’s flags bit-field is used to determine which components of the frame data should be used to build the final position and orientation of the joint for that particular frame. The first bit indicates that the x-component of the joint’s baseframe position should be replaced by the frame data component at the StartIndex position in the frame data. The second bit determines if the y-component of the joint’s baseframe position should be replaced by the next component in the frame data array, and so-on until the 6th bit which if set will cause the z-component of the joint’s baseframe orientation quaternion to be replaced by the next component in the frame data array for that frame. This algorithm will be shown in more detail when the frame skeleton is built for each frame of the animation.

After the joint has been parsed, the joint definition is added to the m_JointInfos array.

After the “hierarchy” section has been parsed, the “bounds” section will be parsed. Each frame of the animation has a bounding box that is used to determine the axis-aligned bounding box for the animated model for each frame of the animation.

Each line in the “bounds” section defines the axis-aligned bounding box that completely contains the animated skeleton for the frame of the animation. The bound definition consists of two 3-component vectors enclosed in parentheses ‘(,)’. The first vector is the minimum coordinate for the bounding volume and the second component is the maximum coordinate for the bounding volume.

The “baseframe” section determines the bind pose for each joint of the skeleton. The base-frame data is used as a bases for each frame of the animation and is used as the default position and orientation of the joint before the animation frame is calculated. This is shown in more detail in the MD5Animation::BuildFrameSkeleton method.

Each line in the “baseframe” section defines the default position and orientation of a joint in the skeletal hierarchy. The position and the orientation are described as a 3-component vector enclosed in parentheses ‘(,)’.

For each frame of the animation there is a “frame” section in the file. The “frame” consists of an array of numbers whose meaning is determined by the joint’s flags bitfield. After the frame data array has been parsed, the a frame skeleton can be built based on the frame data array. The implementation of the method that builds the frame skeleton will be shown next.

Each “frame” starts with the word “frame” followed by the frame number starting at 0, to (numFrames – 1 ).

Each “frame” section consists of numAnimatedComponents floating point values that are used to define the joint information that will be used to build the frame skeleton.

After the frame data has been parsed, we should have enough information to build the frame skeleton for that frame. On line 149, the MD5Animation::BuildFrameSkeleton method is invoked to build the skeleton pose for that frame.

After all the different sections of the .md5anim file have been parsed and processed, a few member variables are initialized that are used during animation.

If the file was parsed and processed, the function will return true

The MD5Animation::BuildFrameSkeleton Method

The MD5Animation::BuildFrameSkeleton method will build the skeleton pose for a single frame of the animation. It does this by combining the baseframe data with the frame data array.

For each joint, the JointInfo and the SkeletonJoint from the base-frame is read. The joint’s m_Flags bit-field member variable is used to determine which components of the base-frame are replaced by the frame data array. Bits 0 through 2 indicate the components of the base frame’s position components should be replaced by the frame data while bits 3 through 5 indicate the components of the base frame’s orientation should be replaced by the frame data.

Once we have the updated animated skeleton joint for the frame, we need to compute the quaternion’s w-component by using the ComputeQuatW helper function.

The resulting animated joint is expressed relative to the parent joint so we need to compute the object-local position and orientation by combining the position and orientation of the parent joint with the current joint. If the joint doesn’t have a parent, it is simply added to the end of the skeleton’s joint array.

Once all of the joints of the skeleton have been processed, the skeleton is pushed to the end of the frame skeletons array. After all the frames have been processed, the frame skeletons array will have one frame skeleton for each frame of the animation. The animated skeleton for

The MD5Animation::Update Method

The MD5Animation::Update method is responsible for calculating the frames of the animation and the interpolation factor that is used to calculate the “in-between” position of the skeleton in order to calculate the exact pose of the skeleton for the current time-step.

The m_fAnimTime is updated based on the elapsed time since this method was last called and the value is then clamped between 0 and m_fAnimDuration so that we don’t try to play a frame of the animation that doesn’t exist.

The first frame index (iFrame0) and the next frame index (iFrame1) are calculated at the ratio of interpolation is computed.

On line 250, the two frame skeleton poses and the interpolation ratio is passed to the MD5Animation::InterpolateSkeletons method and the resulting skeleton pose is stored in the m_AnimatedSkeleton member variable.

The MD5Animation::InterpolateSkeletons Method

To compute the final skeleton pose, the MD5Animation::InterpolateSkeletons method is used. The final skeleton is simply an interpolation of each joint in the previous and next frame poses.

For each joint, the joints for the previous frame and the next frame are read and the positions and orientations are interpolated to compute the final skeleton joint. That’s it! If you were hoping for a big long complicated function then I’m sorry to disappoint you.

The glm::mix library function is equivalent to a spherical linear interpolation between two quaternions which is exactly what we need for our animation.

Now that we have the animated skeleton pose, we need to go back to the MD5Model class to see how the model’s final vertex position and normals are computed.

The MD5Model::Update Method

I skipped the explanation of this method previously in the section which dealt with loading the MD5 model file. Now that we have an animation to assign to the model, I can show how to apply that animation to the vertices of the mesh.

First the animation is updated and the resulting animated skeleton is retrieved. Then, for each mesh of the model the animated skeleton pose is applied to the mesh.

The MD5Model::PrepareMesh Method

The first version of the MD5Model::PrepareMesh method we saw computed the vertex positions of the model’s meshes in the default pose of the model determined by the initial positions and orientations of the skeleton. This version takes an animated skeleton as an argument to the method and computes the animated position of the mesh’s vertices as well as the vertex normal in the animated orientation.

The method accepts the mesh that is to be animated as well as the skeleton that represents the pose of the model.

For each vertex of the mesh, the vertex the position and normal are reset to zero. Then we loop through the weights that are associated with the vertex and for each weight the sum of the weight positions in object local space are applied to the final vertex position.

Since the vertex normal was precomputed in joint local space in the MD5Model::PrepareNormals method we can use that vertex normal to compute the animated vertex normal by rotating it by the animated skeleton joint’s orientation multiplied by the bias of the weight as is shown on line 279.

The MD5Model::CheckAnimation Method

In order for all of this to work, the loaded animation file must match the skeleton joints of the model file. To check this, we will use the MD5Model::CheckAnimation method. If the animation’s joints don’t match up with the model’s joints, the animation will be ignored and the model will appear in it’s default bind pose.

This method is fairly self explanatory. If either the number of joints differ between the model and the animation, or any of the joint’s names or parent ID’s are not the same, this method will return false and the animation will be ignored.

Video

The resulting animation should look something like what is shown in the video below.

The video shows the “Bob with Lamp” model that I downloaded from http://www.katsbits.com/download/models/ provided by “der_ton”.

Conclusion

This article tries to show briefly one possible implementation for loading and animating models stored in the MD5 file format. Although it may be suitable for a demo application, some additions will need to be implemented in order to make these classes suitable for a production environment.

Resources

The primary resource for this article is provided by David Henry in his article written on August 21st, 2005. The original article can be found at http://tfc.duke.free.fr/coding/md5-specs-en.html.

The model used for this demo is downloaded from http://www.katsbits.com/download/models/.

Download the Source

You can download the source including solution files and project files for Visual Studio 2008 here:
[MD5ModelLoader.zip]

You can download the source including solution files and project files for Visual Studio 2010 here:
[MD5ModelLoader.zip (VS2010)]

46 thoughts on “Loading and Animating MD5 Models with OpenGL

  1. thats so awesome.. the first website with a tutorial and example about skeleton animation.. wich will make stuff much more easier from now on thanks alot

  2. Can someone expalin me these lines in MD5Animation.cpp after line 180:

    const JointInfo& jointInfo = jointInfos[i];
    ……
    if ( jointInfo.m_Flags & 1 ) // Pos.x
    {
    animatedJoint.m_Pos.x = frameData.m_FrameData[jointInfo.m_StartIndex + j++ ];
    }
    …..

    • The jointInfo.m_Flags is a bitfield that represents the data in the frameData array that should replace the original animated joint’s position and orientation. If all the bits are set then every component of the original position and rotation of the joint will be replaced by the data in the frameData array (for that frame).

      This is a type of compression for the animation data. If the value doesn’t change, then it doesn’t need to be stored in the animation file.

    • Very good idea. I have another tutorial on terrain rendering and currently its using the fixed-function pipeline but I want to create a tutorial on how to implement terrains properly using tessellation and texture blending in the fragment shader.

  3. Hi there! This is a good tutorial, but unfortunately, it’s using a fixed pipeline…
    I’m in college and our instructor gave us an assignment where we have to load and display a mesh with a skeleton animation. I’ve converted from your fixed pipeline to the shaders we’re using. Although I’m coming across a big problem that no matter how much I poke at it, it still happens…

    When the animation plays, parts of the vertices seem to completely disappear! The lower on the mesh the glitchier it gets. I’m wondering why does the animation plays so smoothly with a fixed pipeline while it doesn’t with shaders?

    • Fred,

      This tutorial is written as an “introduction to OpenGL”. I also made a version of this that uses Cg to transform the vertices of the model.

      GPU Skinning of MD5 Models in OpenGL and Cg:
      http://3dgep.com/?p=1356

      I can only guess why the vertices are disappearing in your demo but I think it might have something to do with the number of weights per vertex is limited to 4. If you have more weights per-vertex, then this is a problem using the programmable shader pipeline. The Bob model used in this demo limits the number of weights per vertex to 4.

      • Thank you for your reply! Although I’m not sending any of the weights to the shader, all that math is still done on the CPU. All I’m doing is uploading the new vertices into a double buffered vao/vbo (To avoid uploading data while drawing it if it can ever occur..). I’m also using the BobLamp lamp model. The only code I’ve changed is the drawing bits. I guess what I really should ask is if you’d be willing to help me deeper into my problem, as I can understand with just this vague information it’d be hard to troubleshoot. Or perhaps even pointers on how I should be uploading your buffers onto the vao/vbo to make sure everything is drawn correctly! Me and my instructor cannot seem to figure this one out! Again, thank you for your reply and taking the time to read my comments, this demo is still quite useful!

    • The vertices disappearing is due to glm. The quaternion’s “mix” fuction in latest version of glm has a problem so that some inputs may get bad output. Try to replace the “mix” function with a older version.(The version in this article’s source download is correct)

  4. Hello!
    I now come to talk with good news!
    I was able to figure out the source of the problem.
    For some reason when calculating the orientations of the joins when loading the animation, glm::mix() returned invalid (Super small floats) quaternions when trying to interpolate between two same quaternions… So I ended up doing:
    finalJoint.m_Orient.x = (joint1.m_Orient.x * fInterpolate) + (joint0.m_Orient.x * (1-fInterpolate));
    finalJoint.m_Orient.y = (joint1.m_Orient.y * fInterpolate) + (joint0.m_Orient.y * (1-fInterpolate));
    finalJoint.m_Orient.z = (joint1.m_Orient.z * fInterpolate) + (joint0.m_Orient.z * (1-fInterpolate));
    finalJoint.m_Orient.w = (joint1.m_Orient.w * fInterpolate) + (joint0.m_Orient.w * (1-fInterpolate));

    instead, works perfectly!
    I’m still curious though how the fixed pipeline worked fine..

  5. Wow, thanks to you (and the original author)! I was about to write a loader myself, your code eased the whole process BIG TIME. After about 90 minutes I had it all up and running in my engine. Very, very nice!

  6. Is there any way to get this md5 loader working with visual studio 2012 without having to rewrite the whole thing seeing as it appears to have been written in 09?

    • SocomTedd, I haven’t tried. What happens if you load the VS2008 solution is VS2012? Do you get any compiler errors? The MD5Model and MD5Animation classes don’t have anything in them that wouldn’t work in VS2012. You may have some trouble with the SOIL library, but the source code for this library is provided, so you should be able to recompile it with the VS2012 compiler.

      Just try to open the projects in VS2012 and recompile. It should work.

  7. Thanks for your great posting and I would like to inform you that I integrated CMake build environment with your source code for my own use. Actually I use MAC and wanted to see skeleton animation on my laptop. I tested it with my MAC OSX 10.7 only and guess this can be buildable on even on Linux. If you are interested in cmake version, here is the link for download. If you dont like my link in open, then let me know.

    http://kevino.tistory.com/entry/MD5-Model-Loader-source-with-CMake

  8. Nice tutorial, but there is not any info on how to create/export MD5 models from Maya/Max etc to even get started

  9. Awesome Tutorial, but Why am I getting this error?

    Cannot open “libboost_filesystem-vc100-mt-1_46.lib”

    In the lib folder they named “libboost_filesystem-vc90-mt-1_46.lib”, however, if I rename them to I am getting a bunch of errors. Anybody can help?

    I am VS2010

    • MrNoway,

      Sorry for the late reply. You are getting this error because the project was created in Visual Studio 2008 and you are opening it with Visual Studio 2010. Boost’s header files will automatically link the correct libraries based on your build environment. In your case, you are using Visual Studio 2010 so boost will try to load the libs for that build environment.

      The solution is to download the 1.46.1 boost libraries:
      http://sourceforge.net/projects/boost/files/boost/1.46.1/
      And rebuild the boost libraries for Visual Studio 2010.

      • i try to using the boost 1.46.1 rebuild the environment in VC2010, but cannot create the four .lib file ( libboost_filesystem-vc100-mt-1_46_1.lib, libboost_filesystem-vc100-mt-1_46_1,libboost_system-vc100-mt-gd-1_46_1, and libboost_system-vc100-mt-1_46_1), may be my step is wrong, Anybody can tell me step ? thank!

        • Shing,

          Mozart had the same question. I’ve provided an additional download link for people using VS 2010. This project is using boost 1.46.0. I’ve recompiled the libs for this version of boost with VS2010 and included them in the additional download at the end of the article.

      • i try to using the boost 1.46.1 rebuild the environment in VS2010, but cannot create the four .lib file ( libboost_filesystem-vc100-mt-1_46_1.lib, libboost_filesystem-vc100-mt-1_46_1,libboost_system-vc100-mt-gd-1_46_1, and libboost_system-vc100-mt-1_46_1), may be my step is wrong, Anybody can tell me step ? thank!

        • Mozart,

          I have created a zip file that contains a solution file for visual studio 2010 including the boost 1.46.0 pre-built libraries. You should be able to download this version for VS2010 and open and build it directly without any errors (just ignore the warnings generated by the SOIL library).

  10. i try to using 1.46.1 boost libraries, but final have a same error “cannot open file ‘libboost_filesystem-vc100-mt-gd-1_46_1.lib”, on the ..\externals\boost_1_46_1\libs cannot find a ‘libboost_filesystem-vc100-mt-gd-1_46_1.lib…
    Anybody can help?

    • Shing,

      I have provided a link to a zip file that contains the project and solution files for VS2010 (including the pre-built boost libraries required to build the project in VS2010). Please see the link at the end of the article.

  11. Nice tutorial!!
    I have a question :
    Why if I insert another .md5mesh file,it doesn’t open the window?
    I export correctly my md5mesh file with blender, but i can’t find a solution.

  12. good, I’ve been using opengl for a while, and I would like to master this topic in applications with VAO VBO, etc. However, I hope to receive help that specifies me how to adapt it to what has already been said, thanks

  13. Thank you so much! I know its an old format but I have an old computer (assimp wouldnt compile in it). I’ve followed the tutorial and it works perfectly.
    I’ve used a 3d model export script for 3ds max in the website you mentioned and had no problems loading a custom model.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.