Clips
An animation clip is a collection of transform tracks. Each track describes the motion of one joint over time. All the tracks combined describe the motion of the animated model over time. When an animation clip is sampled, it yields a Pose
object.
For a basic clip class, all we really need is a vector of Transform Tracks. Because each transform tracks contain the id of the joint it effects, we can have a minimal number of tracks per clip. We also want to keep track of some meta-data like the name of the clip, whether or not the clip is looping and information about the time or duration of the clip.
class Clip { protected: std::vector<TransformTrack> mTracks; std::string mName; float mStartTime; float mEndTime; bool mLooping; // Rest of the class
The cahced start and end times of the Clip
class are calculated similar to the cached start and end times of the TransformTrack
class. Any time any track contained within a clip changes, the cached times must be re-calculated. The start time is the lowest start time of the transform tracks contained in the animation clip. Similarly, the end time is the largest end time of the transform tracks.
When sampling a Clip
, there is no guarantee that the requested time will be within the start and end times of the clip. To deal with this, we implement a helper function that adjusts the provided time to be within the range of the current animation clip. The Sample function will call Clip::AdjustTimeToFitRange
on the input time and return the adjusted time.
float Clip::AdjustTimeToFitRange(float inTime) { if (mLooping) { float duration = mEndTime - mStartTime; if (duration <= 0) { return 0.0f; } inTime = fmodf(inTime - mStartTime, mEndTime - mStartTime); if (inTime < 0.0f) { inTime += mEndTime - mStartTime; } inTime = inTime + mStartTime; } else { if (inTime < mStartTime) { inTime = mStartTime; } if (inTime > mEndTime) { inTime = mEndTime; } } return inTime; }
The Sample
function loops trough every track in the current clip. Each Transform Track is then sampled, and the resulting transform is stored in the output pose. When sampling the track, the local transform of the pose is used as the reference transform. This way, if a track only rotates a joint, only the rotation component changes.
float Clip::Sample(Pose& outPose, float time) { if (GetDuration() == 0.0f) { return 0.0f; } time = AdjustTimeToFitRange(time); unsigned int size = mTracks.size(); for (unsigned int i = 0; i < size; ++i) { unsigned int joint = mTracks[i].GetId(); Transform reference = outPose.GetLocalTransform(joint); Transform animated = mTracks[i].Sample(reference, time, mLooping); outPose.SetLocalTransform(joint, animated); } return time; }
Loading Clips
Animation clips are typically loaded from some file created using a 3D content creation application like Maya. The most common file formats are: collada, fbx, and glTF. In Hands-On C++ Game Animation Programming, animation data is loaded from glTF files using cgltf.
Poses for animating a hierarchy
There are two poses to be aware of for animating a hierarchy. These are the rest pose and the animated pose. The rest pose is a reference pose, it's the pose everything is being animated from. The animated pose is the result of sampling an animation clip.
The naming convention for these poses is not standard. Different animation systems and content creation tools might call these poses different names. One big point of confusion is the difference between rest and bind pose. In an ideal world, i think the rest and bind poses should always be the same. 3D content creation tools disagree.
Often the difficulty in animation programming is working with the different terminology that different standards and application use. To animate a hierarchy (without displaying a skinned mesh), the only poses we will need are the rest pose and animated pose.