I finally have multiple-bone skinning working! I think I can summarize the last few days as "Wow, that was a pain". On the surface, a hierarchical animation system sounds easy: "Just go down the tree and accumulate matrices as you go", but as is usually the case, the devil is indeed in the details.
I would love to make this blog post a full tutorial on skinning, but unfortunately I still don't completely understand it myself. I slapped in some foreign code for nasty jobs so that I could just focus on getting the bones to animate and stay attached to the skin. Also because the code was written by accumulating many tests, it could be improved quite a bit.
Here are some of what I will affectionately call "the devil's details". Hopefully these will help someone who is learning to write a skinning system. Keep in mind that some of these might only apply to the Milkshape3D model format.
Not all vertices will be attached to a bone
While this might not be the case in most animations, you should make sure that your system won't explode when this situation is encountered. One of the animations I tested my code on was a brick hurtling towards a brick wall, bouncing off the wall and falling onto the ground. The wall was part of the animation, but was not attached to a bone.
Keyframes are defined per-bone, not globally
Rather than having a list of keyframes which each have a list of the bones that they affect, each bone has a list of keyframes.
Calculating keyframe indices; not as easy as you might think
The bones are animated by interpolating between two keyframes, so you need to figure out which two keyframes you are in between. You also need to gracefully handle the situation where the current animation time is before the first keyframe for the specified joint, or past the last keyframe. I am currently finding the second keyframe first by looking through the list of keyframes going forward in time and checking for the first keyframe which comes after the current animation time. I then set the first keyframe to be the one previous to the second which I just found. Both keyframes are initialized to the last keyframe, which is the value they take on if no other keyframe is found in the loop. If the second keyframe is found to be index 0, the first keyframe will become negative, in which case the first keyframe is set to be equal to the second. There are two cases where both keyframes will have the same index; if the time is before the first keyframe, or if the time is after the last keyframe. If both keyframes have the same index, interpolation is not required.
Keyframe positions and orientations are relative to the bone's original position and orientation
This was one of the big ones for me. I did not figure this out until well into development, so I had to basically rewrite the skinning system. This means that in order to transform a point to where it should be for the given keyframe, the point must be in the same coordinate space as the bone.
Convert orientations from RH to LH, not just positions!
This was the big one. Because of the previous "detail", things can become really out of sorts if you get rotation wrong. If a parent joint is in a certain orientation, all movement is going to depend on that orientation being correct. If the orientation is wrong, the vertices attached to the joint will be in the wrong spot. This problem gets worse and worse the further down the hierarchy you go. As far as converting the orientations goes, I'm not entirely sure how it works. What ended up working for me was to invert z-axis positions and z-axis rotations, but I have read elsewhere that only the x and y axis rotations should be inverted when z-axis positions are inverted. I'll have to do more research into this, but it is working fine for now!
Bone matrix, what is it?
This had me confused for a while. Basically, the bone matrix needs to transform a vertex in model space (which is attached to the bone) into the coordinate space of the bone (without any animations), apply the keyframe transformation, then transform back into model space.