B |
ack at the beginning of the book you learned about the MD2 format used in id Software’s Quake II. Since then, Quake and its file format have matured. MD2’s bigger brother, MD3, was created for use in Quake III.
Quake III pushed games ahead with more and prettier graphics features, such as curved surfaces. The new MD3 format has the capability to “connect” with other models through the use of tags. The characters in Quake III make use of this system with a different model for the head, torso, and legs. This allows different parts of the body to be running different animation sequences, and can also allow for mix — and-match players if implemented correctly. Figure 9.1 shows id Software’s Quake III in action.
Figure 9.1 id Software’s Quake III (http://www. idsoftware. com),just one of the many games built around the Quake III engine. |
This is another good format to use because nearly all games based off the Quake III engine (such as American McGee’s Alice and Return to Castle Wolfenstein) use the MD3 format as well. This makes for many resources, due to the strong mod community. Members of the gaming community create many new and extra models. Because so many games use this format, many new models are also created in this format.
As with all of the model formats, before you can render or animate anything, you must have the data out of the file. MD3 contains many data structures that are similar to MD2’s, but beware, there are differences. It is best not to try to modify your MD2 code to load MD3s, but rather start from scratch.
The MD3 format is set up with a very specific structure. Starting with the header and going all the way through the mesh data, it is set up much like the MD2 files. Figure 9.2 shows the organization of the MD3 file.
Figure 9.2 The general structure of the MD3 file. This shows what an MD3 file holds, in what order it holds information, and the general layout of the file. |
The MD3 header is very similar to MD2’s header. It contains an ID, a version number, and information about various chunks in the file. Check out the definition of the header:
//—————————————————————————————
//- SMd3Header
//- File header for the md3 file, similar to md2 struct SMd3Header {
int m_iId; //Must be IDP3 (860898377)
int m_iVersion; //Must be 15
char m_cFilename[68]; //Full filename int m_iNumFrames; //Number of animation keyframes
int m_iNumTags; //Number of tags
int m_iNumMeshes; //Number of sub-meshes
int m_iMaxSkins; //Maximum number of skins for model
int m_iHeaderSize; //Size of this header
int m_iTagOffset; //File offset for tags
int m_iMeshOffset; //End of tags
int m_iFileSize; //Size of file
};
Quite a bit of stuff. Check out Table 9.1 for an explanation about how each variable within the header is used.
Then come two more integers that dictate the start and end positions of the tag structures. Again, you’ll get to tags later on in the chapter.
The very last variable is another integer that holds the total file size of the model.
The next part of the file (directly after the header) contains what are called boneframes. Each boneframe is a total of 56 bytes and holds information concerning bounding boxes for the model. There is one boneframe for each keyframe of animation, meaning that the boneframes contain the bounding information for each frame. The first twelve bytes are the minimum X, Y, and Z values of the bounding box, whereas the second twelve bytes are the maximum values.
Table 9.1 Variable m_iID m_iVersion m_cFilename m_iNumFrames m_iNumTags m_iNumMeshes m_iMaxSkins m_iHeaderSize m_iTagOffset m_iMeshOffset int m_iFilesi |
Purpose
This variable identifies the file as an MD3 model. The value of this variable is "IDP3", which translates to 860898377.
Should always be equal to 15. If either the first or second variables are not what they are supposed to be, the model is not valid.
This array of characters holds the filename of the model relative to the BASEQ3 directory of Quake III. This is generally of little use if you are using the model for your own applications.
This variable holds the number of keyframes that are used in the model for purposes of animation.
Here you will find the number of tags, or attachment points, stored in the MD3 model. Tags allow you to attach one MD3 model to another.
Each MD3 contains at least one mesh, or geometry data, chunk. However, this variable will let you know if the model contains just the required one, or two or more submeshes.
This variable tells you the maximum number of skins this MD3 model can use.
The total length of this file header can be found by reading this variable. This value will also tell you the offset, in bytes, from the start of the file to the boneframes.
To read the tags, you must know where in the file they are located. This variable will give you a value, in bytes, that will allow you to seek out the tag structures buried in the model file.
Like the tag offset tells you where to find the tags, the mesh offset tells you where to find the mesh data within the MD3.
The last variable in the header. It contains the total size, in bytes, of the MD3 file. This is useful for verifying that all of the data is here and to help determine whether the file has been tampered with.
~ГГ^Э-
Each of the X, Y, and Z values is a four-byte floating-point number.
Using these six values you can construct a full bounding box for the model. Next comes the X, Y, Z coordinates of the origin. This set of three floats gives you the center of the bounding box. Although it is typically 0, 0, 0, it can vary. The minimum and maximum values of the box are relative to this point.
Next comes a floating-point value, which holds the radius of the bounding sphere for the model. Like the bounding box, the center of this sphere is determined by the origin specified. A bounding sphere provides a faster way to quickly check whether a model might be colliding with an object such as the world or another model.
Last there are 16 bytes that make up the boneframe’s name. This name has no real purpose in the game, but can be helpful for the artists who are creating the model.
Next in line are the tag structures, used to connect models together. The tag structures in MD3 are used when attaching two models together, such as when attaching a character’s legs to its torso or a weapon to a character. Each tag consists of a 3×3 rotation matrix, a position vector, and a 64- character name. The name is simply a string that identifies the tag. Common names for tags include Weapon01, Torso, and Head; each designates what kind of model is meant to be attached to the specific tag. The rotation matrix stores the rotation value for the specific frame. This matrix will tell the attached models how much to rotate. For instance, the attached weapon must rotate if the wrist it is attached to is rotated. Although the rotations are stored in the file as a 3×3 matrix, it is helpful to convert the matrix to a quaternion when you read it in.
Converting the matrix to a rotation quaternion will allow you to take advantage of the quaternion interpolations when animating the model. The last value, the position vector, gives the current position of the tag. It is used to keep the models attached together. The position vector changes when one part of the model changes positions, requiring the other parts to keep up. This happens, for example, when the character crouches, at which point the torso must also move downward to stay on top of the legs.
As mentioned previously, a much better way to efficiently store these tags, as well as cut down on processing time, is to store the rotation
matrix as a quaternion. Using the FromMatrix function of the quaternion class, the rotation matrix can be converted into a quaternion after being read from the file. After converting the data from the file into a matrix, you can call the FromMatrix function included with the CQuaternion class. By passing your rotation matrix to this function, it will fill in the quaternion data for you.
Here is what you finally end up with for a data structure:
//———————————————————————————-
//- SMd3Tag
//- Tag structure for Md3 struct SMd3Tag {
char m_cName[64]; //Name of tag
CVector3 m_vecPos; //Position of the tag
//Even though the file contains a 3×3 matrix
//It is converted into a quaternion before being stored in memory // to conserve space and take advantage of quaternion interpolation CQuaternion m_qRot; //Rotation for the frame
};
The number of tags in each set is determined by the m_iNumTags variable in the file’s header. Because the tags are used for animation, there is one set of tags for every frame of animation. If there are 100 frames of animation, and there is one tag to attach the weapon to, one to attach the head, and one to attach the legs, there will be a total of 300 tags. (Three for each of the 100 keyframes.) To get the total number of tags, you only need to multiply the m_iNumTags and the m_iNumFrames variables that are stored in the header of the MD3 file.
Meshes contain the real meat of the file (or any model file format for that matter). The meshes contain everything seen on the screen. The MD3 meshes contain all the vertex, animation, face, and texture information, a lot like MD2 does. The difference here between MD2 and MD3 is that MD3 can have multiple meshes. The number of meshes in each file is given by the m_iNumMeshes variable in the file header. Multiple meshes can be useful if you want to apply different types of animations to different parts of the model. The arms may be a separate mesh from the torso because it might be easier for the artist
to animate each part separately. Another instance in which multiple meshes can be used is when parts of the model are not connected to other parts of the model and must move independently.
Each mesh is independent of the rest and is set up in a specific order, as shown in Figure 9.3. Because each mesh is independent, you can treat it as its own 3D model. Each mesh contains its own animation data, and its own vertices, texture coordinates, and triangles. A mesh will never need to access data from other meshes, even within the same file.
Figure 9.3 The MD3 mesh layout. Each mesh chunk consists of faces and texture coordinates as well as keyframes. Each keyframe contains a set of vertices for that position. |
Take a look at the following mesh structure:
//—————————————————————————————
//- SMd3Mesh
//- A single mesh in the md3 file struct SMd3Mesh {
SMd3MeshHeader m_Header;
SMd3KeyFrame * m_pKeyFrames;
SMd3Face * m_pFaces;
SMd3TexCoord * m_pTexCoords;
SMd3Skin * m_pSkins;
};
Quite a bit of stuff here! A header, animation data, faces, texture coordinates, and more. There is a pointer for each of the sections, other than the header, which means that you must allocate memory for each section. Each section, including the header, keyframes, faces, texture coordinates, and skins, are explained in their own sections that follow.
For the sake of simplicity, I chose not to use std::vectors here. Because of this, if you look at the code, you will see a constructor and destructor so the structure will clean up after itself when the program exits.
The mesh header tells you everything you need to do to find and load all of the data. The header structure is 108 bytes long and contains the number of each component, and offset into the mesh data for each component as well. Here is the structure code; Table 9.2 contains the mesh header values:
//—————————————————————————-
//- SMd3MeshHeader
//- Header for each mesh in the md3 file struct SMd3MeshHeader {
char m_cMeshId[4]; //Mesh ID
char m_cName[68]; //Mesh name
int m_iNumMeshFrames; //Number of animation frames in mesh
int m_iNumSkins; //Number of skins for this mesh
int m_iNumVerts; //Number of vertices
int m_iNumTriangles; //Number of triangles
nt m_iTriOffset; nt m_iHeaderSize; nt m_iUVOffset; nt m_iVertexOffset; int m iMeshSize; |
//Offset for the triangles //Size of this header //Texture coordinate offset //Offset for the vertices //Total size of the mesh };
Most of this is straightforward, but there are a few things you need to be careful of. The first is to be sure you are reading from the correct place in the file. The offsets in this header are offsets in bytes from the beginning of the mesh chunk itself, not the start of the file or the last piece of data read. The second is to be sure that you store the data in the
Just to reiterate: the offsets in this header are offsets in bytes from the beginning of the mesh chunk itself, not the start of the file or the last piece of data read. Be sure to remember this so that you read from the correct place in the file.
Variable m_cMeshId[4] m_cName[68] m_iNumMeshFrames m_iNumSkins m_iNumVerts m_iNumTriangles m_iTriOffset m_iHeaderSize m_iUVOffset m_iVertexOffset |
Purpose
A four-byte ID for the mesh; can be ignored.
A 68-character mesh name; can also be ignored.
This four-byte integer stores the number of keyframes contained within the mesh.
Another four byte integer. This time, it stores the number of skins that are used to texture this particular mesh.
Yet another four-byte integer. This variable gives you the number of vertices that each keyframe will contain.
A four-byte integer again. It stores the number of triangles that make up the mesh.
Tells you how far into the mesh the triangle information can be found.
Holds the total size of this header.
Tells the program where to find the U/V texture coordinates within the mesh.
Tells the offset into the mesh where the first of the vertices and keyframes can be found. Because this value will take you to the start of the keyframes, you can also think of it as the keyframe offset.
m iMeshSize |
Holds the total size of the mesh in bytes, header and all.
correct format and in the correct place. Nothing is worse than corrupting or losing data.
Before you can go on, you need to know the format of all the data in the mesh structure, or it will not do you any good. Let’s start with the skins.
This is the simplest structure within the MD3 file. It contains only a 68- character string that holds the path and name of the texture file. Because the texture filename is relative to the baseq3 path, which you probably won’t have, I would advise you to hack off the pathname, and keep only the filename. It will make your life a lot easier. Another problem you may encounter is filenames without extensions. With Quake III, this means the model uses a shader for the particular mesh instead of a regular texture. A Quake III shader is a text file, generally with a. shader extension on it. These files contain texture information such as animations and special effects.
If you come across one of these cases, you will need to do one of two things. You will need to create a texture to take the place of the shader or you will need to write a routine to read and parse the shader information to determine, at the very least, the texture filenames. The shader file can contain many other pieces of data, including multitexturing information, deformations, and texture effects. Although they do not have to be loaded, they can add a lot of detail and realism to a model.
These two sections, keyframes and vertices, go hand in hand in an MD3 file. An MD3 keyframe is nothing but a collection of vertices that define where the model is and what it looks like at a particular time.
A model may have a set of keyframes that, when cycled through, make the model appear to be walking or jumping, or doing some other action. Each keyframe stores its own set of vertex positions rather than just storing ones that are moved or updated from frame to frame. This approach is a lot like taking snapshots of the model’s vertices at certain intervals and using these snapshots to animate the model later.
};
//—————————————————————————————
//- SMd3KeyFrame
//- A single animation keyframe, contains new vertex positions struct SMd3KeyFrame {
SMd3Vertex * m_pVertices;
};
This is where you need to be careful. Even though you want the vertices as floats, the MD3 files do not store them this way.
Each vertex takes up eight bytes, instead of twelve, to save space. The X,
Y, and Z components are two bytes each. To move from this format to your three-float format, you divide each component of the vertex by 64.0f—FinalX = OriginalX / 64.0f—and store it in the appropriate place.
The number of keyframes in a mesh is stored in the mesh’s header, as the m_iNumMesh frames variable, and the number of vertices in each frame is contained in the m_iNumVerts variable. Don’t forget to use the offset given to you in the header before you start reading in data.
As with the vertices and keyframes, be sure to jump to the triangle offset as given in the header before you start getting data, lest you have disastrous results. As with MD2, all faces in the MD3 model are triangles. Each face structure contains three unsigned shorts that hold indexes into the arrays of vertices.
There is only one set of faces per mesh; the same indexes are used regardless of which keyframe of animation is being rendered.
Models are pretty boring without textures on them. To use textures, you need texture coordinates. The texture coordinates for each mesh are stored at the offset given by the mesh header. Each texture coordinate is simply two floating-point values, one for each u and v. These coordinates will allow you to have more realistic models, without using too many polygons. The texture can add details that the polygons cannot.
Once you have all this data, you can render and animate the model.
I am not going to go over this in great detail because everything in the MD3 file can be rendered in the same way as an MD2 file. As you loop through each mesh, the faces can be drawn in a traditional manner. Using the vertex indexes stored in the face structures, as well as the texture coordinates, it is very easy to get the model onto the screen. Check out the CMd3::Render function for the exact code. Using the CMd3::Render function is very simple; here’s an example of it:
//Rendering an MD3 model
//The Render function only renders one frame of the model g_Md3Model. Render(frameNum);
Animating the model is also similar to MD2. Using a timer, you calculate an interpolation value and use it to linearly interpolate between the two nearest keyframes. This must be done for each mesh. The simplest way to render the new, interpolated vertices is just to send each to the renderer as soon as it is generated. This method avoids storing the interpolated vertices, even temporarily. The vertices can be sent one by one using a function such as glVertex3f in OpenGL.
If you want to use vertex arrays or other optimization techniques, you might need to store the vertices in a temporary buffer before sending them off to the rendering API. For a little more information and applicable code, check out the CMd3::Animate function in the first demo. The first demo found in the /Code/Chapter9 directory shows the basic rendering and animation discussed here. Figure 9.4 shows a single MD3 model being animated by itself.
The biggest advantage of the MD3 format is its ability to use multi-part models. A multi-part model combines two or more models into one. This is beneficial when you need to mix and match models such as using two torsos and two sets of legs to create four unique models, rather than just two. It also allows you to link weapons or other attachments to a character or object without having to create custom animations for either the original model or the one being attached. Using those mysterious tag structures you loaded in earlier, you can attach one model to another at predefined points.
You can have one model for the legs of the character and another for the torso. When connected together using the tags, they would look and act like a single model. However, each part of the model can be animated individually. The legs can be playing a running animation while the torso is playing a shooting animation, thus allowing the character to run and shoot at the same time. This feature alone has many advantages. In the case of a first-person shooter using a traditional model format such as MD2, the artist would need to make a
jump-and-shoot animation, a run-and-shoot animation, a walk-and — shoot animation, and so on.
With MD3 however, a single shooting animation with the torso can be played along with a running, walking, or jumping animation on the attached legs. This also allows artists to make three leg models and four torso models that can be mixed and matched by the programmers to create a variety of characters.
The second demo (found in /Code/Chapter9/) contains all the code for using multi-part models. Keep in mind that using this code does not prevent you from using single-part models. The code from this demo works just as well with both.
As I mentioned earlier, you finally get to use those tags you loaded from the file. These tags will help you assemble the model together by providing information about how to transform each of the models so that they all stay properly attached. Without tags, attaching legs to a torso can be very difficult because when the torso moves upward, the legs may not, creating a significant visual artifact. A good way to think about the tags is as joints in a skeletal animation system. These tags contain parent nodes and child nodes, just as with regular skeletal animation.
The first thing you need to do is modify the CMd3::Animate function.
Just like the mesh keyframes, the tag positions and rotations must be interpolated as well. Because there is one tag for every frame, the last and next frame indexes, the interpolation values calculated for interpolating between the mesh keyframes can be reused here. This time, rather than sending each vertex to the rendering API as it is calculated, it may help to store them temporarily. This allows you to better control the vertices and allows you to implement features such as vertex arrays in the optimization process.
The CMd3::Animate function only needs to be modified slightly. Because you are using the tags now, they must be interpolated along with the rest of the model. This can be accomplished in the same way as regular skeletal animation—using linear interpolation for the positions and a quaternion SLERP function for the rotations.
~ГГ^Э-
Each tag “frame” consists of two important parts:
■ The position is stored in a three-element vector. For the purpose of interpolation, the position can be treated as a single vertex of the mesh.
■ The rotation is stored in the form of a unit quaternion. The rotation quaternion should be SLERPed in order to give the rotation interpolation a smooth curve. This is done using the SLERP function provided in the math code used throughout the book. Just plug in the quaternion from the previous frame, the quaternion from the next frame, and the interpolation value, and the SLERP function will spit out a new quaternion.
Attaching One Model to Another
Next, you need some way to attach one model to another. This is done using the CMd3::Attach function. The Attach function specifies a pointer to another CMD3 model, and an integer denoting which tag it should be attached to. The Attach function takes this pointer and stores it in an array of pointers known as the child-model array. The child-model array contains one pointer for each tag on the model. All of the pointers in the array start out as NULL, but when a model is attached to that particular joint, the pointer is changed to point to the attached model instead. An example of attaching one model to another using the CMd3::Attach() model is shown here:
//Attach a model called g_Weapon to g_Character at the //first tag g_Character. Attach(g_Weapon, 0);
Models are always attached to their parents, not the other way around. Generally, the root joints of a humanoid character are the legs. The torso model is attached to the legs, the head and weapons models are attached to the torso, and so forth.
Now that you have one model attached to another, it is time to draw them. The first thing to do is to remove any drawing code from the Animate function. Now you can call the animate functions in each frame, but it will simply calculate the interpolated vertices and tags and store them.
The RenderT() function is where all the action takes place. When the RenderT() function of a parent joint is called, the parent joint’s meshes are rendered as normal. Then, for each tag that contains a pointer to a child mesh, a 4×4 transformation matrix is built using the interpolated quaternion and position values of the tag. The rendering API can then be set up to render the mesh in the correct position and with the correct orientation.
This can be accomplished by pushing a new matrix onto the stack, using a call such as glPushMatrix in OpenGL. Using the new copy of the view matrix you just created, multiply it by the transformation matrix you created from the tag data. This will give you the proper position and orientation to render the first child mesh. A child mesh could be used for a weapon attached to the character’s hand, or even perhaps to allow the character’s head to move in sync with its torso.
Because the RenderT() function is recursive, if the child mesh you render has a child of its own, the same thing will happen again. This process will continue down the line until a mesh is reached that has no children. The function then backs up, popping the matrices off the stack as it goes, back to the original parent node. Then, it continues the loop if there are more lines of child models to render. In this way, every child down the line gets the combined transformation of every parent before it.
Figure 9.5 shows a picture of a multi-part character model being rendered. The legs, torso, and head are all separate MD3 models with separate animations. If a weapon were present, it would also be its own MD3 model with separate animations.
After finishing this chapter, you should be able to create an MD3 loader for your engine or game. With loading and animation capabilities, you are ready to tell your artists to start churning out MD3s for your new game. You can even use the attachment points to build full characters and to attach weapons or other models to your characters.
The next chapter takes you through some tips and tricks that can be used with your 3D models, regardless of the format. Sections on calculating face and vertex normals, interpolation, and optimization
can be found in the upcoming chapter. You will learn how to use each of these tips and tricks in a non format-specific way so that you can apply them to any model format, including your own.
T |
his chapter aims to give you a few good tips and tricks that you can apply to any 3D model that you come across. Using the methods here, you will be able to apply lighting to your models, move them around, and even optimize your rendering code a little bit. You first learn how to calculate face and vertex normals, which will enable you to apply lighting to your model. Face normals give you a flat-shaded model, whereas vertex normals give you a smooth-shaded model.