3Д БУМ

3Д принтеры и всё что с ними связано

MlLkShflFE 3D

MlLkShflFE 3D

A

s the book moves forward into this chapter, more advanced formats start appearing. This is the first of several more compli­cated formats. Gone are the days of simple “load-and-cycle” formats such as the famous MD2 format discussed earlier in the book.

This chapter contains everything you need to know about the MS3D format outputted by a very nice, very inexpensive modeling package known as MilkShape 3D. MilkShape 3D was created by chUmbaLum sOft, a small software company consisting of the founder, Mete Ciragan. Mete created MilkShape to allow individuals to create new models for VALVe Software’s Half-Life (https://www. valvesoftware. com) without the use of expensive professional modeling tools.

Since its creation, MilkShape has become one of the most popular modeling tools among independent developers of “mods”—smaller games built on top of commercial ones—and games alike. It now supports importing and exporting formats for many popular games and engines. I recommend that any game developer on a short budget take a look at this wonderful program. You can check out a 30-day trial on the CD. You can find the setup file in the Programs directory.

For your viewing pleasure, Figure 6.1 shows the MilkShape 3D editor with the model you’ll be using as an example in the upcoming pages.

Getting the Data

As always, you start by getting the data out of the file. Unlike most model formats, MS3D does not contain information about the number of vertices, triangles, or anything else at the start of the file. All that is present is a simple header to verify that the file is indeed a valid MilkShape file.

The header is exactly 14 bytes long and contains a 10-character identi­fication string, and a version number. The first 10 bytes contain the string “MS3D000000”. This string identifies the file as a MilkShape file.

Keep in mind that the zeroes in the ID string are characters, not values. Make sure you keep that in mind loading the file. The second four bytes contain a single integer. This is the version number of the file and should contain the value 3 or 4. The format detailed here is for these two versions only. As MilkShape progresses and new versions of the format are released, I will post additions and changes on the book’s Web site.

MlLkShflFE 3D

MlLkShflFE 3D

Figure 6.1 The MilkShape 3D editor complete with an example model.

Vertices

Directly after the header come the vertices. As with other parts of the file, the vertex chunk is prefixed by a two-byte unsigned integer that contains the number of vertices present in the model.

The vertices are a little bit different than what you have been using previously. Instead of simply an X, Y, Z coordinate plane, MilkShape’s vertices also contain a one byte, signed integer that holds the number of that vertex’s “bone”. A value of -1 means that the vertex has no bone attached to it and is not affected during animation.

//————————————————————————-

//- SMs3dVertex //- A single vertex struct SMs3dVertex {

unsigned char m_ucFlags; //Editor flags, unused for the loader CVector3 m_vVert; //X, Y,Z coordinates

char m_cBone; //Bone ID (-1 = no bone)

unsigned char m_ucUnused;

MlLkShflFE 3D

~ГГ^3-

Here is the vertex structure that is used for the MS3D loader:

};

The middle two variables in the structure were explained previously, which leaves only two one-byte variables left unknown. The first con­tains various flags for the editor to use for the vertex. This variable is an unsigned char that holds the status of the vertex within the editor.

If the value is 0, the vertex is visible, but unselected. If the value is 1, the vertex is selected in the editor, and if the value is 2, the vertex is hidden from view in the editor window. A value of 3 means the vertex is both hidden and selected. Although this variable is not necessary for loading the models into your engine, it may be helpful if you are writing an importer for another modeling program, such as 3D Studio Max (www. discreet. com) or Maya (www. aliaswavefront. com). The second variable contains nothing; you can just skip over it.

After the vertices are read, the file moves immediately on to the face information.

Faces

The face for this particular model format contains a lot of informa­tion. But, before you worry about loading the triangles, you need to find out how many of them there are. As with the vertex chunk, the face or triangle chunk starts with a two-byte integer. This integer comes immediately after the last vertex is read, and just before the triangle data is stored. This two-byte integer contains the number of SMs3dTriangle structures to read from the file.

