der_ton@Posted: Fri Feb 13, 2004 10:21 pm :
In the past months, I got several emails with technical questions about the modelviewer, mostly regarding the md5anim files and how to use the information they contain. So I guess it´s time to have that covered in a little post here.

This is interesting if you want to make a program that shows md5 models, or if you want to understand the md5anim format a little better. This is not so interesting if you are an artist and want to make md5 models or anims.

I assume that you know how to deal with the information in the md5mesh file, and that you´re able to see the character in it´s un-animated "bind pose", as it is stored in the md5mesh.

The md5anim contains only information on the skeleton (and, less important for our context here, the boundingbox).
How the animation data is stored is not a big secret I think: every bone has a (x,y,z) translation and (yaw, pitch, roll) rotation that´s stored for each frame. This is the data we have, and what we want to get is the 4x4 matrix of the bone in absolute coordinates, much like the info that is stored with the bones in the md5mesh. This way, you don´t even have to change any code that you´ve already written to get the "bind-pose" model on the screen. You just have to use the bone transformation matríx extracted from the md5anim for a given moment.

The information in the md5anim is stored relative to the parent bone. So the algorithm that evaluates the data and calculates the matrix for a bone has to start at the bones with no parents, and then go through the bones, processing only those whose parent bone already was processed. This is actually not a problem at all, since the order of the bones as stored in the md5mesh file is a valid order in that respect. The order of the channels in the md5anim is not (at least not always).

To extract the right values for a given moment is not a big problem I hope. It can be puzzling at first, to see a channel contain less keyframes than it should, but that simply means that the channel´s value doesn´t change at the beginning or the end (or both). To see what´s going on and what the range means, just study several different md5anims, and you should be able to figure out this stuff. It´s easier to understand it with your own mind than having me explain it.


Then when you´re at the point where you extracted the right data, you have x, y, z, pitch, yaw, roll for each bone. Now here´s how this is used to get the absolute transformation matrix for each bone:

Note that this algorithm requires that you go through the bones in a
certain order, because the matrix of a bone´s parent is required. The
bones in the md5mesh has the right order, the md5anim´s channels do not necessarily have that.

Start with an empty "identity" matrix if the bone has no parent,
or start with the already calculated bone´s parent´s transformation matrix

1. translate by the x,y,z values from the animation channels
2. apply "yaw" rotation (which is rotation around the Z axis)
3. apply "pitch" rotation (around the Y axis)
4. apply "roll" rotation (around the X axis)
the result is "this" bone´s matrix, which will have to be used when
calculating its childrens´ matrices, and which will ultimately be used for skinning (calculating the final vertex coordinates for the mesh).

The transformation column represents the transformation in the same way as the "bindpos" numbers do in the md5mesh file, and the 3x3 rotation submatrix of the matrix represents the rotation in the same way as the "bindmat" numbers do in the md5mesh file.


For a little advanced viewer, interpolation has to be used, which would best be done with quaternions, which I won´t explain here. You can get good info here instead:
http://www.j3d.org/matrix_faq/matrfaq_latest.html

A viewer with sourcecode, made by Sébastien Kuntz:
http://www.nowan.net/cb/3d



pinkie@Posted: Mon Jun 28, 2004 11:32 am :
I thought this is probably a better thread to talk about my anim issues, anyway I am still at a loss, i am sure it is something small i just cannot see myself. I have now changed my code around a bit to look like this:

Code:
    for (int i = 0; i < skeleton.length; i++)
    {
      Bone bone = skeleton[i];

      // new matrix is identity if has no parents or parents bind matrix
      bone.bindMatrix = (bone.parentId < 0) ?
        Matrix4f.createIdentity() : new Matrix4f(skeleton[bone.parentId].bindMatrix);

      // 1. translate by the x,y,z values from the animation channels
      bone.bindMatrix.translate(
        bone.attribute[Md5AnimAttribute.X],
        bone.attribute[Md5AnimAttribute.Y],
        bone.attribute[Md5AnimAttribute.Z]);
     
      //  2. apply "yaw" rotation (which is rotation around the Z axis)
      bone.bindMatrix.rotateAroundZ(bone.attribute[Md5AnimAttribute.YAW]);
      //  3. apply "pitch" rotation (around the Y axis)
      bone.bindMatrix.rotateAroundY(bone.attribute[Md5AnimAttribute.PITCH]);
      //  4. apply "roll" rotation (around the X axis)
      bone.bindMatrix.rotateAroundX(bone.attribute[Md5AnimAttribute.ROLL]);
    }


This looks very much like the pseudo code you give above, so I am very confused as to what I am doing wrong.

I know the channels are set up correctly and if the code above is correct, process of elimination leaves me to think it is the rotation code - so here is a snippet of "rotateAroundX":



