D Animation

In the first demo in , "3D Graphics," you created a simple tree that moved toward and away from the camera, rotating around the y-axis the entire time. In that demo, you didn't have camera movement yet; instead, you applied a transform to the polygons of tree. You'll apply this same concept to move and rotate any 3D object in a game: Every 3D object gets its own transform. By "3D object," I mean a group of polygons that are related to each other—often called a polygon mesh or polygon model. This polygon group can move independently of the world. For example, all the polygons that make up a robot would belong to a 3D object, and they would share the same transform. This technique involves keeping a 3D object in its own coordinate system that is different from the world coordinate system, as shown in Screenshot.

Screenshot 3D objects can have a different coordinate system than the world.

Java graphics 09fig04.gif


Instead of explicitly moving or rotating the polygons in a 3D object, the polygons are static and only the object's transform is changed. As mentioned in previous chapters, if you were to actually move and rotate a 3D object's polygons, over time floating-point errors would accumulate and you could end up with a distorted object. Instead, during the rendering process, copy each polygon to a scratch polygon and transform it by its object transform, just like you did to the tree from . To accomplish independent movement in your 3D engine, just give each object its own Transform3D, the transform class from . However, you really want an easy way to apply motion to an object, so extend the Transform3D class to add different types of motion. Keep in mind that there are two types of motion: spatial motion (displacing the object) and angular motion (rotating the object in space). And there are three types of angular motion, one for each of the x-, y-, and z-axes. Also, you want different objects to move and rotate at different speeds, and sometimes you want to tell an object to move or rotate for a certain period of time and then stop. For example, you might want to tell a robot to turn until facing a certain direction and then stop or move to be close to a player and then stop. With this in mind, let's go over some high-level goals to accomplish with object motion:

  • Move an object to location (x,y,z) at speed s.
  • Move an object in the direction (x,y,z) at speed s for the next t seconds, or forever.
  • Rotate an object around an axis at speed s for the next t seconds, or forever.
  • Rotate an object to face direction 3p/4 radians at speed s.
  • Stop all motion of an object.

All of the moving and rotating goals can be translated, at minimum, to "move at speed x for time t." At the end of the specified amount of time, the movement is stopped. (Optionally, some motion goals have no time limit, so you need to provide a way to keep that motion going forever.) Also, in addition to speed and amount of time, spatial movement requires a direction. You can accomplish these goals in a subclass of Transform3D called MovingTransform3D. The bulk of this generic movement is in MovingTransform3D's Movement inner class shown in Listing 9.2.

Listing 9.2 The Movement Inner Class of MovingTransform3D
/**
 The Movement class contains a speed and an amount of time
 to continue that speed.
*/
protected static class Movement {
 // change per millisecond
 float speed;
 long remainingTime;
 /**
 Sets this movement to the specified speed and time
 (in milliseconds).
 */
 public void set(float speed, long time) {
 this.speed = speed;
 this.remainingTime = time;
 }
 public boolean isStopped() {
 return (speed == 0) || (remainingTime == 0);
 }
 /**
 Gets the distance traveled in the specified amount of
 time in milliseconds.
 */
 public float getDistance(long elapsedTime) {
 if (remainingTime == 0) {
 return 0;
 }
 else if (remainingTime != FOREVER) {
 elapsedTime = Math.min(elapsedTime, remainingTime);
 remainingTime-=elapsedTime;
 }
 return speed * elapsedTime;
 }
}


The Movement inner class just keeps track of the speed and remaining time of a generic type of movement. Optionally, the remaining time can be FOREVER, in which case the movement never stops. For example, a projectile shot into the sky will never stop moving (unless you tell it to). The getDistance() method gets the distance traveled based on the specified amount of elapsed time (remember, distance = speed x time). Also in this method, remainingTime is decreased by elapsedTime until it reaches 0. Here you keep track of the remaining time left in the movement rather than the absolute end time. If you keep track of the absolute end time, things that manipulate the flow of time, such as pausing the game or saving and reloading the game later, would cause the movement to be incorrect. For example, if at 7:58 p.m. you said "move forward until 8:00 p.m.," but the user paused the game from 7:59 p.m. until 8:01 p.m., when the user resumed the game, the movement would stop, which would be incorrect; the movement occurred for one minute when you really wanted the movement to occur for two minutes. You'll use one instance of the Movement class here for spatial movement and three instances for angular movement (one for each axis). First, let's talk about and implement spatial movement.

Spatial Movement

Just like you did for sprites way back in , "2D Graphics and Animation," you'll give transforms a velocity to apply spatial movement. First define a couple fields in the MovingTransform3D class for the velocity of the transform:

