Add ProjectedXZto the game class to interpolate the X and Z positions for objects in leading and trailing cells: public Vector3 ProjectedXZVector3 position, Vector3 speed, float changeX
Trang 1These next instructions belong inside LoadContent() to load the spaceshipmodel:
shipModel = Content.Load<Model>("Models\\alien1");
shipMatrix = new Matrix[shipModel.Bones.Count];
shipModel.CopyAbsoluteBoneTransformsTo(shipMatrix);
UpdateShipPosition()is used in the game class to not only move the ship on
X and Z but also Y to match the height of the terrain below:
void UpdateShipPosition(GameTime gameTime){
const float HOVER_DISTANCE = 0.04f;
// ship's X, Y, Z position without hover distance above the ground shipPosition.Y = shipPosition.Y - HOVER_DISTANCE;
// reverse direction if right boundary exceeded
if (shipPosition.Z < -BOUNDARY && positiveDirection == false){
shipVelocity *= -1.0f;
positiveDirection = true;
}
// reverse direction if left boundary exceeded
else if (shipPosition.Z > BOUNDARY && positiveDirection == true){
shipPosition.X+= shipVelocity.X * time;
shipPosition.Y = CellHeight(shipPosition) + HOVER_DISTANCE;
428
Trang 2When you’re drawing the spaceship, the ship’s Up vector is calculated using a
weighted average of leading and trailing normal vectors in the ship’s path (see Figure
25-5) This weighted average prevents a jerking motion caused as the ship’sUpvector
changes from one cell to the next If you want to make it look as if you don’t have any
shock absorption, you can just use the normal vector for the current cell only
Whether you are calculating weighted or normal vectors, a method is required to
project or interpolate the object’s position ahead or behind When
directionScalarequals +1, the position in one cell ahead is determined When
directionScalar equals -1, the position one cell behind is determined Add
ProjectedXZ()to the game class to interpolate the X and Z positions for objects
in leading and trailing cells:
public Vector3 ProjectedXZ(Vector3 position, Vector3 speed,
float changeX = directionScalar * terrain.cellWidth * velocity.X;
float changeZ = directionScalar * terrain.cellHeight * velocity.Z;
return new Vector3(position.X + changeX, 0.0f, position.Z + changeZ);
}
CellWeight()determines the remaining distance within the current cell relative
to the total distance projected into the neighboring cell This fraction is then used to
Trang 3weight the height values and Up vectors in trailing and leading height map cells.CellWeight()belongs in the game class:
float CellWeight(Vector3 currentPosition, Vector3 nextPosition){
Vector3 currRowColumn = RowColumn(currentPosition);
int currRow = (int)currRowColumn.Z;
int currCol = (int)currRowColumn.X;
Vector3 nextRowColumn = RowColumn(nextPosition);
int nextRow = (int)nextRowColumn.Z;
int nextCol = (int)nextRowColumn.X;
// find row and column between current cell and neighbor cell
int rowBorder, colBorder;
colBorder = currCol; // next cell at left of current cell
Vector3 intersect = Vector3.Zero; // margins between current
// and next cell intersect.X = -BOUNDARY + colBorder*terrain.cellWidth;
intersect.Z = -BOUNDARY + rowBorder*terrain.cellHeight;
currentPosition.Y = 0.0f; // not concerned about height
// find distance between current position and cell border
Vector3 difference = intersect - currentPosition;
float lengthToBorder = difference.Length();
// find distance to projected location in neighboring cell
difference = nextPosition - currentPosition;
float lengthToNewCell = difference.Length();
if(lengthToNewCell==0) // prevent divide by zero
return 0.0f;
// weighted distance in current cell relative to the entire
430
Trang 4// distance to projected position
return lengthToBorder / lengthToNewCell;
}
Since the normal vector is projected in the cell ahead or trailing cell behind, an
ad-justment is required to handle situations where the current and projected cell are
both off the height map Replace the existingHandleOffHeightMap()method
with this revision to remedy this case If you don’t, you will notice the spaceship
dis-appears when it reaches the end of the world when Z is positive:
private void HandleOffHeightMap(ref int row, ref int col){
CellNormal()receives the height map row and column as parameters and
re-turns the corresponding normal vector The normal vector serves as a measure of
up-rightness for the object travelling above this location:
Vector3 CellNormal(int row, int col){
HandleOffHeightMap(ref row, ref col);
return terrain.normal[col + row * terrain.NUM_COLS];
}
Normal()projects the normal vector inside the cell according to the position
rel-ative to the surrounding height map cell vertices Chapter 24 explains theLerp()
calculation behind this projection:
Vector3 Normal(Vector3 position){
// coordinates for top left of cell
Vector3 cellPosition = RowColumn(position);
int row = (int)cellPosition.Z;
int col = (int)cellPosition.X;
// distance from top left of cell
float distanceFromLeft = position.X%terrain.cellWidth;
Trang 5float distanceFromTop = position.Z%terrain.cellHeight;
// use lerp to interpolate normal at point within cell
Vector3 topNormal = Vector3.Lerp(
CellNormal(row, col), CellNormal(row,col+1), distanceFromLeft); Vector3 bottomNormal = Vector3.Lerp(
CellNormal(row+1,col),CellNormal(row+1,col+1),distanceFromLeft); Vector3 normal = Vector3.Lerp(
topNormal, bottomNormal, distanceFromTop); normal.Normalize(); // convert to unit vector for consistency
return normal;
}
NormalWeight()is needed in the game class to allocate a weighted portion foreach normal vector contained in a fixed range along the object’s path, as shown inFigure 25-5 These weighted normal vectors are later combined to generate the up-right vector for the spaceship If you only use the current normal vector for yourship’sUpdirection, you will notice sudden changes in orientation at each cell and theride will appear to be a rough one:
Vector3 NormalWeight(Vector3 position, Vector3 speed,
float numCells, float directionScalar){
float weight = 0.0f;
float startWeight = 0.0f;
float totalSteps = (float)numCells;
Vector3 nextPosition;
Vector3 cumulativeNormal = Vector3.Zero;
for (int i = 0; i <= numCells; i++)
{ // get position in next cell
nextPosition = ProjectedXZ(position, speed, directionScalar);
if (i == 0){ // current cell startWeight = CellWeight(position, nextPosition);
weight = startWeight/totalSteps;
} else if (i == numCells) // end cell weight = (1.0f - startWeight)/totalSteps;
else // all cells in between weight = 1.0f/totalSteps;
432
Trang 6ProjectedUp()drives the normal vector calculation for the ship from the game
class This method ensures that your ship is oriented properly above the terrain face:
Vector3 ProjectedUp(Vector3 position, Vector3 speed, int numCells){
Vector3 frontAverage, backAverage, projectedUp;
// total steps must be 0 or more 0 steps means no shock absorption.
if (numCells <= 0)
return Normal(position);
// weighted average of normals ahead and behind enable smoother ride.
else{
frontAverage = NormalWeight(position, speed, numCells, 1.0f);
backAverage = NormalWeight(position, speed, numCells,-1.0f);
ShipWorldMatrix()assembles the cumulative transformation for the
space-ship It performs the same scaling and translation routine that we have implemented
in previous chapters.ShipWorldMatrix()also calculates the ship’s orientation
according to both the ship’s direction and the slope of the terrain underneath The
di-rection matrix used is described in more detail in Chapter 8 These are the steps used
to generate the direction matrix (refer to Figure 26-6):
1. Initialize a direction matrix using a fixed rotation about the Y axis This is
arbitrary but the direction vectors contained within this matrix will be
corrected later
2. Calculate the properUpvector using a weighted average of leading and
trailing normal vectors on the ship’s path, as shown in Figure 25-5
3. Generate theRightvector from the initialForwardand weightedUp
vector
4. Calculate the properForwardvector using the cross product of theUp
andRightvectors
Trang 7Matrix scale = Matrix.CreateScale(0.3f, 0.3f, 0.3f);
Matrix translation = Matrix.CreateTranslation(shipPosition);
// 1.
// generate direction matrix with fixed rotation about Y axis
Matrix dir = Matrix.CreateRotationY(MathHelper.Pi);
Vector3 velocity = Vector3.Normalize(shipVelocity);
dir.Right = Vector3.Normalize(dir.Right);
// 4.
// Re-calculate FORWARD with known UP and RIGHT vectors
dir.Forward = Vector3.Cross(dir.Up, dir.Right);
F I G U R E 2 5 - 6
Direction matrix
Trang 8dir.Forward = Vector3.Normalize(dir.Forward);
// apply other transformations along with direction matrix
return scale * rotationY * dir * translation;
}
DrawModel()is needed in the game class to draw the ship It draws the ship at
the position and with the orientation to fit the terrain location and slope:
void DrawModel(Model model){
// declare matrices
Matrix world = ShipWorldMatrix();
foreach (ModelMesh mesh in model.Meshes){
foreach (BasicEffect effect in mesh.Effects)
When you run the program, your hills will appear, and as you move over them the
camera will rise and fall with their elevation The spaceship will travel back and forth
riding the changes in terrain slope As you can see, this impressive effect was created
with very little effort
If you like the textures generated by the noncommercial version of Terragen, you
should consider purchasing a license so you have the ability to create even larger
im-age sizes and you can access more features
Trang 9C HAPTER 25 REVIEW EXERCISES
To get the most from this chapter, try out these chapter review exercises
1. Implement the step-by-step demonstration discussed in this chapter, if youhave not already done so
2. Reduce theCELL_SPANvalue to 0 inShipWorldMatrix()and run yourgame code Notice the spaceship ride is much rougher because the normalvectors are not weighted
3. Create your own height map Load it into your application To add detail,apply multitexturing to the terrain
4. Modify theheightScalevalue inside TerrainContent.cs to heighten orflatten your terrain
5. If you are feeling ambitious, try adjusting the camera’s view vector tochange with the slope of the terrain just as the spaceship does
436
Trang 10CHAPTER 26
Animated Models
Trang 11WE are sure you will agree that animated models are among the most excit-ing features of any game This chapter presents several options for ating and loading pre-animated 3D models in your code XNA does not currentlyship with a library that automatically animates 3D models, so you have to find aloader that you can integrate into your code or you have to write your own animatedmodel loader As an alternative, we provide a model loader that loads and displays
cre-animated Quake II models, which are stored in the md2 model format.
Of course, you can use MilkShape to create and export your animated models to.md2 format However, if you are using a different model loader for other 3D modelformats, you may still be able to create your model in MilkShape and then export it toyour desired format Alternatively, if you developed your 3D model in another 3Dmodel tool, you may be able to import it into MilkShape, animate it, and then export
it to a Quake II model format or other format, as needed.
Whatever method you use to develop your models, make sure you test the loadand display of your 3D models from your XNA code It is worth the time to ensureyour models load and animate properly in your loader before you invest heavily increating and animating them
T HE QUAKE II FORMAT
This chapter does not fully explain how the animated Quake II model source code
works However, a brief overview of the md2 format is presented, and if you need to
study it more, all of the Quake II model loader code is available with this book for
you to view and modify This chapter explains how you can add this MD2 class toplay your animations, change animations, play sequences of animations, or pauseand resume your animations
The MD2 format was developed by id Software, and it was first introduced as part
of id Software’s Quake II id Software has since released the source code for their
Quake II game engine to the public under the GNU General Public License Since
then, the Quake II model format has become popular with game coders because it is
reliable for animations, it is easy to implement, and decent low-cost tools are able to create models
avail-The Quake II format implements animation entirely through keyframe
anima-tions The model’s vertices are positioned at each keyframe During the animation,the vertices are projected according to their relative position on the timeline betweenthe closest keyframes
When creating Quake II models in a modeling tool such as MilkShape, you attach the groups of vertices (known as meshes) to bones These bones are connected by a
series of joints to create the skeleton The bones can be moved and rotated at differentframes in the timeline to create keyframes The attached meshes move with the boneswhen you create the animation The joints keep the bones together to ensure yourmeshes move properly within the skeletal system for the model When you export the
Trang 12C H A P T E R 2 6
model and keyframes to the md2 format, the bones are thrown out and you are left
with a header file that describes the model’s vertex data, the texture or skin
informa-tion, and the information about the keyframe animations
Unlike other model formats, Quake II models do not use the skeletal hierarchy or
skin weights that are assigned during the model-creation process This absence of
in-formation can lead to unrealistic crinkling of skin around model joints However,
you can avoid this crinkling (or minimize it) with careful planning while designing
your model Up close your Quake II model skins may appear to be a bit wobbly or
watery due to their keyframe animation, but this defect isn’t noticeable from most
distances
Quake II models cannot use more than 4,096 triangles However, this limitation
is reasonable because you can still generate decent-looking models with this polygon
count
A Closer Look at the md2 Data
This section provides a brief overview of how the md2 file is loaded and how it
en-ables your animated models
The Quake II data is stored in binary format in a manner that permits for some
compression of the vertex and frame data To help you unravel this data, the start of
the file contains a header that describes the file type, the texture properties, the vertex
properties, the total number of vertices, the total number of frames, and binary
off-sets in the file (to access details about the vertices and animation frames) Here is the
standard md2 header:
struct md2{ int fileFormatVersion; // file type which must equal 844121161
int version; // file format version which must be 8
int skinWidth; // texture width
int skinHeight; // texture height
int frameSize; // bytes per frame
int numSkins; // total skins used
int numVertices; // total vertices per frame
int numUVs; // total texture UV's
int numTris; // number of triangle coordinates
int numglCommands; // number of glCommands
int numFrames; // number of keyframes
int offsetSkins; // binary offset to skin data
int offsetUV; // offset to texture UV data
int offsetTriangle; // offset to triangle list data
int offsetFrames; // offset to frame data
int offsetglcmds; // offset to OpenGL command data
int offsetEnd; // offset to end of file
};
Trang 13Each vertex in every frame is indexed The indexes are ordered in a sequence of angle lists When the file is loaded, the indices are used to generate a list of vertex co-ordinates The coordinates are then used to build a series of triangle lists Forefficiency, you could use the glCommands data to rewrite your model-loading andanimation code to render your models using triangle strips or triangle fans
tri-As you would expect, it is possible to store more than one animation with the
Quake II format For example, your model may have a running, jumping, taunting,
saluting, crouching, and idling animation You will want to be able to switch tween these animations on demand To access this information, use the md2 header,which contains the offset to the frame descriptions The frame descriptions can beread in using a binary read at the offset All frame descriptions are located togethersequentially from the starting frame to the very last frame Each frame description in-cludes an animation name and a frame number
be-To determine the starting and ending frames for each individual animation, youmust parse each frame description so you can match the animation names Once youhave a series of matching animation names, you can store the starting and endingframe numbers in this series When you want to play the animation on demand, youcan set the frame number to the starting frame in the animation series When the ani-mation reaches the last frame, you can start the animation over again or you canswitch to another animation
During the animation sequence, the vertices are projected on the timeline betweenthe keyframes used in the animation The normal vectors must also be interpolated inthis manner
Textures with md2 Format
For the actual Quake II game, Quake II models use pcx files for textures However,
the pcx format is not supported in XNA’s content pipeline A way to get around thislimitation is to use an image-editing program such as the freeware image editorGIMP to load your *.pcx skins and save them to *.tga format, which is supported in
the content pipeline You can then use the *.tga files to texture your Quake II els Although it is possible to have more than one texture for a Quake II model, the
mod-Quake II model loader provided with this chapter only handles one texture or skin.
When you build your Quake II models, be sure to use only one skin Thecode used in this chapter can only handle one skin
A NIMATING MODELS IN MILKSHAPE
To show you how to create an animated model using MilkShape, this example onstrates how to create an animated lamp that pivots left and right and also performs
dem-a bowing dem-animdem-ation
Trang 14Creating the Quake II Model
Before you can create an animation, you first need to create a model You can create
your own model, use the one that is provided with the book, or search online for one
to use
Creating the Meshes
Your first task is to create two separate meshes for the top and bottom portions of a
lamp, similar to the ones shown on the left side of Figure 26-1 For a refresher on how
to use MilkShape to create meshes like these, review Chapter 14, “3D Models.”
To enable smooth animations, be sure to position your model at the origin
Once you have created your meshes, you need to position them together so they
appear as one lamp However, to enable the animation, you must ensure that the
meshes remain as two separate groups If your model uses more than two mesh
groups, you will need to merge them so you end up with a top mesh group and a
bot-tom mesh group Merging can be performed on the Groups tab using the Regroup
button (Merging groups is also explained in Chapter 14.)
Creating the Skeleton
Once you have the top and bottom mesh groups in position, you must add three
joints to create pivot points for the animation The end result is shown in the diagram
on the right in Figure 26-1
Joints can be added in MilkShape from the Model tab While the Joint button is
se-lected, click into the viewport to add a joint where the cursor is placed To enable
Trang 15proper mesh positioning with the bones (when animating your lamp model), youmust add each of the three joints in sequence from the bottom to the top The firstjoint is placed at the base of the lamp After the first joint is set, whenever a new joint
is added, a bone is automatically generated between the new joint and the joint thatwas previously added
To enable use of the bones as guides for the mesh animations, you must attach themeshes to the bones The bottom mesh will be attached to the bottom bone You can
select the bottom bone by clicking the joint listed at the top on the Joints tab, then lect the bottom mesh When doing this, choose the Select button on the Groups tab to
se-ensure the bottom mesh is the only mesh group highlighted in red When the bottommesh group is highlighted in red and the bottom joint is also highlighted in red, click
the Assign button on the Joints tab to assign the bottom mesh to the bottom bone Figure 26-2 shows the viewport and Joints tab where the bottom mesh has been as-
signed to the lower bone
Next, you must repeat this process to assign the top mesh to the top bone To select
the top bone, click the middle joint, which is joint2, to highlight it in red Then select the top mesh in the viewport on the Groups tab and ensure that is the only one high- lighted in red Once both the top joint and top mesh are selected, click the Assign but- ton on the Joints tab to attach the upper mesh to the upper bone.
To ensure you have the correct mesh attached to the correct bone, you canselect the joint on the Joints tab and click the SelAssigned button to highlightthe mesh that is attached
442
F I G U R E 2 6 - 2
Attaching the bottom mesh to the bottom bone