Loading and Animating Md5 Models With Opengl - 3d Game Engine Programming
Loading and Animating Md5 Models With Opengl - 3d Game Engine Programming
Contents [hide]
1 Introduction
2 Dependencies
3 MD5 Model Format
4 The .md5mesh File
4.1 The .md5mesh Header
4.2 The “joints” section
4.3 The “mesh” section
5 The .md5anim File
5.1 The .md5anim Header
5.2 The “hierarchy” Section
6 The MD5Model Class
6.1 The MD5Model::LoadModel Method
6.2 The MD5Mesh::PrepareMesh Method
6.3 The MD5Mesh::PrepareNormals Method
6.4 The MD5Model::Render Method
6.5 The MD5Model::RenderMesh Method
7 The MD5Animation Class
7.1 The MD5Animation::LoadAnimation Method
7.2 The MD5Animation::BuildFrameSkeleton Method
7.3 The MD5Animation::Update Method
7.4 The MD5Animation::InterpolateSkeletons Method
7.5 The MD5Model::Update Method
7.6 The MD5Model::PrepareMesh Method
7.7 The MD5Model::CheckAnimation Method
8 Video
9 Conclusion
10 Resources
11 Download the Source
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.
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.
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.
C++
1 MD5Version <int:version>
2 commandline <string:commandline>
3
4 numJoints <int:numJoints>
5 numMeshes <int:numMeshes>
6
7 joints {
8 <string:name> <int:parentIndex> ( <vec3:position> ) ( <vec3
9 ...
10 }
11
12 mesh {
13 shader <string:texture>
14
15 numverts <int:numVerts>
16 vert <int:vertexIndex> ( <vec2:texCoords> ) <int:startWeight
17 ...
18
19 numtris <int:numTriangles>
20 tri <int:triangleIndex> <int:vertIndex0> <int:vertIndex1>
21 ...
22
23 numweights <int:numWeights>
24 weight <int:weightIndex> <int:jointIndex> <float:weightBias
25 ...
26
27 }
28 ...
C++
1 MD5Version <int:version>
2 commandline <string:commandline>
3
4 numJoints <int:numJoints>
5 numMeshes <int:numMeshes>
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.
C++
1 joints {
2 <string:name> <int:parentIndex> ( <vec3:position> ) ( <vec3
3 ...
4 }
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.
C++
1 mesh {
2 shader <string:texture>
3 ...
4 }
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.
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.
C++
1 numtris <int:numTriangles>
2 tri <int:triangleIndex> <int:vertIndex0> <int:vertIndex1> <
3 ...
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.
C++
1 numweights <int:numWeights>
2 weight <int:weightIndex> <int:jointIndex> <float:weightBias
3 ...
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 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.
C++
1 MD5Version <int:version>
2 commandline <string:commandline>
3
4 numFrames <int:numFrames>
5 numJoints <int:numJoints>
6 frameRate <int:frameRate>
7 numAnimatedComponents <int:numAnimatedComponents>
8
9 hierarchy {
10 <string:jointName> <int:parentIndex> <int:flags> <int:startIndex
11 ...
12 }
13
14 bounds {
15 ( vec3:boundMin ) ( vec3:boundMax )
16 ...
17 }
18
19 baseframe {
20 ( vec3:position ) ( vec3:orientation )
21 ...
22 }
23
24 frame <int:frameNum> {
25 <float:frameData> ...
26 }
27 ...
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.
C++
1 MD5Version <int:version>
2 commandline <string:commandline>
3
4 numFrames <int:numFrames>
5 numJoints <int:numJoints>
6 frameRate <int:frameRate>
7 numAnimatedComponents <int:numAnimatedComponents>
The “MD5Version” parameter defines the version of the file. In this demo, I
will assume this version number is always “10”.
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 “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.
C++
1 hierarchy {
2 <string:jointName> <int:parentIndex> <int:flags> <int:startIndex
3 ...
4 }
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.
C++
1 bounds {
2 ( vec3:boundMin ) ( vec3:boundMax )
3 ...
4 }
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.
C++
1 baseframe {
2 ( vec3:position ) ( vec3:orientation )
3 ...
4 }
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.
C++
1 frame <int:frameNum> {
2 <float:frameData> ...
3 }
4 ...
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 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.
MD5Model.h C++
1 #pragma once;
2
3 #include "MD5Animation.h"
4
5 class MD5Model
6 {
7 public:
8 MD5Model();
9 virtual ~MD5Model();
10
11 bool LoadModel( const std::string& filename );
12 bool LoadAnim( const std::string& filename );
13 void Update( float fDeltaTime );
14 void Render();
15
16 protected:
17 typedef std::vector<glm::vec3> PositionBuffer;
18 typedef std::vector<glm::vec3> NormalBuffer;
19 typedef std::vector<glm::vec2> Tex2DBuffer;
20 typedef std::vector<GLuint> IndexBuffer;
21
22 struct Vertex
23 {
24 glm::vec3 m_Pos;
25 glm::vec3 m_Normal;
26 glm::vec2 m_Tex0;
27 int m_StartWeight;
28 int m_WeightCount;
29 };
30 typedef std::vector<Vertex> VertexList;
31
32 struct Triangle
33 {
34 int m_Indices[3];
35 };
36 typedef std::vector<Triangle> TriangleList;
37
38 struct Weight
39 {
40 int m_JointID;
41 float m_Bias;
42 glm::vec3 m_Pos;
43 };
44 typedef std::vector<Weight> WeightList;
45
46 struct Joint
47 {
48 std::string m_Name;
49 int m_ParentID;
50 glm::vec3 m_Pos;
51 glm::quat m_Orient;
52 };
53 typedef std::vector<Joint> JointList;
54
55 struct Mesh
56 {
57 std::string m_Shader;
58 // This vertex list stores the vertices in the bind pose.
59 VertexList m_Verts;
60 TriangleList m_Tris;
61 WeightList m_Weights;
62
63 // A texture ID for the material
64 GLuint m_TexID;
65
66 // These buffers are used for rendering the animated mesh
67 PositionBuffer m_PositionBuffer; // Vertex position stream
68 NormalBuffer m_NormalBuffer; // Vertex normals stream
69 Tex2DBuffer m_Tex2DBuffer; // Texture coordinate set
70 IndexBuffer m_IndexBuffer; // Vertex index buffer
71 };
72 typedef std::vector<Mesh> MeshList;
73
74 // Prepare the mesh for rendering
75 // Compute vertex positions and normals
76 bool PrepareMesh( Mesh& mesh );
77 bool PrepareMesh( Mesh& mesh, const MD5Animation::FrameSkeleton
78 bool PrepareNormals( Mesh& mesh );
79
80 // Render a single mesh of the model
81 void RenderMesh( const Mesh& mesh );
82 void RenderNormals( const Mesh& mesh );
83
84 // Draw the skeleton of the mesh for debugging purposes.
85 void RenderSkeleton( const JointList& joints );
86
87 bool CheckAnimation( const MD5Animation& animation )
88 private:
89
90 int m_iMD5Version;
91 int m_iNumJoints;
92 int m_iNumMeshes;
93
94 bool m_bHasAnimation;
95
96 JointList m_Joints;
97 MeshList m_Meshes;
98
99 MD5Animation m_Animation;
100
101 glm::mat4x4 m_LocalToWorldMatrix;
102
103 };
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.
The RenderMesh method will render a single mesh of the model using
OpenGL.
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.
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.
MD5Model.cpp C++
19 bool MD5Model::LoadModel( const std::string& filename )
20 {
21 if ( !fs::exists(filename) )
22 {
23 std::cerr << "MD5Model::LoadModel: Failed to find file: "
24 return false;
25 }
26
27 fs::path filePath = filename;
28 // store the parent path used for loading images relative to this file.
29 fs::path parent_path = filePath.parent_path();
30
31 std::string param;
32 std::string junk; // Read junk from the file
33
34 fs::ifstream file(filename);
35 int fileLength = GetFileLength( file );
36 assert( fileLength > 0 );
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.
MD5Model.cpp C++
38 m_Joints.clear();
39 m_Meshes.clear();
40
41 file >> param;
42
43 while ( !file.eof() )
44 {
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.
MD5Model.cpp C++
45 if ( param == "MD5Version" )
46 {
47 file >> m_iMD5Version;
48 assert( m_iMD5Version == 10 );
49 }
50 else if ( param == "commandline" )
51 {
52 IgnoreLine(file, fileLength ); // Ignore the contents of the line
53 }
54 else if ( param == "numJoints" )
55 {
56 file >> m_iNumJoints;
57 m_Joints.reserve(m_iNumJoints);
58 }
59 else if ( param == "numMeshes" )
60 {
61 file >> m_iNumMeshes;
62 m_Meshes.reserve(m_iNumMeshes);
63 }
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.
MD5Model.cpp C++
64 else if ( param == "joints" )
65 {
66 Joint joint;
67 file >> junk; // Read the '{' character
68 for ( int i = 0; i < m_iNumJoints; ++i )
69 {
70 file >> joint.m_Name >> joint.m_ParentID >>
71 >> joint.m_Pos.x >> joint.m_Pos.y >>
72 >> joint.m_Orient.x >> joint.m_Orient
73
74 RemoveQuotes( joint.m_Name );
75 ComputeQuatW( joint.m_Orient );
76
77 m_Joints.push_back(joint);
78 // Ignore everything else on the line up to the end‐of‐line characte
79 IgnoreLine( file, fileLength );
80 }
81 file >> junk; // Read the '}' character
82 }
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:
Helpers.cpp C++
30 void ComputeQuatW( glm::quat& quat )
31 {
32 float t = 1.0f ‐ ( quat.x * quat.x ) ‐ ( quat.y * quat
33 if ( t < 0.0f )
34 {
35 quat.w = 0.0f;
36 }
37 else
38 {
39 quat.w = ‐sqrtf(t);
40 }
41 }
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.
MD5Model.cpp C++
83 else if ( param == "mesh" )
84 {
85 Mesh mesh;
86 int numVerts, numTris, numWeights;
87
88 file >> junk; // Read the '{' character
89 file >> param;
90 while ( param != "}" ) // Read until we get to the '}' character
91 {
92 if ( param == "shader" )
93 {
94 file >> mesh.m_Shader;
95 RemoveQuotes( mesh.m_Shader );
96
97 fs::path shaderPath( mesh.m_Shader );
98 fs::path texturePath;
99 if ( shaderPath.has_parent_path() )
100 {
101 texturePath = shaderPath;
102 }
103 else
104 {
105 texturePath = parent_path / shaderPath
106 }
107
108 if ( !texturePath.has_extension() )
109 {
110 texturePath.replace_extension( ".tga"
111 }
112
113 mesh.m_TexID = SOIL_load_OGL_texture(
114
115 file.ignore(fileLength, '\n' ); // Ignore everything else on th
116 }
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.
MD5Model.cpp C++
117 else if ( param == "numverts")
118 {
119 file >> numVerts; // Read in the vertices
120 IgnoreLine(file, fileLength);
121 for ( int i = 0; i < numVerts; ++i )
122 {
123 Vertex vert;
124
125 file >> junk >> junk >> junk
126 >> vert.m_Tex0.x >> vert.m_Tex0
127 >> vert.m_StartWeight >> vert
128
129 IgnoreLine(file, fileLength);
130
131 mesh.m_Verts.push_back(vert);
132 mesh.m_Tex2DBuffer.push_back(vert
133 }
134 }
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.
MD5Model.cpp C++
135 else if ( param == "numtris" )
136 {
137 file >> numTris;
138 IgnoreLine(file, fileLength);
139 for ( int i = 0; i < numTris; ++i )
140 {
141 Triangle tri;
142 file >> junk >> junk >> tri.m_Indices
143
144 IgnoreLine( file, fileLength );
145
146 mesh.m_Tris.push_back(tri);
147 mesh.m_IndexBuffer.push_back( (GLuint
148 mesh.m_IndexBuffer.push_back( (GLuint
149 mesh.m_IndexBuffer.push_back( (GLuint
150 }
151 }
MD5Model.cpp C++
152 else if ( param == "numweights" )
153 {
154 file >> numWeights;
155 IgnoreLine( file, fileLength );
156 for ( int i = 0; i < numWeights; ++i
157 {
158 Weight weight;
159 file >> junk >> junk >> weight.m_JointID
160 >> weight.m_Pos.x >> weight.m_Pos
161
162 IgnoreLine( file, fileLength );
163 mesh.m_Weights.push_back(weight);
164 }
165 }
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.
MD5Model.cpp C++
166 else
167 {
168 IgnoreLine(file, fileLength);
169 }
170
171 file >> param;
172 }
173
174 PrepareMesh(mesh);
175 PrepareNormals(mesh);
176
177 m_Meshes.push_back(mesh);
178
179 }
180
181 file >> param;
182 }
183
184 assert( m_Joints.size() == m_iNumJoints );
185 assert( m_Meshes.size() == m_iNumMeshes );
186
187 return true;
188 }
MD5Model.cpp C++
227 bool MD5Model::PrepareMesh( Mesh& mesh )
228 {
229 mesh.m_PositionBuffer.clear();
230 mesh.m_Tex2DBuffer.clear();
231
232 // Compute vertex positions
233 for ( unsigned int i = 0; i < mesh.m_Verts.size(); ++
234 {
235 glm::vec3 finalPos(0);
236 Vertex& vert = mesh.m_Verts[i];
237
238 vert.m_Pos = glm::vec3(0);
239 vert.m_Normal = glm::vec3(0);
240
241 // Sum the position of the weights
242 for ( int j = 0; j < vert.m_WeightCount; ++j )
243 {
244 Weight& weight = mesh.m_Weights[vert.m_StartWeight
245 Joint& joint = m_Joints[weight.m_JointID];
246
247 // Convert the weight position from Joint local space to object space
248 glm::vec3 rotPos = joint.m_Orient * weight.m_Pos
249
250 vert.m_Pos += ( joint.m_Pos + rotPos ) * weight
251 }
252
253 mesh.m_PositionBuffer.push_back(vert.m_Pos);
254 mesh.m_Tex2DBuffer.push_back(vert.m_Tex0);
255 }
256
257 return true;
258 }
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.
MD5Model.cpp C++
287 bool MD5Model::PrepareNormals( Mesh& mesh )
288 {
289 mesh.m_NormalBuffer.clear();
290
291 // Loop through all triangles and calculate the normal of each triangle
292 for ( unsigned int i = 0; i < mesh.m_Tris.size(); ++i
293 {
294 glm::vec3 v0 = mesh.m_Verts[ mesh.m_Tris[i].m_Indices
295 glm::vec3 v1 = mesh.m_Verts[ mesh.m_Tris[i].m_Indices
296 glm::vec3 v2 = mesh.m_Verts[ mesh.m_Tris[i].m_Indices
297
298 glm::vec3 normal = glm::cross( v2 ‐ v0, v1 ‐ v0 )
299
300 mesh.m_Verts[ mesh.m_Tris[i].m_Indices[0] ].m_Normal
301 mesh.m_Verts[ mesh.m_Tris[i].m_Indices[1] ].m_Normal
302 mesh.m_Verts[ mesh.m_Tris[i].m_Indices[2] ].m_Normal
303 }
304
305 // Now normalize all the normals
306 for ( unsigned int i = 0; i < mesh.m_Verts.size(); ++
307 {
308 Vertex& vert = mesh.m_Verts[i];
309
310 glm::vec3 normal = glm::normalize( vert.m_Normal
311 mesh.m_NormalBuffer.push_back( normal );
312
313 // Reset the normal to calculate the bind‐pose normal in joint space
314 vert.m_Normal = glm::vec3(0);
315
316 // Put the bind‐pose normal into joint‐local space
317 // so the animated normal can be computed faster later
318 for ( int j = 0; j < vert.m_WeightCount; ++j )
319 {
320 const Weight& weight = mesh.m_Weights[vert.m_StartWeight
321 const Joint& joint = m_Joints[weight.m_JointID
322 vert.m_Normal += ( normal * joint.m_Orient )
323 }
324 }
325
326 return true;
327 }
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.
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.
MD5Model.cpp C++
343 void MD5Model::Render()
344 {
345 glPushMatrix();
346 glMultMatrixf( glm::value_ptr(m_LocalToWorldMatrix) )
347
348 // Render the meshes
349 for ( unsigned int i = 0; i < m_Meshes.size(); ++i )
350 {
351 RenderMesh( m_Meshes[i] );
352 }
353
354 m_Animation.Render();
355
356 for ( unsigned int i = 0; i < m_Meshes.size(); ++i )
357 {
358 RenderNormals( m_Meshes[i] );
359 }
360
361 glPopMatrix();
362 }
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.
MD5Model.cpp C++
364 void MD5Model::RenderMesh( const Mesh& mesh )
365 {
366 glColor3f( 1.0f, 1.0f, 1.0f );
367 glEnableClientState( GL_VERTEX_ARRAY );
368 glEnableClientState( GL_TEXTURE_COORD_ARRAY );
369 glEnableClientState( GL_NORMAL_ARRAY );
370
371 glBindTexture( GL_TEXTURE_2D, mesh.m_TexID );
372 glVertexPointer( 3, GL_FLOAT, 0, &(mesh.m_PositionBuffer
373 glNormalPointer( GL_FLOAT, 0, &(mesh.m_NormalBuffer[0
374 glTexCoordPointer( 2, GL_FLOAT, 0, &(mesh.m_Tex2DBuffer
375
376 glDrawElements( GL_TRIANGLES, mesh.m_IndexBuffer.size
377
378 glDisableClientState( GL_NORMAL_ARRAY );
379 glDisableClientState( GL_TEXTURE_COORD_ARRAY );
380 glDisableClientState( GL_VERTEX_ARRAY );
381
382 glBindTexture( GL_TEXTURE_2D, 0 );
383 }
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 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.
MD5Animation.h C++
1 #pragma once;
2
3 class MD5Animation
4 {
5 public:
6 MD5Animation();
7 virtual ~MD5Animation();
8
9 // Load an animation from the animation file
10 bool LoadAnimation( const std::string& filename );
11 // Update this animation's joint set.
12 void Update( float fDeltaTime );
13 // Draw the animated skeleton
14 void Render();
15
16 // The JointInfo stores the information necessary to build the
17 // skeletons for each frame
18 struct JointInfo
19 {
20 std::string m_Name;
21 int m_ParentID;
22 int m_Flags;
23 int m_StartIndex;
24 };
25 typedef std::vector<JointInfo> JointInfoList;
26
27 struct Bound
28 {
29 glm::vec3 m_Min;
30 glm::vec3 m_Max;
31 };
32 typedef std::vector<Bound> BoundList;
33
34 struct BaseFrame
35 {
36 glm::vec3 m_Pos;
37 glm::quat m_Orient;
38 };
39 typedef std::vector<BaseFrame> BaseFrameList;
40
41 struct FrameData
42 {
43 int m_iFrameID;
44 std::vector<float> m_FrameData;
45 };
46 typedef std::vector<FrameData> FrameDataList;
47
48 // A Skeleton joint is a joint of the skeleton per frame
49 struct SkeletonJoint
50 {
51 SkeletonJoint()
52 : m_Parent(‐1)
53 , m_Pos(0)
54 {}
55
56 SkeletonJoint( const BaseFrame& copy )
57 : m_Pos( copy.m_Pos )
58 , m_Orient( copy.m_Orient )
59 {}
60
61 int m_Parent;
62 glm::vec3 m_Pos;
63 glm::quat m_Orient;
64 };
65 typedef std::vector<SkeletonJoint> SkeletonJointList;
66
67 // A frame skeleton stores the joints of the skeleton for a single frame.
68 struct FrameSkeleton
69 {
70 SkeletonJointList m_Joints;
71 };
72 typedef std::vector<FrameSkeleton> FrameSkeletonList;
73
74 const FrameSkeleton& GetSkeleton() const
75 {
76 return m_AnimatedSkeleton;
77 }
78
79 int GetNumJoints() const
80 {
81 return m_iNumJoints;
82 }
83
84 const JointInfo& GetJointInfo( unsigned int index ) const
85 {
86 assert( index < m_JointInfos.size() );
87 return m_JointInfos[index];
88 }
89
90 protected:
91
92 JointInfoList m_JointInfos;
93 BoundList m_Bounds;
94 BaseFrameList m_BaseFrames;
95 FrameDataList m_Frames;
96 FrameSkeletonList m_Skeletons; // All the skeletons for all the frames
97
98 FrameSkeleton m_AnimatedSkeleton;
99
100 // Build the frame skeleton for a particular frame
101 void BuildFrameSkeleton( FrameSkeletonList& skeletons
102 void InterpolateSkeletons( FrameSkeleton& finalSkeleton
103
104 private:
105 int m_iMD5Version;
106 int m_iNumFrames;
107 int m_iNumJoints;
108 int m_iFramRate;
109 int m_iNumAnimatedComponents;
110
111 float m_fAnimDuration;
112 float m_fFrameDuration;
113 float m_fAnimTime;
114 };
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 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.
MD5Animation.cpp C++
22 bool MD5Animation::LoadAnimation( const std::string& filename
23 {
24 if ( !fs::exists(filename) )
25 {
26 std::cerr << "MD5Animation::LoadAnimation: Failed to find file: "
27 return false;
28 }
29
30 fs::path filePath = filename;
31
32 std::string param;
33 std::string junk; // Read junk from the file
34
35 fs::ifstream file(filename);
36 int fileLength = GetFileLength( file );
37 assert( fileLength > 0 );
38
39 m_JointInfos.clear();
40 m_Bounds.clear();
41 m_BaseFrames.clear();
42 m_Frames.clear();
43 m_AnimatedSkeleton.m_Joints.clear();
44 m_iNumFrames = 0;
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.
MD5Animation.cpp C++
46 file >> param;
47
48 while( !file.eof() )
49 {
50 if ( param == "MD5Version" )
51 {
52 file >> m_iMD5Version;
53 assert( m_iMD5Version == 10 );
54 }
55 else if ( param == "commandline" )
56 {
57 file.ignore( fileLength, '\n' ); // Ignore everything else on the line
58 }
59 else if ( param == "numFrames" )
60 {
61 file >> m_iNumFrames;
62 file.ignore( fileLength, '\n' );
63 }
64 else if ( param == "numJoints" )
65 {
66 file >> m_iNumJoints;
67 file.ignore( fileLength, '\n' );
68 }
69 else if ( param == "frameRate" )
70 {
71 file >> m_iFramRate;
72 file.ignore( fileLength, '\n' );
73 }
74 else if ( param == "numAnimatedComponents" )
75 {
76 file >> m_iNumAnimatedComponents;
77 file.ignore( fileLength, '\n' );
78 }
For this demo, I will only support MD5Version 10. If the file encounters any
other file version, it will fail to load.
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.
MD5Animation.cpp C++
79 else if ( param == "hierarchy" )
80 {
81 file >> junk; // read in the '{' character
82 for ( int i = 0; i < m_iNumJoints; ++i )
83 {
84 JointInfo joint;
85 file >> joint.m_Name >> joint.m_ParentID >>
86 RemoveQuotes( joint.m_Name );
87
88 m_JointInfos.push_back(joint);
89
90 file.ignore( fileLength, '\n' );
91 }
92 file >> junk; // read in the '}' character
93 }
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.
MD5Animation.cpp C++
94 else if ( param == "bounds" )
95 {
96 file >> junk; // read in the '{' character
97 file.ignore( fileLength, '\n' );
98 for ( int i = 0; i < m_iNumFrames; ++i )
99 {
100 Bound bound;
101 file >> junk; // read in the '(' character
102 file >> bound.m_Min.x >> bound.m_Min.y >>
103 file >> junk >> junk; // read in the ')' and '(' characters.
104 file >> bound.m_Max.x >> bound.m_Max.y >>
105
106 m_Bounds.push_back(bound);
107
108 file.ignore( fileLength, '\n' );
109 }
110
111 file >> junk; // read in the '}' character
112 file.ignore( fileLength, '\n' );
113 }
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.
MD5Animation.cpp C++
114 else if ( param == "baseframe" )
115 {
116 file >> junk; // read in the '{' character
117 file.ignore( fileLength, '\n' );
118
119 for ( int i = 0; i < m_iNumJoints; ++i )
120 {
121 BaseFrame baseFrame;
122 file >> junk;
123 file >> baseFrame.m_Pos.x >> baseFrame.m_Pos
124 file >> junk >> junk;
125 file >> baseFrame.m_Orient.x >> baseFrame
126 file.ignore( fileLength, '\n' );
127
128 m_BaseFrames.push_back(baseFrame);
129 }
130 file >> junk; // read in the '}' character
131 file.ignore( fileLength, '\n' );
132 }
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.
MD5Animation.cpp C++
133 else if ( param == "frame" )
134 {
135 FrameData frame;
136 file >> frame.m_iFrameID >> junk; // Read in the '{' character
137 file.ignore(fileLength, '\n' );
138
139 for ( int i = 0; i < m_iNumAnimatedComponents
140 {
141 float frameData;
142 file >> frameData;
143 frame.m_FrameData.push_back(frameData);
144 }
145
146 m_Frames.push_back(frame);
147
148 // Build a skeleton for this frame
149 BuildFrameSkeleton( m_Skeletons, m_JointInfos
150
151 file >> junk; // Read in the '}' character
152 file.ignore(fileLength, '\n' );
153 }
154
155 file >> param;
156 } // while ( !file.eof )
Each “frame” starts with the word “frame” followed by the frame number
starting at 0, to (numFrames – 1 ).
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.
MD5Animation.cpp C++
158 // Make sure there are enough joints for the animated skeleton.
159 m_AnimatedSkeleton.m_Joints.assign(m_iNumJoints, SkeletonJoint
160
161 m_fFrameDuration = 1.0f / (float)m_iFramRate;
162 m_fAnimDuration = ( m_fFrameDuration * (float)m_iNumFrames
163 m_fAnimTime = 0.0f;
164
165 assert( m_JointInfos.size() == m_iNumJoints );
166 assert( m_Bounds.size() == m_iNumFrames );
167 assert( m_BaseFrames.size() == m_iNumJoints );
168 assert( m_Frames.size() == m_iNumFrames );
169 assert( m_Skeletons.size() == m_iNumFrames );
170
171 return true;
172 }
If the file was parsed and processed, the function will return true
MD5Animation.cpp C++
174 void MD5Animation::BuildFrameSkeleton( FrameSkeletonList&
175 {
176 FrameSkeleton skeleton;
177
178 for ( unsigned int i = 0; i < jointInfos.size(); ++i
179 {
180 unsigned int j = 0;
181
182 const JointInfo& jointInfo = jointInfos[i];
183 // Start with the base frame position and orientation.
184 SkeletonJoint animatedJoint = baseFrames[i];
185
186 animatedJoint.m_Parent = jointInfo.m_ParentID;
187
188 if ( jointInfo.m_Flags & 1 ) // Pos.x
189 {
190 animatedJoint.m_Pos.x = frameData.m_FrameData
191 }
192 if ( jointInfo.m_Flags & 2 ) // Pos.y
193 {
194 animatedJoint.m_Pos.y = frameData.m_FrameData
195 }
196 if ( jointInfo.m_Flags & 4 ) // Pos.x
197 {
198 animatedJoint.m_Pos.z = frameData.m_FrameData
199 }
200 if ( jointInfo.m_Flags & 8 ) // Orient.x
201 {
202 animatedJoint.m_Orient.x = frameData.m_FrameData
203 }
204 if ( jointInfo.m_Flags & 16 ) // Orient.y
205 {
206 animatedJoint.m_Orient.y = frameData.m_FrameData
207 }
208 if ( jointInfo.m_Flags & 32 ) // Orient.z
209 {
210 animatedJoint.m_Orient.z = frameData.m_FrameData
211 }
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.
MD5Animation.cpp C++
213 ComputeQuatW( animatedJoint.m_Orient );
214
215 if ( animatedJoint.m_Parent >= 0 ) // Has a parent joint
216 {
217 SkeletonJoint& parentJoint = skeleton.m_Joints
218 glm::vec3 rotPos = parentJoint.m_Orient * animatedJoint
219
220 animatedJoint.m_Pos = parentJoint.m_Pos + rotPos
221 animatedJoint.m_Orient = parentJoint.m_Orient
222
223 animatedJoint.m_Orient = glm::normalize( animatedJoint
224 }
225
226 skeleton.m_Joints.push_back(animatedJoint);
227 }
228
229 skeletons.push_back(skeleton);
230 }
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
MD5Animation.cpp C++
232 void MD5Animation::Update( float fDeltaTime )
233 {
234 if ( m_iNumFrames < 1 ) return;
235
236 m_fAnimTime += fDeltaTime;
237
238 while ( m_fAnimTime > m_fAnimDuration ) m_fAnimTime ‐=
239 while ( m_fAnimTime < 0.0f ) m_fAnimTime += m_fAnimDuration
240
241 // Figure out which frame we're on
242 float fFramNum = m_fAnimTime * (float)m_iFramRate;
243 int iFrame0 = (int)floorf( fFramNum );
244 int iFrame1 = (int)ceilf( fFramNum );
245 iFrame0 = iFrame0 % m_iNumFrames;
246 iFrame1 = iFrame1 % m_iNumFrames;
247
248 float fInterpolate = fmodf( m_fAnimTime, m_fFrameDuration
249
250 InterpolateSkeletons( m_AnimatedSkeleton, m_Skeletons
251 }
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.
MD5Animation.cpp C++
253 void MD5Animation::InterpolateSkeletons( FrameSkeleton& finalSkeleton
254 {
255 for ( int i = 0; i < m_iNumJoints; ++i )
256 {
257 SkeletonJoint& finalJoint = finalSkeleton.m_Joints
258 const SkeletonJoint& joint0 = skeleton0.m_Joints[
259 const SkeletonJoint& joint1 = skeleton1.m_Joints[
260
261 finalJoint.m_Parent = joint0.m_Parent;
262
263 finalJoint.m_Pos = glm::lerp( joint0.m_Pos, joint1
264 finalJoint.m_Orient = glm::mix( joint0.m_Orient,
265 }
266 }
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.
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.
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.
MD5Model.cpp C++
329 void MD5Model::Update( float fDeltaTime )
330 {
331 if ( m_bHasAnimation )
332 {
333 m_Animation.Update(fDeltaTime);
334 const MD5Animation::FrameSkeleton& skeleton = m_Animation
335
336 for ( unsigned int i = 0; i < m_Meshes.size(); ++
337 {
338 PrepareMesh( m_Meshes[i], skeleton );
339 }
340 }
341 }
MD5Model.cpp C++
260 bool MD5Model::PrepareMesh( Mesh& mesh, const MD5Animation
261 {
262 for ( unsigned int i = 0; i < mesh.m_Verts.size(); ++
263 {
264 const Vertex& vert = mesh.m_Verts[i];
265 glm::vec3& pos = mesh.m_PositionBuffer[i];
266 glm::vec3& normal = mesh.m_NormalBuffer[i];
267
268 pos = glm::vec3(0);
269 normal = glm::vec3(0);
270
271 for ( int j = 0; j < vert.m_WeightCount; ++j )
272 {
273 const Weight& weight = mesh.m_Weights[vert.m_StartWeight
274 const MD5Animation::SkeletonJoint& joint = skel
275
276 glm::vec3 rotPos = joint.m_Orient * weight.m_Pos
277 pos += ( joint.m_Pos + rotPos ) * weight.m_Bias
278
279 normal += ( joint.m_Orient * vert.m_Normal )
280 }
281 }
282 return true;
283 }
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.
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.
MD5Model.cpp C++
201 bool MD5Model::CheckAnimation( const MD5Animation& animation
202 {
203 if ( m_iNumJoints != animation.GetNumJoints() )
204 {
205 return false;
206 }
207
208 // Check to make sure the joints match up
209 for ( unsigned int i = 0; i < m_Joints.size(); ++i )
210 {
211 const Joint& meshJoint = m_Joints[i];
212 const MD5Animation::JointInfo& animJoint = animation
213
214 if ( meshJoint.m_Name != animJoint.m_Name ||
215 meshJoint.m_ParentID != animJoint.m_ParentID
216 {
217 return false;
218 }
219 }
220
221 return true;
222 }
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.
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.
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)]
This entry was posted in Graphics Programming, OpenGL, Programming and tagged
Animation, boost, C++, filesystem, game, games, gaming, glut, Graphics, loading, MD5,
model, OpenGL, parsing, Programming, rendering by Jeremiah. Bookmark the permalink.
Thanks a lot.
Perfect tutorial.
Keep up the good work =)
Reply ↓
Reply ↓
Nate on September 19, 2012 at 2:24 am said:
Reply ↓
Reply ↓
Reply ↓
Reply ↓
Reply ↓
Reply ↓
Reply ↓
Reply ↓
Fred,
Reply ↓
Fred
on December 6, 2012 at 7:17 pm said:
Reply ↓
sapphiresoul
on February 3, 2013 at 11:31 am said:
Reply ↓
Fred on December 12, 2012 at 8:29 pm said:
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));
Reply ↓
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!
Reply ↓
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?
Reply ↓
Reply ↓
Reply ↓
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
Reply ↓
Louis: That’s great to hear that you were able to port to MAC
(with CMake) without many issues. Thanks for making this
available to others!
Reply ↓
Nice tutorial, but there is not any info on how to create/export MD5
models from Maya/Max etc to even get started
Reply ↓
Jeremiah van Oosten
on May 22, 2013 at 10:30 am said:
Paul,
Reply ↓
I am VS2010
Reply ↓
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.
Reply ↓
Shing
on July 12, 2013 at 4:48 pm said:
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!
Reply ↓
Shing,
Reply ↓
Mozart
on July 12, 2013 at 4:59 pm said:
Reply ↓
Mozart,
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?
Reply ↓
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.
Reply ↓
Reply ↓
Rahul,
Reply ↓
Jeje
on March 18, 2018 at 6:56 pm said:
Reply ↓
Jeje
on March 18, 2018 at 6:57 pm said:
Reply ↓
Jeje,
Great tutorial!
Is there any binary-formatted MD5 files? ASCII format MD5 files are
too large.
Reply ↓
Jeremiah van Oosten
on August 12, 2018 at 11:22 am said:
Froser,
Reply ↓
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.
Reply ↓
Did you try to debug the application? Perhaps the model file
format does not match correctly to the parser in this demo.
Reply ↓
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
Reply ↓
Jeremiah
on January 26, 2021 at 9:43 pm said:
Marco,
Thanks for your interest. This is indeed quit an old post and it
should be updated to use modern OpenGL, unfortunately I
cannot prioritize this at this time, but I can recommend the
learnopengl.com/Model-Loading/Mesh to see how to use
VAO’s and VBO’s to load the vertex data.
Reply ↓
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.
Reply ↓
Reply ↓
Jeremiah
on January 12, 2024 at 7:32 pm said:
Reply ↓
Reply ↓
Jeremiah
on January 12, 2024 at 7:28 pm said:
Sorry about that. It’s a very old post and I think I have lost
those zip files now.
Reply ↓
Leave a Reply
Your email address will not be published. Required fields are marked *
Comment *
Name *
Email *
Website
Save my name, email, and website in this browser for the next time I comment.
Post Comment
This site uses Akismet to reduce spam. Learn how your comment
data is processed.