// velocity (units per millisecond)
private Vector3D velocity;
private Movement velocityMovement;


Because the velocity vector is both a magnitude and a direction, you don't really need the speed field of the Movement class in this case. So, for velocity, the speed of the Movement is set to 1 as long as the velocity magnitude isn't 0. Otherwise, the speed is set to 0. Now let's create a few methods to explicitly set the velocity of the transform:

/**
 Sets the velocity to the specified vector.
*/
public void setVelocity(Vector3D v) {
 setVelocity(v, FOREVER);
}
/**
 Sets the velocity. The velocity is automatically set to
 zero after the specified amount of time has elapsed. If
 the specified time is FOREVER, then the velocity is never
 automatically set to zero.
*/
public void setVelocity(Vector3D v, long time) {
 if (velocity != v) {
 velocity.setTo(v);
 }
 if (v.x == 0 && v.y == 0 && v.z == 0) {
 velocityMovement.set(0, 0);
 }
 else {
 velocityMovement.set(1, time);
 }
}
/**
 Adds the specified velocity to the current velocity. If
 this MovingTransform3D is currently moving, its time
 remaining is not changed. Otherwise, the time remaining
 is set to FOREVER.
*/
public void addVelocity(Vector3D v) {
 if (isMoving()) {
 velocity.add(v);
 }
 else {
 setVelocity(v);
 }
}
/**
 Returns true if currently moving.
*/
public boolean isMoving() {
 return !velocityMovement.isStopped() &&
 !velocity.equals(0,0,0);
}


The setVelocity() methods set the velocity to be applied for a specific amount of time, or FOREVER, with the idea that the movement stops after the time is up. You can add to the velocity using the addVelocity() method (for example, if you want to add gravity to an already moving object). Finally, you can check whether the transform is moving with the isMoving() method. Next you'll write a method to explicitly define where to move:

/**
 Sets the velocity to move to the following destination
 at the specified speed.
*/
public void moveTo(Vector3D destination, float speed) {
 temp.setTo(destination);
 temp.subtract(location);
 // calc the time needed to move
 float distance = temp.length();
 long time = (long)(distance / speed);
 // normalize the direction vector
 temp.divide(distance);
 temp.multiply(speed);
 setVelocity(temp, time);
}


The moveTo() method is really a convenience method that calculates the velocity (both magnitude and direction) and the amount of time to move to reach the goal location based on the specified speed. It uses a scratch vector, temp, and calls the setVelocity() method to set the velocity. This way you can just tell an object where to move without calculating the direction and amount of time.

Angular Movement

The angular movement, or rotation, is similar to the velocity, except that you don't need a velocity vector; you need just three Movement instances:

// angular velocity (radians per millisecond)
private Movement velocityAngleX;
private Movement velocityAngleY;
private Movement velocityAngleZ;


The angular movement is pretty much the same for each axis, so we just focus on the y-axis here. First, here are the basic methods to set the angular velocity and check whether the object is currently rotating around the y-axis:

/**
 Sets the angular speed of the y axis.
*/
public void setAngleVelocityY(float speed) {
 setAngleVelocityY(speed, FOREVER);
}
/**
 Sets the angular speed of the y axis over the specified
 time.
*/
public void setAngleVelocityY(float speed, long time) {
 velocityAngleY.set(speed, time);
}
/**
 Returns true if the y axis is currently turning.
*/
public boolean isTurningY() {
 return !velocityAngleY.isStopped();
}