Right after the two-bytes worth of data that determine the number of faces to read come the faces themselves. The face or triangle structure contains editor flags, vertex indexes, texture coordinates, grouping

MlLkShflFE 3D

info, and even the vertex normal information. Lets take a look at the SMs3dTriangle structure to see what solidifying the model entails:

//———————————————————————-

//- SMs3dTriangle //- Triangle data structure struct SMs3dTriangle {

unsigned short m_usFlags; //Editor flags

unsigned short m_usVertIndices[3]; //Vertex indexes

CVector3 m_vNormals[3]; //Vertex normals;

float m_fTexCoords[2][3]; //Texture coordinates

unsigned char m_ucSmoothing; //Smoothing group

unsigned char m_ucGroup; //Group index

};

Well, that’s not too bad. A total of six variables are used for every triangle in the MilkShape 3D model. The first, like the vertex struc­ture, is just an editor flag. Like vertices, a 0 means a regular, unselected face, a 1 means the face is selected, and a 2 means the face is hidden from view. Again, a face can be both selected and hidden if the value is 3. Notice that this flag variable here is two bytes, rather than just 1 like it is in the vertex structure.

Next come three more unsigned two-byte integers. These three inte­gers are indexes into the array of vertices covered in the last section. The three vertices form a single triangle in the model. Using only the data covered so far, it is possible to create a solid model. However, it would be kind of boring with no textures or lighting.

Moving on down the line, you come to the vertex normals. A vertex normal is used for lighting. Each vertex normal is an average of all the normals of the faces its vertex shares. A face normal is perpendicular to the plane the face lies in; the vertex normal is the average of all the perpendicular vectors. A normal, whether it is a vertex or face normal, must be a unit vector with a magnitude of 1. These are stored in a CVector3 class. The CVector3 is made for vectors that consist of three floating-point variables that take up a total of 12 bytes. The main advan­tage of using a Cvector3 for each normal rather than a simple array of floats is that the CVector3 class contains a myriad of functions. These functions make it a lot easier for you later on when you start animating the model. There are three normals—one for each vertex index.

MlLkShflFE 3D

~ГГ^3-

Up next are the texture coordinates. The u and v coordinates are stored kind of strangely in MilkShape. There are a total of six floats— one pair of coordinates for each of the three vertices that make up the face. However, instead of being stored u1, v1, u2, v2, u3, v3 like you might expect, MilkShape stores all the us first, followed by all the v s.

This makes the order u1, u2, u3, v1, v2, and v3. If you do not remember this ordering, it will come back to bite you. I spent several hours debugging a program only to find I used the wrong texture coordi­nates in the wrong places.

The last two variables deal with the group the face belongs to. These variables are not too important as you will see in the section coming up. The groups or meshes in the model take care of knowing which faces belong to each group.

Meshes

For maximum flexibility, MilkShape 3D’s triangles are grouped into meshes or groups. This allows different sections of the model to use different textures, materials, and even render only certain sections of the model. The mesh section of the file follows the triangle or face section, and like the other section is preceded by a two-byte integer telling how many meshes there are. Immediately following are the groups. There are 35 bytes of general information, followed by a num­ber of two-byte triangle indexes. The number of indexes is not constant; some groups may have more than others. To compensate for this dis­crepancy, you need to be able to dynamically allocate memory in each group to hold these indexes. Here is what the structure looks like.

//————————————————————————————————

//- SMs3dMesh

//- Group of triangles in the ms3d file struct SMs3dMesh {

unsigned char m_ucFlags; //Editor flags again

char m_cName[32]; //Name of the mesh

unsigned short m_usNumTris;//Number of triangles in the group

unsigned short * m_uspIndices; //Triangle indexes

char m_cMaterial; //Material index, -1 = no material

//Let it clean up after itself like usual

MlLkShflFE 3D

SMs3dMesh()

{

m_uspIndices = 0;

}

~SMs3dMesh()

