An Animation Controller

Published February 28, 2014
Advertisement
I published an article on animated skinned meshes earlier this week, and got a comment that more information about an animation controller and blending animations would be nice. I put together a quick animation controller yesterday, data being based on the DirectX x-file AnimationSet format. I only have limited experience with other formats, but, from what I've read, the same data is available and it should just be a matter of having format-specific file loading routines.

First: blending animations is a whole different ball-game from just animating from a single animation set. Designing a blender for an animation controller requires a lot of consideration. The simplest concept - a straight-forward (S)LERPing animation timed keys over a fixed period of time - may well produce horrendous results. Consider a "sitting" action transitioning to a "walk" action. The character will likely begin walking before he's more than partially arisen. Should the blender rely on modeling? I.e., if blending a walk into a run, can the controller know when the left foot is down in both actions? It requires some more research.

Second: I'm in the midst of transitioning to D3D11 from D3D9, so I can't spend as much time on an animation controller as I'd like to at the moment. What I came up with works, but there is a lot of improvement needed. For expediency, I used STL stuff, strings and vectors, because I didn't what to think about memory allocations. I haven't gotten into smart_ptrs and such yet, which might take care of such things.

In any case, the data used by the animation controller is structured as follows:struct AnimationTimedKey { DWORD keyTick; float val[4]; // accomodates quaternion (4) and vector (3) values};struct AnimationKey { int type; // 0 = rotation, 1 = scale, 2 = translate DWORD maxTicks; // used only for checking consistency for the animationset int numTimedKeys; // same as timedKeys.size() std::vector timedKeys;};struct Animation { std::string frameName; DWORD maxTicks; // used only for checking consistency for the animationset std::vector animKeys;};struct AnimationSet { bool checkedAgainstRootFrame; std::string animationSetName; std::vector animations; DWORD ticksPerSecond, maxTicks, curTicks; double currentTime, period, fCurTicks;};struct RootFrame { bool checkedAgainstAnimationSets; D3DXFRAME* _hierarchyRootFrame;};
Some of the struct members are used only during loading, and reflect constraints I placed on the animation set. For instance, every animation (a set of key frames associated with a single bone-frame) has a set of key-frames, each key-frame being one of 3 types (a rotation, scaling, or translation) with an associated tickcount. The keyframes must start with 0 (beginning of the sequence) and be in order by tickcount ( time ) to allow for quick searching for a particular tickcount in the set. A keyframe set can have just a single keyframe, but it must be for time 0. If there is more than one keyframe, the maximum tickcount ( the end of the sequence ) must be the same for all timed keys in the entire animation set. That is, a single animationset for a fixed length in tickcounts. Without giving it must thought, I just set the max ticks for various objects in the data, pass them on and check for consistency before I call the data an "animationset" ready for use.

I wasn't sure when the frame hierarchy (pose mode data) would be available for any particular file load, before or after the animationset load. Also, I wanted to allow for loading just an animationset, independent of any frame hierarchy previously loaded. However, the animation names (bone-frames) must match the names in the frame hierarchy to (help) ensure the animationset is applicable to a particular character model. At a minimum, each animation has to have an equivalently named frame in the hierarchy. So, both the animationset and the frame hierarchy have flags (bool's) to indicate whether compatibility has been checked.

