• Advertisement
Sign in to follow this  
  • entries
    3
  • comments
    0
  • views
    5126

About this blog

Anything to do with theory and coding of animated skinned meshes

Entries in this blog

Notice: Rather than continuing this particular part of my blog, I buckled down and wrote an article on a blending-capable animation controller. No sense in putting more here.

Continuing with exploration for the development of an animation controller for a skinned mesh, I've added a "track" to the concept to support blending of two or more animation sets (character "actions".) It isn't a fluke that it looks similar to ID3DXAnimationController. I'm familiar with the behavior of that object and could probably do worse than emulate its features.

The previous implementation of the animation controller calculated interpolated keyframes as matrices and stored them directly in the frame hierarchy. Blending of animation sets (for my purposes) means combining, in some way or another, interpolated values from each of the animation sets to be blended. Each timed key comes in one of three flavors: scale, rotation or translation. The AnimTrackKey structure (below) provides storage for the interpolated value of each type. I maintain the SRT values separately (rather than as a matrix) to permit interpolation with other animation sets without the need for matrix decomposition (which isn't robust, in any case), before the transformation matrices are formed and stored in the frame hierarchy.

To support blending of animation sets, a temporary location is needed for storing the AnimTrackKey interpolated data. The AnimTrack struct contains a std::vector of AnimTrackKeys, trackKeys, representing the interpolated data for each bone-frame at one tickcount of the animation set associated with that track. The order in which the keys are stored must be the same for all tracks. I.e., an index into the AnimTrackKey vector must correspond to the same frame in the frame hierarchy. That allows trackKey[ n ] for one track to be combined in some fashion with trackKey[ n ] in another track, with the assurance that the result is applicable to, and the transformation matrix can be stored in, the correct frame in the hierarchy.

I added some internal functions to support that concept. The process loads all the frame names in the hierarchy in the order they're recursivley accessed in a fixed manner (load a frame's name; load the frame-names from the frame's siblings and its children; load the frame-names from the frame's child and the child's children). A single instance of the animation controller corresponds to a single instance of a frame hierarchy. The frame names are stored in a std::vector frameNames, a data member of that animation controller.

When the frame names have been loaded, the animation sets are looped through, and each animation (the timed key data for a single frame) is given an index number corresponding to the index into the frameNames vector for that animation's frame-name. Following that, each track's trackKey vector is sized for frameNames.size() and filled with default AnimTrackKeys. Default AnimTrackKeys ("identity" transformations) are used to allow for animation sets which may not have animations for every frame in the hierarchy. In the process of blending, one track may animate just a head movement, and another track may not animate the head at all. When the track keys are combined, the Armature_Head bone (for instance) will be just be the transform from the first track.

The functions for interpolating a single animation timed key array was modified to store the data with the proper trackKeys index passed to it. The AdvanceTime function was modified to loop through each enabled track and do the calcs for that animationset. Before exiting that function, the various trackKeys are combined in a Transformation matrix which is stored in the frame hierarchy.

At present, the code works fine for a single animation set. I haven't actually blended two tracks yet, but I have the assets to test that. When I've done a bit more coding and testing, I'll post some more code that may be of interest.

Animation Blending

In the above discussion, note that I used the word "combined" with regard to the blending of trackKeys. At the present, it's hard-coded as an interpolation but may be modified to allow user selection of the combination method: interpolation, adding, subtracting, etc.

I'm trying to put a bit of flexibilty into the animation blending process as combining animations for various animation sets could result in enough difficulty for modelers and programmers as it is. Consider an sitting-idle action for a character that transitions to a walk action. Without synchronization of the animations, the character may appear to begin walking before it's risen from the sitting position. More research and contemplation needed.
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 conistency for the animationset int numTimedKeys; // same as timedKeys.size() std::vector timedKeys;};struct AnimTrackKey{ AnimTrackKey() { Init(); } void Init() { tQuat = D3DXQUATERNION(0, 0, 0, 1); tScale.x = tScale.y = tScale.z = 1; tTrans.x = tTrans.y = tTrans.z = 0; } D3DXQUATERNION tQuat; D3DXVECTOR3 tScale, tTrans;};struct Animation { Animation() { trackKeyIndex = -1; } std::string frameName; int trackKeyIndex; // storage location in AnimTrack.trackKeys for interpolated timedKeys DWORD maxTicks; // used only for checking conistency 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;};enum ANIMTRACK_PRIORITY { ANIMTRACK_LOW_PRI, ANIMTRACK_HIGH_PRI };struct AnimTrackDesc{ ANIMTRACK_PRIORITY trackPriority; float weight; float speed; double position; bool enable;};struct AnimTrack{ UINT animSet; // animationSet currently assigned to this track std::vector trackKeys; // interpolated timed keys for curTicks AnimTrackDesc desc;};
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.
From the time I started building and programming computers ... [this space intentionally left blank] years ago, I've always been interested in computer animation.

My first computer was a prototype board, wire-wrapped, scratch-built using an Fairchild F8 (8-bit) processor. I added [hang onto your hats] 64 bytes of external static ram! Yeah, you read that right - 64 bytes. The F8 had 64 bytes of internal ram for a whopping total of 128 bytes! And I had that cpu humming along at 128KHz. I was working at Oak Ridge National Lab (ORNL) at the time and was able to program a UV EEPROM (maybe a 1702?) for my BIOS, an additional 256 bytes of memory. I added a 4 character hex display (that took some power) and a hex-key input pad.

The F8 had bunches of bidirectional IO (16 bits?), and I added a 1-bit speaker, the hex-pad and the hex display to it. The keypad was a 6x4 cross-bar scanned input with debouncing done in the BIOS. The hex display was 4 seven-segment displays. Each segment drew 15mA (~ 1/2 amp total). I built a 1-amp 6-volt (filament transformer) power supply (huge) with a 1F (yeah, 1 Farad) filter capacitor. At the time, I was also into ham radio and had shelves of parts I bought at hamfests at prices of a dollar-a-pound. Transistors, scavenged from discarded main-frame computer memory boards, were a penny apiece at a local electronics second-hand shop.

My first program (took me days to program it - and no way to save it after power-off) was my first shot at computer animation. By timed toggling of one IO bit, I got one segment of one of the four seven-segment displays to blink! Within weeks, I expanded my repertoire of programs to including blinking the seven segments of one display in a figure-eight pattern.

At ORNL at the time, there was some "advanced" animation work going on. We were trying to build a graphics based control station (a GUI) for a uranium reprocessing plant, as the plant operators would be remote but still need real-time updating of the process. By storing 32 images of a stick-man in various walking poses and blitting the images continuously from the disk to the screen, one of the programmers actually displayed character animation! I was hooked.

I graduated from the F8 to an Intel 8080 with 16K of dynamic ram and a 32K bios. That was still 8-bit IO but the 8080 had a couple 16-bit registers which could be used for some real computing power, and it ran in the multi-MHz range. It was still a home-brew machine, but my output was to a ray-traced O-scope with actual letters displayed on-screen. My animation programs included Lissajous patterns and even a rotating square!

As my salary increased slowly, I was able to afford a commercially made computer! I bought a Radio Shack TRS-80 (Tandy Radio Shack) Color Computer (the C64 was the biggest competitor then), a beautiful machine. It had 32K RAM, 32K ROM (holding the Color Basic interpreter), output to a TV "monitor," ADC (analog-to-digital converter, successive-approximation with resistors) port, a tape-recorder connection, and an external (5-1/4) floppy drive connection. I couldn't afford a floppy just then, but I was actually able to save my programs to cassette tape!

I could draw geometric shapes (filled or "wire-frame,") and text, all in any of a dozen or so colors. I graduated from 2D to 3D animations and managed to display a rotating cube, using sines and cosines and all kinds of math functions. I think I even got it to display at one or two frames-per-second by storing patterns in ram. That was my introduction to graphics eating up memory at a prodigious rate! I needed more RAM.

Another "feature" of the TRS-80 was a warranty-violation sticker over one of the chassis screws. However, it was easily penetrated by a Phillips-head screwdriver and I installed (a proto-board wire-wrapped) 64K shadow RAM circuit. Programmatically, from the lower 32K memory space, I could copy (byte-by-byte) the (PROM) BIOS to RAM, and use some of the RAM above the Basic interpreter code! I even found a way to switch from 32K to 64K, read the shadowed RAM, switch back to 32K mode and now had 32K of extra RAM! All that just for better graphics.

Now I needed to store some of the graphics offline. After adding a floppy drive, I could store up to 128K of data to a one-sided floppy. With a little trickery, one could even cut holes in the other side of a floppy, insert it backwards into the drive and use those "expensive" one-sided floppies as two-sided "flippies" to store an incredible 256K of data! However, that was also my introduction to bleed-through. When you write to one side of thin magnetic media, the possibility exists that the same signal can be written sort-of upside-down and backwards, on the other side of that media. Simply put - "data loss." sigh.

Then came the wonderful years, extending to the present, of 16-bit, 32-bit and 64-bit machines, DirectX and OpenGL, internal hard drives, and ( can you believe it? ) monitors capable of displaying 64K color images at an incredible rate! I could display that walking stick-man of the past so fast the screen would tear.

My introduction to honest-to-goodness animated skinned meshes was through Frank Luna's book, Introduction to 3D Game Programming With DirectX 9.0c - A Shader Approach. I had read about "real-time mesh deformation" and spent hours modifying code and trashing perfectly good shaders, just to see how it was all done. So much of it was hidden beneath D3D9 objects like the skininfo object and ID3DXAnimationController. It seemed like it took forever to figure what "semantics" were about and why you had to convert to an indexed-blended mesh with a particular vertex declaration. However, D3DXLoadMeshHierarchyFromX and Luna's SkinnedMesh class were the best things that had happened to me since I had been bitten by the bug so many years earlier.

I learned how to use Blender (after a fashion) to create a model other than "tiny.x" to really see what's happening under the hood. I've written my own file-loader, hierarchy-builder, skininfo class and animation controller to support my need to understand it all. Nothing I've done is as efficient as the API stuff, but I'm moving on to D3D11 and, without the API built-in functions, that effort will serve me well.

Some time ago (again, the time frame is intentionally left blank,) I graduated from just trying to program the stuff, to learning the theory behind the stuff. As with just about all programming I do, having an understanding of the concept I'm implementing is the key to efficient use of my time.

Note to those responsible for innumerable posts asking for help getting this or that to work, having tried using sine instead of cosine, reversing the order of function calls, and all sorts of hacking attempts, hoping to stumble on something that works - save yourself a lot of time and grief with a simple process - UTCTRTFM.Understand The Concept, Then Read The Friggin' Manual
- Understand the concept of what you're trying to do. Explain it out loud to someone who hasn't a clue what a matrix is, much less the inverse-of-the-transpose of same. Use your native language, not in terms related to Java or C or Python. You may be pleasantly surprised when you do that, as you force yourself to think in non-programming terms about what you're trying to accomplish.

- RTFM ( Read The Friggin' Manual ). How many posts have you seen quoting the docs - "The description of that function states you can't use managed memory for that" - followed (perhaps a day later) by "Thanks! Changed to system memory and it works now." A day of your life wasted.
Sign in to follow this  
  • Advertisement