{

if(m_uspIndices)

{

delete [] m_uspIndices; m_uspIndices = 0;

}

}

};

This structure takes a bit of care when being read in. The m_uspIndices variable is a pointer, meaning you just can’t read in NumberOfMeshes * sizeof(SMs3dMesh). For each mesh, you must read the first 35 bytes that consist of some editor flags—the same flags that the vertices and triangles use—0 for unselected, 1 for selected, and 2 for hidden. You must also read a 32-character mesh name and a two-byte integer that contains the number of triangles in the mesh. Using this last variable, you must allocate the memory for the triangles indexes.

The m_uspVariable is now ready to hold all of the two-byte integers necessary to show which triangles are used in the mesh. Each element of the array is an index into the array of triangles that was created and filled earlier in the load sequence.

Right after you read all the triangle indexes, there is a lone, single-byte variable that holds the index into the materials array (which you will be getting to in a second). The variable is signed, and a value of -1 means the mesh contains no material.

Last of all, as you can see, the structure takes care of deleting its own memory, meaning you do not have to worry about remembering to clear it when you are done using it. Deleting the array of meshes will automatically delete everything in them.

Materials

To really make the model stand out and to add lots of customization, you can use materials. Materials control the way the renderer handles

MlLkShflFE 3D

~ГГ^3-

the texturing and lighting of the model. From textures, to color, to transparency, materials do it all.

The materials structure is fairly large and contains a lot of data, so bear with me here.

Remember to read in your two-byte variable that tells you how many materials there are before you jump into reading the material data.

Here is the material structure:

//—————————————————————————-

//- SMs3dMaterial

//- Material information for the mesh struct SMs3dMaterial {

char m_cName[32]; //Material name

float m_fAmbient[4]; //Ambient values

float m_fDiffuse[4]; //Diffuse values

float m_fSpecular[4]; //Specular values

float m_fEmissive[4]; //Emissive values

float m_fShininess; //0 — 128

float m_fTransparency; //0 — 1

char m_cMode; //unused

char m_cTexture[128]; //Texture map file

char m_cAlpha[128]; //Alpha map file

CImage m_Texture;

};

You need to be careful when you start reading the data from the file. Instead of reading the whole group of materials in at once, you must loop and read them in one at a time. The reason for this is the very last variable in the structure, m_Texture. m_Texture is an image class that is used to store the texture for the material. This eliminates trying to sort out the textures later when you need to use them during the program. Note that this part of the structure is not actually contained within the file, making the actual size of the structure in the file 361 bytes. The CImage variable is a custom class that resides in the general basecode in the files (image. cpp and image. h). You can easily remove this variable and replace it with your own image-loading system.

The first variable in the structure is a 32-character array that holds the name of the material. This isn’t all that important if you are just

making a loader, but it’s nice to have when working with the model in MilkShape itself.

The next six variables contain the materials properties. These proper­ties determine the way the lighting of the scene will affect the model.

■ The m_fAmbient and m_fDiffuse each store four floating-point values that represent the red, green, blue, and alpha (RGBA) values of a color. These colors help define the color of the material and determine how the polygons using this material will react to lighting in the scene.

■ The next variable, m_fSpecular, also contains an RGBA color.

The specular material properties dictate the color of the specu­lar highlights or “shiny places” on the mesh using the material.

■ The last array of floats is the m_fEmissive set. This variable also contains four floats. The emissive property specifies how intense the material will emit light. The higher the values, the “brighter” the material will be.

■ The final material property is the shininess of the material. The m_fShininess variable contains a single float. This variable deter­mines just how shiny the specular highlights are. The lower the value, the darker and duller the highlights will be—just the opposite as the value increases. Darker and duller highlights are used for materials such as wood and asphalt, whereas brighter, shinier highlights are best for shiny metal and artificial materials.