The class, so far:class AnimController{public: AnimController(); ~AnimController(); // public methods bool Init(); bool LoadAnimationSet(std::string filePath); // extract animationset independent of frame loading // LoadAnimationSet expects file pointer to be just after "AnimationSet" but before name or "{" bool LoadAnimationSet(std::ifstream& inputStream, AnimationSet& animSet); // LoadTicksPerSecond expects file pointer to be just after "AnimTicksPerSecond but before "{" // could be private, I suppose bool LoadTicksPerSecond(std::ifstream& inputStream); bool SetAnimationSet(std::string); // not implemented yet. bool SetAnimationSet(DWORD animSetNum); // not implemented yet. bool SetTime(DWORD whichAnimSet, double setTime); // reset the current animationset bool AdvanceTime(double deltaTime); // advance action. This could be Update() or similar. bool SetHierarchy(D3DXFRAME* newRootFrame); // either before or after animationset loaded bool SetTicksPerSecond(DWORD newTicks, DWORD whichAnimationSet); // speed up/slow down as desiredprotected: bool initialized; // methods // SkipComment skips (remainder of) lines starting with "// " or "# " and returns next token // ret: false if read error, else true bool SkipComment(std::ifstream& file, std::string& token); bool LoadAnimation(std::ifstream& file, AnimationSet& animSet); bool ExtractToken(std::string& token); // separate braces from what's inside bool LoadAnimationKey(std::ifstream& file, Animation& anim); bool LoadAnimationTimedKey(std::ifstream& file, AnimationKey& animKey); // CheckCompatibility() ensures the rootframe hierarchy has framenames matching the animationsets bool CheckCompatibility(); D3DXFRAME* FrameWithName(std::string& frameName, D3DXFRAME* frame); void SetCurTicks(DWORD animSetNum); // adjusts period, etc. bool CalculateAnimationMatrices(Animation& anim, DWORD curTicks, double fTicks); D3DXMATRIX& CalculateKeyMatrix(AnimationKey& animKey, DWORD curTicks, double fTicks); // attributes DWORD _ticksPerSecond; RootFrame _rootFrame; // for storing TransformationMatrix and finding names DWORD curAnimSet; std::vector _animSets;};
Some of the methods I came up with before coding. Others were added as I needed them. I had neglected to make FrameWithName global in my mesh class (mentioned in the Animated Skinned Mesh article), so I added it in. I can code a function faster than I can setup headers and extern's, and I usually regret not taking the time. That's an example.

For the sake of publishing this entry, here's two of the more important functions.

When AdvanceTime is called, the parameters for the current animation time are updated, including a calculation of a floating point fTicks (ticksPerSecond * currentTime) used for interpolation. Then, for each animation in the set:
bool AnimController::CalculateAnimationMatrices(Animation& anim, DWORD curTicks, double fTicks){ static D3DXMATRIX animMat; // set default D3DXMatrixIdentity(&animMat); for (size_t i = 0; i < anim.animKeys.size(); i++) { //if (anim.frameName == "Armature") //{ // //OutputDebugString("CalAnimMatrices: Armature\n"); //} animMat *= CalculateKeyMatrix(anim.animKeys, curTicks, fTicks); // GLOBALMSG } D3DXFRAME* frame = FrameWithName(anim.frameName, _rootFrame._hierarchyRootFrame); if (frame == NULL) return false; //if (frame) frame->TransformationMatrix = animMat; if (frame) ((MultiAnimFrame*)frame)->animTestFrame = animMat; return true;}
Calculate the interpolated value for the timed keys. Returned as a matrix ready for multiplication.
// CalculateKeyMatrix calculates a matrix for rotation, scale or translation which is// an interpolation of two keys: key with tickcount <= curTicks, and key with// tickcount >= curTicks. Because a matrix is calculated, this routine cannot be used for// animationset blending. For blending, a set of timed keys should be returned for each// animationset, and those timed keys interpolated by blend weight between animationsets.//// This routine supports instancing of the animationset as no calculated time related data// is stored in the animation controller.//// TODO: use separate routines for rotation, scaling and translation types to avoid// the switch statements - increases code size but maybe increase efficiency?// TODO: consider analysing timedKeys during loading, looking for duplicate entries// (using an epsilon), to reduce the number of timed keys// TODO: calculate NLERP, SLERP and LERP "long-hand" rather than using library functions//D3DXMATRIX& AnimController::CalculateKeyMatrix(AnimationKey& animKey, DWORD curTicks, double fTicks){ // This algorithm assumes that the timed key array: // - have first entry with tickcount == 0 // - is in time sequential order static D3DXMATRIX mat; // set default return D3DXMatrixIdentity(&mat); // assume that fTicks is always >= curTicks, and curTicks is < maxTicks // that is, the following search will never reach vector.end() // find the key for a tick count > curTicks std::vector::iterator iter = animKey.timedKeys.begin(), iterBefore; // TODO: use a "last key used" index and start the search there as, for most animations // at small delta-times will have the same curTicks several times in a row. // NOTE: that approach is incompatible with instancing (multiple instances using the same // animationset) unless "last key used" is stored separately for EACH instance. // TODO: increment using animKey.numTimedKeys rather than use iterator. // Get an early return for single entries. while (iter != animKey.timedKeys.end() && iter->keyTick < curTicks) iter++; // iter is at or above curTicks if (iter == animKey.timedKeys.end()) { // error condition return mat; } // get two timed keys. This is based on the assumption that // fTicks will always be >= curTicks if (fTicks > double(curTicks)) { iterBefore = iter; iter++; if (iter == animKey.timedKeys.end()) return mat; } else iterBefore = iter - 1; float ratio = float((fTicks - double(iterBefore->keyTick)) / (double(iter->keyTick - iterBefore->keyTick))); //float ratio = float(curTicks - iterBefore->keyTick) / float(iter->keyTick - iterBefore->keyTick); D3DXQUATERNION quat, quat1, quat2; D3DXVECTOR3 vec1, vec2, vec; // calc the matrix by timedkey type switch (animKey.type) { case 0: // rotation // ugly use of iterators! quat1 = D3DXQUATERNION(iterBefore->val[1], iterBefore->val[2], iterBefore->val[3], -iterBefore->val[0]); quat2 = D3DXQUATERNION(iter->val[1], iter->val[2], iter->val[3], -iter->val[0]); D3DXQuaternionSlerp(&quat, &quat1, &quat2, ratio); return *D3DXMatrixRotationQuaternion(&mat, &quat); case 1: // scale vec1 = D3DXVECTOR3(iterBefore->val[0], iterBefore->val[1], iterBefore->val[2]); vec2 = D3DXVECTOR3(iter->val[0], iter->val[1], iter->val[2]); vec = (1.0f - ratio)*vec1 + (ratio)*vec2; return *D3DXMatrixScaling(&mat, vec.x, vec.y, vec.z); case 2: // translate vec1 = D3DXVECTOR3(iterBefore->val[0], iterBefore->val[1], iterBefore->val[2]); vec2 = D3DXVECTOR3(iter->val[0], iter->val[1], iter->val[2]); vec = (1.0f - ratio)*vec1 + (ratio)*vec2; return *D3DXMatrixTranslation(&mat, vec.x, vec.y, vec.z); default: return mat; // error condition }}
That's all for now. LOTS of improvements can be made as this was all coded within an hour or two with the only consideration being proof-of-concept.

Mar 1 2014: Revised the routine to eliminate the use of vector::iterator as follows:
D3DXMATRIX& AnimController::CalculateKeyMatrix2(AnimationKey& animKey, DWORD curTicks, double fTicks){ static D3DXMATRIX mat; // set default D3DXMatrixIdentity(&mat); int i = 0; D3DXQUATERNION quat, quat1, quat2; D3DXVECTOR3 vec1, vec2, vec; if (animKey.numTimedKeys != 1) // other than just a tick==0 key { // find timedkey with ticks >= curTicks while (i < animKey.numTimedKeys && animKey.timedKeys.keyTick < curTicks) i++; if (i >= animKey.numTimedKeys) return mat; // error condition // SetCurTicks() should ensure the following condition is always true if (i > 0 && i < animKey.numTimedKeys-1) { AnimationTimedKey key1 = animKey.timedKeys; AnimationTimedKey key2 = animKey.timedKeys[i+1]; float ratio = float((fTicks - double(key1.keyTick)) / (double(key2.keyTick - key1.keyTick))); switch (animKey.type) { case 0: // rotation quat1 = D3DXQUATERNION(key1.val[1], key1.val[2], key1.val[3], -key1.val[0]); quat2 = D3DXQUATERNION(key2.val[1], key2.val[2], key2.val[3], -key2.val[0]); D3DXQuaternionSlerp(&quat, &quat1, &quat2, ratio); return *D3DXMatrixRotationQuaternion(&mat, &quat); case 1: // scale, translate case 2: vec1 = D3DXVECTOR3(key1.val[0], key1.val[1], key1.val[2]); vec2 = D3DXVECTOR3(key2.val[0], key2.val[1], key2.val[2]); vec = (1.0f - ratio)*vec1 + (ratio)*vec2; if (animKey.type==1) return *D3DXMatrixScaling(&mat, vec.x, vec.y, vec.z); return *D3DXMatrixTranslation(&mat, vec.x, vec.y, vec.z); default: return mat; // error condition } } } // default to using the tick==0 timed key AnimationTimedKey key = animKey.timedKeys[0]; switch (animKey.type) { case 0: quat = D3DXQUATERNION(key.val[1], key.val[2], key.val[3], -key.val[0]); return *D3DXMatrixRotationQuaternion(&mat, &quat); case 1: case 2: vec = D3DXVECTOR3(key.val[0], key.val[1], key.val[2]); if (animKey.type == 1) return *D3DXMatrixScaling(&mat, vec.x, vec.y, vec.z); return *D3DXMatrixTranslation(&mat, vec.x, vec.y, vec.z); default: return mat; }}
I didn't profile it, but it seems to be a bit faster. It certainly looks better.

I cleaned up a bit of the support code. The GLOBALMSG notations indicate some possible silent failures that could be reported if DEBUG defined, or (eventually) a compile- or run-time option to return something with a description.
bool AnimController::SetTicksPerSecond(DWORD newTicks, DWORD whichAnimationSet){ if (!initialized) return false; if (whichAnimationSet >= _animSets.size()) return false; // GLOBALMSG if (newTicks == 0) return false; // GLOBALMSG AnimationSet& animSet = _animSets[whichAnimationSet]; animSet.ticksPerSecond = newTicks; animSet.currentTime = 0; animSet.curTicks = 0; animSet.period = double(animSet.maxTicks) / double(animSet.ticksPerSecond); return true;}// To be called when currentTime has been changed// The intent is to ensure that the current tick count is >= 0 and <= maxTicks// to ensure timedKey access index will be valid (i.e., avoid "index out of range"bool AnimController::SetCurTicks(DWORD animSetNum){ if (animSetNum >= _animSets.size()) return false; double curTime = _animSets[animSetNum].currentTime; double period = _animSets[animSetNum].period; // NOTE: the following will cause the animation to LOOP from the end of the animation // back to the beginning. // Other actions which could be taken: // - ping-pong: at the end of an action, reverse back through the keyframes // to the beginning, etc. // - terminate the animation: perhaps provide a callback to report same while (curTime >= period) curTime -= period; // loop within the animation // SAFETY if (curTime < 0) curTime = -curTime; // loop back into the animation _animSets[animSetNum].currentTime = curTime; // At this point, the current time is guanteed to be < the period, // and ticks, therefore, less than maxTicks. The intent is to ensure the last timedkey // will not be indexed and CalculateKeyMatrix will always have a "next higher" timedKey DWORD ticks = (DWORD)((double)_animSets[animSetNum].ticksPerSecond * _animSets[animSetNum].currentTime); double fTicks = (double)_animSets[animSetNum].ticksPerSecond * _animSets[animSetNum].currentTime; // calculated and then loaded for debugging purposes. _animSets[animSetNum].curTicks = ticks; _animSets[animSetNum].fCurTicks = fTicks; return true;}// advance the tick count.// calculate animation matrices and store matrices in hierarchy TransformationMatrixbool AnimController::AdvanceTime(double deltaTime){ if (!initialized) return false; _animSets[curAnimSet].currentTime += deltaTime; if (!SetCurTicks(curAnimSet)) return false; // loop through animations for (size_t i = 0; i < _animSets[curAnimSet].animations.size(); i++) { if (!CalculateAnimationMatrices(_animSets[curAnimSet].animations, _animSets[curAnimSet].curTicks, _animSets[curAnimSet].fCurTicks)) return false; // GLOBALMSG } return true;}
Still a work in progress.
1 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement