Animator - Fish-In-A-Suit/Conquest GitHub Wiki
Animation is a series of keyframes and a keyframe is a certain pose at certain time of the animation. The job ob Animator is to apply an animation to an AnimatedModel by going though the animation, calculating the current pose of the model and setting joint transforms accordingly.
Every frame Animator increases the animation time, so that the animation keeps progressing. It then calculates the current pose in bone-space by interpolating between poses at previous and next keyframes. Finally it calculates and sets the joint transforms in model-space for all the joints to apply the pose to the model.
package animation;
import java.util.HashMap;
import java.util.Map;
import org.lwjgl.util.vector.Matrix4f;
import animatedModel.AnimatedModel;
import animatedModel.Joint;
import utils.DisplayManager;
/**
*
* This class contains all the functionality to apply an animation to an
* animated entity. An Animator instance is associated with just one
* {@link AnimatedModel}. It also keeps track of the running time (in seconds)
* of the current animation, along with a reference to the currently playing
* animation for the corresponding entity.
*
* An Animator instance needs to be updated every frame, in order for it to keep
* updating the animation pose of the associated entity. The currently playing
* animation can be changed at any time using the doAnimation() method. The
* Animator will keep looping the current animation until a new animation is
* chosen.
*
* The Animator calculates the desired current animation pose by interpolating
* between the previous and next keyframes of the animation (based on the
* current animation time). The Animator then updates the transforms all of the
* joints each frame to match the current desired animation pose.
*
* @author Karl
*
*/
public class Animator {
private final AnimatedModel entity;
private Animation currentAnimation;
private float animationTime = 0;
/**
* @param entity
* - the entity which will by animated by this animator.
*/
public Animator(AnimatedModel entity) {
this.entity = entity;
}
/**
* Indicates that the entity should carry out the given animation. Resets
* the animation time so that the new animation starts from the beginning.
*
* @param animation
* - the new animation to carry out.
*/
public void doAnimation(Animation animation) {
this.animationTime = 0;
this.currentAnimation = animation;
}
/**
* This method should be called each frame to update the animation currently
* being played. This increases the animation time (and loops it back to
* zero if necessary), finds the pose that the entity should be in at that
* time of the animation, and then applies that pose to all the model's
* joints by setting the joint transforms.
*/
public void update() {
if (currentAnimation == null) {
return;
}
increaseAnimationTime();
Map<String, Matrix4f> currentPose = calculateCurrentAnimationPose();
applyPoseToJoints(currentPose, entity.getRootJoint(), new Matrix4f());
}
/**
* Increases the current animation time which allows the animation to
* progress. If the current animation has reached the end then the timer is
* reset, causing the animation to loop.
*/
private void increaseAnimationTime() {
animationTime += DisplayManager.getFrameTime();
if (animationTime > currentAnimation.getLength()) {
this.animationTime %= currentAnimation.getLength();
}
}
/**
* This method returns the current animation pose of the entity. It returns
* the desired local-space transforms for all the joints in a map, indexed
* by the name of the joint that they correspond to.
*
* The pose is calculated based on the previous and next keyframes in the
* current animation. Each keyframe provides the desired pose at a certain
* time in the animation, so the animated pose for the current time can be
* calculated by interpolating between the previous and next keyframe.
*
* This method first finds the preious and next keyframe, calculates how far
* between the two the current animation is, and then calculated the pose
* for the current animation time by interpolating between the transforms at
* those keyframes.
*
* @return The current pose as a map of the desired local-space transforms
* for all the joints. The transforms are indexed by the name ID of
* the joint that they should be applied to.
*/
private Map<String, Matrix4f> calculateCurrentAnimationPose() {
KeyFrame[] frames = getPreviousAndNextFrames();
float progression = calculateProgression(frames[0], frames[1]);
return interpolatePoses(frames[0], frames[1], progression);
}
/**
* This is the method where the animator calculates and sets those all-
* important "joint transforms" that I talked about so much in the tutorial.
*
* This method applies the current pose to a given joint, and all of its
* descendants. It does this by getting the desired local-transform for the
* current joint, before applying it to the joint. Before applying the
* transformations it needs to be converted from local-space to model-space
* (so that they are relative to the model's origin, rather than relative to
* the parent joint). This can be done by multiplying the local-transform of
* the joint with the model-space transform of the parent joint.
*
* The same thing is then done to all the child joints.
*
* Finally the inverse of the joint's bind transform is multiplied with the
* model-space transform of the joint. This basically "subtracts" the
* joint's original bind (no animation applied) transform from the desired
* pose transform. The result of this is then the transform required to move
* the joint from its original model-space transform to it's desired
* model-space posed transform. This is the transform that needs to be
* loaded up to the vertex shader and used to transform the vertices into
* the current pose.
*
* @param currentPose
* - a map of the local-space transforms for all the joints for
* the desired pose. The map is indexed by the name of the joint
* which the transform corresponds to.
* @param joint
* - the current joint which the pose should be applied to.
* @param parentTransform
* - the desired model-space transform of the parent joint for
* the pose.
*/
private void applyPoseToJoints(Map<String, Matrix4f> currentPose, Joint joint, Matrix4f parentTransform) {
Matrix4f currentLocalTransform = currentPose.get(joint.name);
Matrix4f currentTransform = Matrix4f.mul(parentTransform, currentLocalTransform, null);
for (Joint childJoint : joint.children) {
applyPoseToJoints(currentPose, childJoint, currentTransform);
}
Matrix4f.mul(currentTransform, joint.getInverseBindTransform(), currentTransform);
joint.setAnimationTransform(currentTransform);
}
/**
* Finds the previous keyframe in the animation and the next keyframe in the
* animation, and returns them in an array of length 2. If there is no
* previous frame (perhaps current animation time is 0.5 and the first
* keyframe is at time 1.5) then the first keyframe is used as both the
* previous and next keyframe. The last keyframe is used for both next and
* previous if there is no next keyframe.
*
* @return The previous and next keyframes, in an array which therefore will
* always have a length of 2.
*/
private KeyFrame[] getPreviousAndNextFrames() {
KeyFrame[] allFrames = currentAnimation.getKeyFrames();
KeyFrame previousFrame = allFrames[0];
KeyFrame nextFrame = allFrames[0];
for (int i = 1; i < allFrames.length; i++) {
nextFrame = allFrames[i];
if (nextFrame.getTimeStamp() > animationTime) {
break;
}
previousFrame = allFrames[i];
}
return new KeyFrame[] { previousFrame, nextFrame };
}
/**
* Calculates how far between the previous and next keyframe the current
* animation time is, and returns it as a value between 0 and 1.
*
* @param previousFrame
* - the previous keyframe in the animation.
* @param nextFrame
* - the next keyframe in the animation.
* @return A number between 0 and 1 indicating how far between the two
* keyframes the current animation time is.
*/
private float calculateProgression(KeyFrame previousFrame, KeyFrame nextFrame) {
float totalTime = nextFrame.getTimeStamp() - previousFrame.getTimeStamp();
float currentTime = animationTime - previousFrame.getTimeStamp();
return currentTime / totalTime;
}
/**
* Calculates all the local-space joint transforms for the desired current
* pose by interpolating between the transforms at the previous and next
* keyframes.
*
* @param previousFrame
* - the previous keyframe in the animation.
* @param nextFrame
* - the next keyframe in the animation.
* @param progression
* - a number between 0 and 1 indicating how far between the
* previous and next keyframes the current animation time is.
* @return The local-space transforms for all the joints for the desired
* current pose. They are returned in a map, indexed by the name of
* the joint to which they should be applied.
*/
private Map<String, Matrix4f> interpolatePoses(KeyFrame previousFrame, KeyFrame nextFrame, float progression) {
Map<String, Matrix4f> currentPose = new HashMap<String, Matrix4f>();
for (String jointName : previousFrame.getJointKeyFrames().keySet()) {
JointTransform previousTransform = previousFrame.getJointKeyFrames().get(jointName);
JointTransform nextTransform = nextFrame.getJointKeyFrames().get(jointName);
JointTransform currentTransform = JointTransform.interpolate(previousTransform, nextTransform, progression);
currentPose.put(jointName, currentTransform.getLocalTransform());
}
return currentPose;
}
}
Animator references AnimatedModel that it is currently applying animation to with variable named entity. Animation currentAnimation is the current animation that is to be applied to the model. animationTime is used so that Animator knows how far the animation has already progressed.
doAnimation(Animation animation)
sets the current Animation to be applied to the model and resets animationTime (to 0), so the animation is properly played from the beginning.
"All of the real action" happens in the update()
method, which is the method that is responsible for animating the model. This method needs to be called every frame. It does three things:
- increase the animation time, so that the animation progresses. When it reaches the end of the animation, it sets animationTime back to 0, which is what causes animation to keep looping.
- calculates the current pose based on the animation time and the keyframe information it calculates the current pose for the model (position in which every joint in the model should currently be). The pose is represented as a Map, with String as key (referring to the name of the joint) and Matrix4f as value (referring to the bone-space transform of that joint). To calculate this pose, it first finds the previous and the next keyframe in the animation based on the current animation time:
KeyFrame[] frames = getPreviousAndNextFrames()
. It then calculates the progression value, which indicates how far between these two keyframes the animation currently is. Finally, it interpolates between those keyframes using progression. - applies the pose to the joints by calculating and setting joint transforms (still in bone-space)
Method KeyFrame[] getPreviousAndNextFrames()
finds the previous and next keyframe in the animation based on animationTime. It loops though the frames until it finds one with a time that is greater than the current animation time, so that must therefore be the next keyframe. The previous keyframe is therefore the keyframe from the previous iteration. This method needs heavy optimization, however. For example, you could keep track of the current frame, so you don't have to loop through the keyframes per every frame.
calculateCurrentAnimationPose(KeyFrame previousFrame, KeyFrame nextFrame)
calculates how far between the previous and next keyframe the current animation time is and returns it as a value between 0 and 1. To do this, the total time (in seconds) between the previous and next keyframe is calculated; it also calculates time in seconds indicating how far after the previous keyframe the animation time currently is --> divide time since last frame with total time to get a **progression **value between 0 and 1.
Method interpolatePoses(KeyFrame previousFrame, KeyFrame nextFrame, float progression)
calculates all the bone-space joint transforms for the desired current pose by interpolating between the transforms ast the previous and next keyframes. They (joint transforms) are returned in a map, indexed by the name of the joint to which they should be applied. It gets the joint transforms of previousFrame and nextFrame, interpolates between them using the method interpolate
in the JointTransform class. It then stores this current transform in a Map as a Matrix4f, with the name of the joint being the key.
So, now we have achieved to do the following:
- increase the animation time
- calculate what current pose of the model should be (stored as currentPose, but still bone-space)
All is left to do is to put the AnimatedModel in that pose by setting the joint transforms (in the Joint class). However, joint transformns in the Joint class represenet model-space transforms to transform a Joint from its original model-space position when no animation is applied, to its model-space position when animation is applied. The pose that we have just calculated above stores bone-space transforms for each joint --> there is a need to calculate final joint transforms in model-space.
These calculations all take place in applyPoseToJoints(Map<String, Matrix4f> curPose, Joint joint, Matrix4f parentTransform)
method. This method gets called recursively for every Joint in the model. For each joint, we get its current pose bone-space transform by getting it from the hashmap using its name: Matrix4f currentLocalTransform currentLocalTransform = currentPose.get(joint.name)
. Next, this transform is converted from bone-space into model space: Matrix4f currentTransform = Matrix4f.mul(parentTransform, currentLocalTransform);
This method then gets called recursively for all the children joints. Then comes the final calculation to calculate the joint transform, which uses the inverse of the bind transform of a Joint. It's quite tricky so I'd suggest watching this video at 10:26. Take a look at the following image:
Blue shape refers to the original position of a joint in a model when no animation is applied. Purple position is the position of the joint in the current pose. The transform that we are trying to calculate is denoted by the green arrow. We have model-space joint transform in the pose position, since we've calculated it in the applyPoseToJoints
method (currentTransform). To perform the calculation, the original model-space position of the joint when no animation is applied is needed aswell ("model-space bind transform"). The calculation seems pretty easy, since the majority of the people would subtract T and S. However, 4x4 matrices are not vectors. In matrix math, the same result is achieved by multiplying model-space pose transform of a joint (T) with the inverse of the local bind transform (S):
This final transform is then set to the current Joint instance as it's transform.
After this method is called for all the joints, the model will be able to be rendered in the current pose.