After all these material properties comes one more—the transparency of the material, which is stored in m_fTransparency. Although the other material variables deal with color, transparency sets how opaque or “see-through” the mesh is. A value of 1 is completely opaque and a value of 0 is complitely transparent, or invisible. Because of the way OpenGL handles materials, the easiest way to handle this is to take this value and plug it into the last element of the diffuse property. This method creates a reasonably nice transparent mesh.

After these are all taken care of, you must skip a byte to compensate for a small, unused variable in the material information. This single byte variable m_ucMode is unused by the format for now. It is in there for use in later versions.

Then you get to the textures. The texture name is stored in a 128-byte string. After you acquire it, you can send it directly to the Load function

~ГГ^~

of the CImage class included in the structure. This will retrieve the texture and take care of loading it so it is ready to use when you get to the rendering stage. The filename stored in the file can be passed directly to the CImage::Load() function or to your own texture-loading functions.

The last variable holds the filename of the alpha map. Due to the fact that I could find no information on this at the time this was written, the use of alpha maps on models is not currently supported. However, keep checking the Web site for the book and as soon as information is available I will update the code and the text and post it there.

Whew, that was a lot of information. However, you now have enough to render the model in what I call its “initial position”. This is the posi­tion before any animation is applied. In general, this position is opti­mized for ease of editing and probably does not appear in the actual animation sequence.

Figure 6.2 shows what the model would look like rendered.

MlLkShflFE 3D

Figure 6.2 The model rendered in its initial position with textures, materials, and lighting enabled.

Pretty cool huh? Rendering the model isn’t too hard. It basically involves drawing the meshes one by one, making sure to set the appropriate material and lighting properties beforehand. As usual, in order to make the code easier to read and convert to other languages and APIs, I use immediate mode calls that are pretty obvious in what they do.

The first thing that must be done is the material information. In OpenGL this can be done using the glMaterialf and glMaterialfv calls.

for(int x = 0; x < m_usNumMeshes; x++)

{

//Set up materials if(m_pMeshes[x].m_cMaterial >= 0)

{

SMs3dMaterial * pCurMat = &m_pMaterials[m_pMeshes[x].m_cMaterial]; //Set the alpha for transparency pCurMat->m_fDiffuse[3] = pCurMat->m_fTransparency;

glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, pCurMat->m_fAmbient); glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, pCurMat->m_fDiffuse); glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, pCurMat->m_fSpecular); glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, pCurMat->m_fEmissive); glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, pCurMat->m_fShininess); glEnable(GL_BLEND);

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

//Texture map

pCurMat->m_Texture. Bind();

}

else

glDisable(GL_BLEND);

This little bit of code should be easy to follow. Because each model is broken into meshes, you must loop through them and draw them one at a time. The first thing to do is check to see whether the mesh does indeed have a material attached to it. If the material index of the mesh is not -1, a pointer to the appropriate material is obtained. Now you are almost ready to send the material information to OpenGL; how­ever, you first must take care of transparency. This is done by taking the transparency variable and using it to replace the alpha value of the diffuse property. Once that simple operation is completed, you can set

~ГГ^3-

the material properties. Every property except Shininess uses glMaterialfv. This is because all other values are arrays of values, whereas shininess is simply a single float.

The next section simply turns on blending and sets the appropriate blending mode. This assures that transparency will work correctly and will minimize any funny visual artifacts.

Last, using the CImage class that you used to load the skin earlier, you bind the texture to the mesh.

If there is no material for the group, you must make sure to turn off blending. Failure to do so can cause very strange visual artifacts and unwanted graphical glitches. You can also use glMaterial to set the materials back to default. The default values for ambient, diffuse, specular, emissive materials are (0.2, 0.2, 0.2, 1.0), (0.8,0.8,0.8,1.0), (0.0,0.0,0.0,1.0), and (0.0,0.0,0.0,1.0), respectively. The shininess material is also set to 0.

The code that sends the vertices to the rendering system follows:

//Draw mesh

glBegin(GL_TRIANGLES);

for(int y = 0; y < m_pMeshes[x].m_usNumTris; y++)