Code:
  public void rotateAroundX(float angle)
  {
    //   Use the 4x4 matrix:
    //
    //      |  1  0       0       0 |
    //   M = |  0  cos(A) -sin(A)  0 |
    //      |  0  sin(A)  cos(A)  0 |
    //      |  0  0       0       1 |
      angle = (float)Math.toRadians(angle);

    Matrix4f mat = new Matrix4f();
    mat.data[0] = 1.0f;
    mat.data[1] = 0.0f;
    mat.data[2] = 0.0f;
    mat.data[3] = 0.0f;
    mat.data[4] = 0.0f;
    mat.data[5] = (float)Math.cos(angle);
    mat.data[6] = (float)Math.sin(angle);
    mat.data[7] = 0.0f;
    mat.data[8] = 0.0f;
    mat.data[9] = (float) -Math.sin(angle);
    mat.data[10] = (float)Math.cos(angle);
    mat.data[11] = 0.0f;
    mat.data[12] = 0.0f;
    mat.data[13] = 0.0f;
    mat.data[14] = 0.0f;
    mat.data[15] = 1.0f;
    mat.mul(this);
    this.set(mat);
  }


The two main things im not 100% about here are the +ve's and -ve's for the sin values (left handed or right handed notation?) and the multiplication at the end, any ideas?

Simon.



der_ton@Posted: Mon Jun 28, 2004 12:27 pm :
Angles in the md5anim are in degrees, in your code they are passed to sin() and cos(), so they have to be in radians. Do you convert somewhere inbetween?



pinkie@Posted: Mon Jun 28, 2004 9:38 pm :
Thanks again for the prompt reply der_ton.

The angles are converted to radians in the rotation methods, i.e.:
"angle = (float)Math.toRadians(angle);"

It's a bit ugly as it gets done every time, but plenty of time for optimising once it's going :).

Simon.



pinkie@Posted: Mon Jun 28, 2004 10:58 pm :
Actually, what co-ord system do you use? Do you use ID's version where up is -Z, or do you use the "normal" version where Y is up (right handed)? This would probably influence the order of rotation I guess.

Simon.



der_ton@Posted: Tue Jun 29, 2004 8:13 am :
pinkie wrote:
Actually, what co-ord system do you use? Do you use ID's version where up is -Z, or do you use the "normal" version where Y is up (right handed)? This would probably influence the order of rotation I guess.

Yes I think it would. I'm using a righthanded coord system, like OpenGL. In fact, I let OpenGL do the matrix rotations and multiplications, much like in Sebastien Kuntz's viewer. This might be slower (I haven't done any speed comparisons though), but it's much less confusing, and is always consistent with OpenGL's matrix implementation, no matter what they might change about it in the future.



pinkie@Posted: Wed Jun 30, 2004 12:33 am :
Hmm, after saying "it is not a swizzling issue" I have realised that is most likely what it is!

I have now made a method in my matrix4f class named rotate that takes a angle and a arbituary vector to rotate around (e.g. like glRotatef(trans[YAW] , 0,0,1 )) , but I am still getting the same result : /.

I have a feeling that in your GL code you use the right handed co ord system but set the up vector to -Z (0,0,-1), this is what ID uses (John Carmack if your listening WHY!!!) : ). For our game we use the "standard" up vector of (0,1,0), so y is up.

As for speed, the matrix calculations will be much faster in openGL as it uses hardware to do the math, but as there are a shite load of bones all the pushing an popping may slow it down - basically I'm not sure what would be quicker eitheir, any one out there know? Could likely mix the two up and get best of both worlds, Hmmm....

As the rotations are now around an arbitary vector, I think it is most likely just my translations that need swizzling. I will keep hacking and post code when i get it to go.

Anyway, hope all this babbling may be useful for other people, don't mean to post whore the forum's!

Simon.



bozo@Posted: Wed Jun 30, 2004 4:42 pm :
my tip is that your combination of the parentmatrix, translation and rotations are wrong, maybe the sequence or the kind of matrix mul (mul the new matrix from the left or right side to the existing matrix)

ie. i do the following (but maybe does not apply to your setting/coordsys) in pseudocode:
Code:
QuaternionFromAngles(q1, keyframe1_angles);
QuaternionFromAngles(q2, keyframe2_angles);
QuaternionSlerp(q1, q2, keyframe_delta, orient);
QuaternionToMatrix(orient, rotmat);
Interpolate(keyframe1_pos, keyframe2_pos, keyframe_delta, trans);
TranslationMatrix(trans, tmat);
Matrix_Multiply(rotmat, tmat, transmat);
if no parent_bone
   Matrix_Copy(transmat, bone->transmat);
