• Announcements

    • khawk

      Download the Game Design and Indie Game Marketing Freebook   07/19/17

      GameDev.net and CRC Press have teamed up to bring a free ebook of content curated from top titles published by CRC Press. The freebook, Practices of Game Design & Indie Game Marketing, includes chapters from The Art of Game Design: A Book of Lenses, A Practical Guide to Indie Game Marketing, and An Architectural Approach to Level Design. The GameDev.net FreeBook is relevant to game designers, developers, and those interested in learning more about the challenges in game development. We know game development can be a tough discipline and business, so we picked several chapters from CRC Press titles that we thought would be of interest to you, the GameDev.net audience, in your journey to design, develop, and market your next game. The free ebook is available through CRC Press by clicking here. The Curated Books The Art of Game Design: A Book of Lenses, Second Edition, by Jesse Schell Presents 100+ sets of questions, or different lenses, for viewing a game’s design, encompassing diverse fields such as psychology, architecture, music, film, software engineering, theme park design, mathematics, anthropology, and more. Written by one of the world's top game designers, this book describes the deepest and most fundamental principles of game design, demonstrating how tactics used in board, card, and athletic games also work in video games. It provides practical instruction on creating world-class games that will be played again and again. View it here. A Practical Guide to Indie Game Marketing, by Joel Dreskin Marketing is an essential but too frequently overlooked or minimized component of the release plan for indie games. A Practical Guide to Indie Game Marketing provides you with the tools needed to build visibility and sell your indie games. With special focus on those developers with small budgets and limited staff and resources, this book is packed with tangible recommendations and techniques that you can put to use immediately. As a seasoned professional of the indie game arena, author Joel Dreskin gives you insight into practical, real-world experiences of marketing numerous successful games and also provides stories of the failures. View it here. An Architectural Approach to Level Design This is one of the first books to integrate architectural and spatial design theory with the field of level design. The book presents architectural techniques and theories for level designers to use in their own work. It connects architecture and level design in different ways that address the practical elements of how designers construct space and the experiential elements of how and why humans interact with this space. Throughout the text, readers learn skills for spatial layout, evoking emotion through gamespaces, and creating better levels through architectural theory. View it here. Learn more and download the ebook by clicking here. Did you know? GameDev.net and CRC Press also recently teamed up to bring GDNet+ Members up to a 20% discount on all CRC Press books. Learn more about this and other benefits here.
Sign in to follow this  
Followers 0
  • entries
    3
  • comments
    0
  • views
    4864

An Animation Controller

Sign in to follow this  
Followers 0
Buckeye

1190 views

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
Sign in to follow this  
Followers 0


0 Comments


There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!


Register a new account

Sign in

Already have an account? Sign in here.


Sign In Now