{

//Get a pointer to the current triangle

SMs3dTriangle * pCurTri = &m_pTriangles[m_pMeshes[x].m_uspIndices[y]];

//Send the normal

glNormal3fv(pCurTri->m_vNormals[0].Get());

//Send texture coords

glTexCoord2f(pCurTri->m_fTexCoords[0][0], pCurTri->m_fTexCoords[1][0]);

//Send vertex position

glVertex3fv(m_pVertices[pCurTri->m_usVertIndices[0]].m_vVert. Get()); glNormal3fv(pCurTri->m_vNormals[1].Get());

glTexCoord2f(pCurTri->m_fTexCoords[0][1], pCurTri->m_fTexCoords[1][1]); glVertex3fv(m_pVertices[pCurTri->m_usVertIndices[1]].m_vVert. Get());

glNormal3fv(pCurTri->m_vNormals[2].Get());

glTexCoord2f(pCurTri->m_fTexCoords[0][2], pCurTri->m_fTexCoords[1][2]); glVertex3fv(m_pVertices[pCurTri->m_usVertIndices[2]].m_vVert. Get());

}

}

Just as you loop through each mesh, you must loop through each triangle within the mesh itself. After acquiring a pointer to the appro­priate triangle structure in the face array, you can send the normal vector, texture coordinates, and vertex position to OpenGL. This must be done three times for each triangle and once for each vertex.

Animation

This is the part you have all been waiting for. The first section that uses what you learned in Chapter 5, “Introduction to Skeletal Animation” (you did read that didn’t you?)

First thing you have to do is finish loading the model. The remaining part of the model contains the joint (bone) data, including their names, initial positions, and keyframes.

Like the other section of the ms3d file, the joints structure is preceded by a two-byte integer that tells how many joints the file contains. Then comes the data. The joints and accompanying structures are fairly complicated, so bear with me.

First, the simple structure. This structure is used for storing the rota­tion and translation of the keyframes:

//—————————————————————————————

//- SMs3dKeyFrame

//- Rotation/Translation information for joints struct SMs3dKeyFrame {

float m_fTime; float m_fParam[3];

};

This is a simple enough structure. The keyframe structure stores a single “stop point” or landmark of the model. The first variable, fTime, stores the time in seconds that this keyframe would be used to set the position of the particular joint. The second variable is an array of three floats. They store the rotations around the X, Y, and Z axes, or they

contain the X, Y, and Z translation values. Each joint contains a set of these keyframes for both rotation and translation values.

Now, on to the joint structure. This is a fairly large structure that con­tains a lot of data. Some of it is to be loaded from the file; some of it is created from these values instead of being loaded directly from the file.

Here again, the structure in the code varies from the structure in the file. The file contains the editor flags, the joint name, the parent joint’s name, the initial position and rotation, the number of keyframes for rotation and translation, and the actual translation and rotation keyframes. As was the case back in the group structures when you needed to use a dynamically allocated array to hold the indexes, you need to allocate memory for the keyframes before reading them in.

Brace yourself. Here is the code for SMs3dJoint, the joint structure.

//———————————————————————————

//- SMs3dJoint

//- Bone Joints for animation struct SMs3dJoint {

//Editor flags //Bone name

//Parent name //Starting rotation //Starting position //Number of rotation frames

//Number of translation frames

//Data from file unsigned char m_ucpFlags; char m_cName[32];

char m_cParent[32]; float m_fRotation[3]; float m_fPosition[3]; unsigned short m_usNumRotFrames;

unsigned short m_usNumTransFrames;

SMs3dKeyFrame * m_RotKeyFrames; SMs3dKeyFrame * m_TransKeyFrames;

//Rotation keyframes //Translation keyframes

//Parent joint index

//Data not loaded from file short m_sParent;

CMatrix4X4 m_matLocal; CMatrix4X4 m_matAbs; CMatrix4X4 m_matFinal;