else
   Matrix_Multiply(transmat, parent_bone->transmat, bone->transmat);



(also i had some problems when i try to interpolate between two frames with the rotationangles, sometimes the euler's goes wrong, so i changed to quaternions)



pinkie@Posted: Mon Jul 05, 2004 8:23 am :
Thanks bozo, I think I will stick with standard matrices for now, quaternians seem like another level of complexity I don't need right now - will have a look into applying them once my code is a bit more "tuned".

Anyway, I have finally got animations going, and it was indeed due to swizzling - of some sort! (http://www.doom3world.org/phpbb2/viewtopic.php?p=25266#25266) for more details on co-ord system/mesh probs.

And as for the code I promised:

Code:
    yaw = new Vector3f(0.0f, 0.0f, 1.0f);
    pitch = new Vector3f(0.0f, 1.0f, 0.0f);
    roll = new Vector3f(1.0f, 0.0f, 0.0f);

    for (int i = 0; i < skeleton.length; i++)
    {
        Bone bone = skeleton[i];
       
        // new matrix is identity if has no parents or parents bind
        bone.bindMatrix = (bone.parentId < 0) ?
               Matrix4f.createIdentity() : new Matrix4f(skeleton[bone.parentId].bindMatrix);
       
        // 1. translate by the x,y,z values from the animation channels
        bone.bindMatrix.translate(
               bone.attribute[Md5AnimAttribute.X],
               bone.attribute[Md5AnimAttribute.Y],
               bone.attribute[Md5AnimAttribute.Z]);
        // 2. apply "yaw" rotation (which is rotation around the Z axis)
        bone.bindMatrix.rotate(bone.attribute[Md5AnimAttribute.YAW], yaw);
        // 3. apply "pitch" rotation (around the Y axis)
        bone.bindMatrix.rotate(bone.attribute[Md5AnimAttribute.PITCH], pitch);
        // 4. apply "roll" rotation (around the X axis)
        bone.bindMatrix.rotate(bone.attribute[Md5AnimAttribute.ROLL], roll);
    }


Thats the guts of the animation code (java based), obviously heavily based on the pseudo from der_ton (thanks!!!). I would post the timing code for setting attribs, but it is only really specfic to what I am trying to do, if you have any questions feel free to ask.

And so no-one falls in the same trap I did, my models looked fine loading straight from mesh HOWEVER they were on their side, but I assumed it was because of a funky co-ord system, I also assumed the animation file would be in same system so I figured it would be fine, and I could deal with orientation later.

I WAS WRONG!! :shock: they looked gammy when animated and I couldn't figue out why, many many useless hours later, my friend and I came across these lines in Sébastien Kuntz code which I had missed earlier:

Quote:
v.x += (b.rot[0] * w.x + b.rot[3] * w.y + b.rot[6] * w.z + b.pos[0]) * w.t;
v.z += (b.rot[1] * w.x + b.rot[4] * w.y + b.rot[7] * w.z + b.pos[1]) * w.t;
v.y += (b.rot[2] * w.x + b.rot[5] * w.y + b.rot[8] * w.z + b.pos[2]) * w.t;


We tried swapping the z and the y and walla! it worked! who knows why but it worked! normally y becomes z and z becomes -ve y, but hey if it works it works :).

Thanks for your time and patience der_ton, and your code to look at Sébastien Kuntz (whoever you may be:) ). I expect any code or helpful tips for anything doom 3 related to be used for research only - as I have done for learning about skeletal animation.

Well, of to tune code...



neakor@Posted: Sat Mar 01, 2008 11:08 pm :
sry to bring this up. but im having problems on animation.

im writing the md5importer for java monkey engine. i loaded the mesh and texture successfully but i cant seem to get the animation right.

this is how i read the flags

Code:
      flag = (int)this.reader.nval;
      for(int i = 0; i < 6; i++)
      {
            this.frameflags.set(joint * 6 + i, (flag & (1 << i)) != 0);
      }


so heres what i did for reading in the data.

Code:
      float[] values = new float[6];
      for(int i = 0; i < this.parentHierarchy.length; i++)
      {
         for(int j = 0; j < values.length; j++)
         {
            if(this.frameflags.get(i * 6 + j))
            {
               while(this.reader.nextToken() != StreamTokenizer.TT_NUMBER);
               values[j] = (float)this.reader.nval;
            }
            else
            {
               values[j] = this.baseframe.getTransformValue(i, j);
            }
         }
         for(int t = 0; t < values.length; t++)
         {
            this.frames[index].setTransform(i, t, values[t]);
         }
      }
      this.frames[index].processTransform();


im not sure if im doing the right thing here but hopefully i am.

and the real question is how do i translate the translation and orientation i read in into bone's local transforms.