When using the setAngleVelocityY() methods, a positive speed is equivalent to turning counterclockwise, and a negative speed is the same as turning clockwise. The trickier part is telling the object which direction to face. Whenever you want an object to face a certain direction, you need to calculate which is faster: turning clockwise or turning counterclockwise. A lot of the time, the solution is trivial, but not all the time. Take Screenshot, for example. In this figure, the start location is on one end of the scale near –p, and the end location is on the other end of the scale near p. When you look at these angles on the circle, it's easy to tell which direction to turn. But it's not as obvious when you look at the angles on a linear scale from –p to p, as shown in the bottom of Screenshot. (Likewise, you don't want the object to turn a full circle if you tell it to turn from –p to p.)

Screenshot Finding the shortest angular distance between two angles.

Java graphics 09fig05.gif


To solve this, calculate both distances: the angular distance to the goal by turning clockwise, and the angular distance to the goal by turning counterclockwise. Whichever is shortest is the way to turn. This is shown here in the turnTo() method:

/**
 Turns the y axis to the specified angle with the specified
 speed.
*/
public void turnYTo(float angleDest, float speed) {
 turnTo(velocityAngleY, getAngleY(), angleDest, speed);
}
/**
 Turns the y axis to face the specified (x,z) vector
 direction with the specified speed.
*/
public void turnYTo(float x, float z, float angleOffset,
 float speed)
{
 turnYTo((float)Math.atan2(-z,x) + angleOffset, speed);
}
/**
 Turns the movement angle from the startAngle to the
 endAngle with the specified speed.
*/
protected void turnTo(Movement movement,
 float startAngle, float endAngle, float speed)
{
 startAngle = ensureAngleWithinBounds(startAngle);
 endAngle = ensureAngleWithinBounds(endAngle);
 if (startAngle == endAngle) {
 movement.set(0,0);
 }
 else {
 float distanceLeft;
 float distanceRight;
 float pi2 = (float)(2*Math.PI);
 if (startAngle < endAngle) {
 distanceLeft = startAngle - endAngle + pi2;
 distanceRight = endAngle - startAngle;
 }
 else {
 distanceLeft = startAngle - endAngle;
 distanceRight = endAngle - startAngle + pi2;
 }
 if (distanceLeft < distanceRight) {
 speed = -Math.abs(speed);
 movement.set(speed, (long)(distanceLeft / -speed));
 }
 else {
 speed = Math.abs(speed);
 movement.set(speed, (long)(distanceRight / speed));
 }
 }
}
/**
 Ensures the specified angle is with -pi and pi. Returns
 the angle, corrected if it is not within these bounds.
*/
protected float ensureAngleWithinBounds(float angle) {
 if (angle < -Math.PI || angle > Math.PI) {
 // transform range to (0 to 1)
 double newAngle = (angle + Math.PI) / (2*Math.PI);
 // validate range
 newAngle = newAngle - Math.floor(newAngle);
 // transform back to (-pi to pi) range
 newAngle = Math.PI * (newAngle * 2 - 1);
 return (float)newAngle;
 }
 return angle;
}


Because the turnTo() method requires that angles be between –p and p, it calls the ensureAngleWithinBounds() method to translate the angle to be within this range, if necessary. That's it for MovingTransform3D. The rest of the class is listed here in Listing 9.3.

Listing 9.3 The Rest of MovingTransform3D.java
package com.brackeen.javagamebook.math3D;
/**
 A MovingTransform3D is a Transform3D that has a location
 velocity and an angular rotation velocity for rotation around
 the x, y, and z axes.
*/
public class MovingTransform3D extends Transform3D {
 public static final int FOREVER = -1;
 // Vector3D used for calculations
 private static Vector3D temp = new Vector3D();
 // velocity (units per millisecond)
 private Vector3D velocity;
 private Movement velocityMovement;
 // angular velocity (radians per millisecond)
 private Movement velocityAngleX;
 private Movement velocityAngleY;
 private Movement velocityAngleZ;
 /**
 Creates a new MovingTransform3D
 */
 public MovingTransform3D() {
 init();
 }
 /**
 Creates a new MovingTransform3D, using the same values as
 the specified Transform3D.
 */
 public MovingTransform3D(Transform3D v) {
 super(v);
 init();
 }
 protected void init() {
 velocity = new Vector3D(0,0,0);
 velocityMovement = new Movement();
 velocityAngleX = new Movement();
 velocityAngleY = new Movement();
 velocityAngleZ = new Movement();
 }
 public Object clone() {
 return new MovingTransform3D(this);
 }
 /**
 Updates this Transform3D based on the specified elapsed
 time. The location and angles are updated.
 */
 public void update(long elapsedTime) {
 float delta = velocityMovement.getDistance(elapsedTime);
 if (delta != 0) {
 temp.setTo(velocity);
 temp.multiply(delta);
 location.add(temp);
 }
 rotateAngle(
 velocityAngleX.getDistance(elapsedTime),
 velocityAngleY.getDistance(elapsedTime),
 velocityAngleZ.getDistance(elapsedTime));
 }
 /**
 Stops this Transform3D. Any moving velocities are set to
 zero.
 */
 public void stop() {
 velocity.setTo(0,0,0);
 velocityMovement.set(0,0);
 velocityAngleX.set(0,0);
 velocityAngleY.set(0,0);
 velocityAngleZ.set(0,0);
 }
 /**
 Gets the amount of time remaining for this movement.
 */
 public long getRemainingMoveTime() {
 if (!isMoving()) {
 return 0;
 }
 else {
 return velocityMovement.remainingTime;
 }
 }
}


Note that a MovingTransform3D's update() method should be called for every frame. This is the method that does all the work, updating the transform location and (x,y,z) angles. Also, because the polygons are static, you need to apply the MovingTransform3D whenever the polygons are drawn. Next, you'll create a class to group related polygons and apply transforms to the polygons.

Screenshot


   
Comments