unsigned short m_usCurRotFrame;

Let’s walk through this step by step. Like all the data structures from MS3D, this one has one byte’s worth of editor flags that can be ig­nored. Following that there are two 32-character strings. These strings hold the joint’s name and the joint’s parent’s name. Later, you’ll match each joint to its parent so you don’t have to compare all the strings every time the joint positions need to be recomputed.

Next come the initial rotations and translations of the joints. These six values—three for rotation and three for translation—give the starting positions of the joints.

Then come two two-byte integers, one for the number of rotation keyframes and one for the number of translation keyframes. These are used to allocate memory for the next two variables that hold the actual rotation and translation data, respectively.

After allocating the memory, you can read the data from the file into the newly created arrays. This is all the data that needs to come from the file. You can close the file and get rid of any temporary buffers you may have created or allocated.

Finding the Lost Parents

Each joint stores the name of its parent. Although this name can be used, it is a pain to run lots of string compare operations and search through every joint to find the right one. It is better that this compari­son be performed only once, during the loading of the model. That’s where the m_sParent variable comes in. This variable is an index into the array ofjoints. At this index is the currentjoint’s parent. If the joint is a root joint, meaning it has no parent, this is set to -1 to avoid confusion.

To calculate this variable for the current joint, you start at the begin­ning of the array and loop through the joints until one of the joint’s names matches the current joint’s parent’s name. Be sure to deter­mine whether the parent name is blank first. A blank parent name signifies that the joint has no parent, and it would be a waste of time to search for one.

Initial Setup

Before you can start animating, you must do a bit more setup. Each joint’s matrices must be set to the initial rotation and translation, and the vertices and normals attached to each bone must be transformed by these matrices. Take a look at the CMs3d::Setup() function. (Because the function is too large to display here, you might want to refer to the code. CMs3d::Setup can be found in the Code/Chapter6 directory in ms3d. cpp. The function starts around line 490.)

The Setup function consists of three parts. The first part loops through the joints and creates the matrices. The first matrix created is the relative matrix, stored in the variable m_matLocal. This is the rotation and translation of the joint by itself. Using the m_fRotation and m_fTranslation arrays in the joint, the m_matLocal matrix can be created with the SetRotation and SetTranslation member functions of the matrix class.

Next comes the absolute matrix. The absolute matrix is simply the relative matrix (m_matLocal) multiplied by the joint’s parent’s relative matrix. The resulting matrix is stored in the m_matAbs variable. If the joint has no parent, the absolute matrix is the same as the relative or local matrix.

The third matrix variable (m_matFinal) is the final transformation matrix used during animation. For now, it can just be set to the same value as the absolute matrix.

The second part of the setup function involves transforming the vertices into what I call the “initial animation position”. The initial animation position is simply the position assumed by the model when the joints are set using the staring rotations and translation values— the m_fRotation and m_fTranslation values—for each joint. This position might not be included in the first animation.

To perform this transformation, all you need to do is retrieve the final matrix from the joint the current vertex is attached to. The vertex must be transformed by the inverse of this matrix. The inverse of a rotation matrix will rotate the opposite way of the initial matrix. This can be accomplished using the InvRotateVec and InvTranslateVec func­tions included in the matrix class (matrix. inl).

Last of all, you must set the normals up to ensure proper lighting. Because the normals are stored in the face structure, you have to loop

through the faces instead of the vertices. To obtain the appropriate matrix for transforming the normals, you must use the vertex indexes of the face structure to retrieve the joint and matrix used to transform the vertex, which the normal belongs to. Once you have this matrix, the normal can be rotated. Like the vertices, you use the InvRotateVec function. However, because lighting normals are unit vectors, there is no need to translate them as well.

Now, everything is set up and ready to animate.

Animation and Interpolation

Finally you get to the good stuff. All the vertices are in the right start­ing places, all the joints are set up, and all the other data is loaded and taken care of. Now you can animate the model.

The Animate function of the CMs3d class takes four parameters. The first is the speed. The speed is a floating-point value that designates how fast the model should animate. A value of 1.0f means the model should animate exactly as fast as it was meant to when it was created. A value of 2.0f will animate twice as fast as the original, and 0.5f will animate the model half as fast as the original and so on.

The next two parameters, fStartTime and fEndTime, tell the function which parts of the animation to use. Because each joint can have a varying number of keyframes and the keyframes of a joint do not need to occur at the same time intervals, it is unpractical to use a start and end keyframe. The fStartTime and fEndTime give a starting and ending time of the animation segment. If fStartTime is 0.3f and fEndTime is 0.9f, only the six tenths of a second worth of animation between the two times is drawn. Because there is nothing in the MS3D file that tells you where separate animations start and stop, you might record the start and end time of certain animations. For example, 0.0 to 0.5 seconds contains a run animation and 0.5 to 1.3 contains a jump animation. By plugging these values into the Animate() function, you can display just the run or just the jump animation.

Now you need to determine just what part of the animation to display at the current instant. It is not enough to just pick the keyframe with the time closest to the current time. Doing this would result in jerky, unnatural motion because the distance between keyframes can be large and the time it takes to move from one to another can be fairly

long. A keyframe might exist only at the start and the end of the leg movement, and simply alternating between the two frames to repre­sent walking would look awful. Instead, you need to interpolate be­tween the keyframes.

Using the timer class included with the CMs3d class, you get the time that has elapsed in seconds since the last frame. Adding this time to the time elapsed since the last time the animation was restarted, and adding the result to the beginning time specified in the parameters of the function should give you a time value to use when finding the position of the model.

Armed with this time value, you can find the “current” and “previous” frame for each frame. The time value you calculated in the previous step must fall somewhere between the previous and current keyframe, with no keyframes between them. If the time falls between frame 5 and frame 6, the previous frame is 5 and the current frame is 6.

Let’s start with the translation.

Translation

The first thing you do is find the current keyframe. This is done using a loop to increment the frame counter, starting at 0, while there are still translation keyframes, and while the time value for that current keyframe is less than your calculated elapsed time.

There are three possibilities for the value of the frame counter, as follows:

■ The first is 0. This means that you need to simply copy the translation values for the very first translation keyframe into a temp variable holding the current translation.

■ The second possibility for the frame counter is that it holds the number of the last keyframe. In this case, you do as you did for 0, except you use the last frame in the array of translation keyframes.

■ The final and most likely value is the number of a keyframe in the middle of the two extremes. If this happens, you need to interpo­late between this translation value of this keyframe and the translation value of the previous keyframe (framecounter — 1).

So just how do you do this? Remember linear interpolation from when you worked with formats such as md2? The same concept applies here.

(CurrentTime — TimeOfPrevFrame)/At

You are subtracting the time of the previous frame from the current time and then dividing by the change in time between the two frames. Using the resultant value, you can easily interpolate between your X, Y, and Z translation values.

Rotation

Now for rotation. Rotation is almost the same process. The current and previous frames are calculated in the same way, and you follow nearly the same process if the model is at the extremes. If the model is at one of its extremes, meaning the current keyframe is equal to the very first or very last keyframe of the model, the rotation values of the last keyframe are placed into a temporary matrix using the SetRotation function of the CMatrix4X4 class.

The main difference shows up when you need to interpolate between rotations. Although you could use the same method as you did for translation, a much better and more graphically pleasing method involves using quaternions. If you skipped over Chapter 2, now would be a good time to read (or re-read) it.

Because the model stores its rotation angles as three Euler angles, the first thing to do is create two quaternions using the FromEulers func­tion. You need to make two separate quaternions from the current and previous rotation keyframes. These two quaternions represent the rotations of each of the frames. Now, you must calculate the quater­nion that represents the rotation at the current position. Once the interpolation value is calculated (it is done the same way as for transla­tion), the quaternions can be fed into the SLERP function, along with the interpolation value, to create a new quaternion containing the correct rotation for the current time.

Because OpenGL uses matrices, it is necessary to convert the quater­nion. A quaternion can then be turned back into a matrix using the ToMatrix4 function of the CQuaternion class.

After the rotation matrix is built, you need to add the translation to it. Using the same temp matrix you used for rotations, you can call the

~ГГ^3-

SetTranslation function with the translation values you calculated earlier to finish it off.

There’s only one step left before you can start drawing the model again. To find the joint’s final matrix, you need to multiply the joint’s local matrix (m_matLocal) by the temporary matrix you created. This result can be stored in m_matFinal, and is the matrix that will be used for transforming the vertices and normals later.

If you want, you can actually draw the bones alone now. The X, Y, and Z positions of the bones are stored in elements 12, 13, and 14 of the joint’s final matrix. Drawing a line from this point to elements 12, 13, and 14 of the joint’s parent’s final matrix will give you an animated “skeleton” for the model, as Figure 6.3 shows.

MlLkShflFE 3D

Figure 6.3 The animated skeleton of the model all by itself.

Now that you have the new joint information, you must transform all the vertices and vertex normals. For clarity, all the code to transform and display the model is in the Cmas3d::RenderT() function. This function is called directly after the final matrices for all the joints are calculated.

As with the old render function, the meshes are drawn one at a time.

For each mesh, the materials are first set up. Then, you move on to transforming the vertices and normals. You can start the rendering right away. As the faces are looped through, the ones that have no bones attached are drawn as normal, with no modifications whatsoever.

The rest of them, on the other hand, are different stories. A temporary vector is set up to hold both the new normal and the new vertex. First the normal must be taken care of. Because the matrix transformations actually modify the vector that is passed to them, the first thing to do is put the current normal into the temporary holder. After determining which joint the normal’s vertex is attached to, you can use the Trans — form3 function to modify the temporary normal, using only the rota­tion part of the matrix.

The vertex can be transformed in much the same way. The only major difference here is that the Transform4 functions should be used, rather than Transform3. Transform4 adds the translation in as well, giving the vertex its proper position.

Everything can now be sent to the rendering API. Don’t forget to send the texture coordinates as well as the normal and vertex—make sure they are sent in the correct order!

Figure 6.4 shows the model in its full animating glory.

MlLkShflFE 3D

Figure 6.4 The transformed model in mid-animation. Notice the bones that have been drawn over the top as a reference.

As you notice in Figure 6.4, you can see the bones over the top of the model. In the demo this is done by disabling depth testing and draw­ing the bones using the technique put forward earlier. Just make sure you re-enable depth testing when you are done.

Well, there you have it. You can now load the low polygon format, which is my personal favorite. (A low polygon format simply means that it produces models with low polygon counts. Because you are dealing with games that must be rendered in real-time, low polygon models are a must. Using a high polygon model will slow down your game to the point of being unplayable.) I definitely suggest checking the editor and format out if you are an independent or money-strapped develop­ment team. It is quite nice, and comes at a great price.

Hope you enjoyed this chapter. Remember, if you have comments, questions, or suggestions, feel free to contact me at evan@codershq. com.

Conclusion

This chapter signifies the completion of your first skeletally animated format. You should now be able to load and use a very useful format that almost seems to be made just for games. You have learned how to animate bones by interpolating between their rotations and transla­tion. You have also seen the most common use of quaternions in 3D game programming, interpolating between rotations.

Finally, the chapter tied it all together to create a fully working MS3D loader, complete with skeletal animation. Be sure to check out MilkShape’s site at https://www. milkshape3d. com and check out the demo of MilkShape 3D on the CD (in the Software/MilkShape3d directory).

The next chapter covers one of the most popular formats, 3DS. You will learn how to load and render this fairly complicated format. You will learn how to use “chunk-based” formats. You will also catch a glimpse of some of the information that you can store in a file.

Для любых предложений по сайту: [email protected]