Skeletal animation with ASSIMP, COLLADA, and glm

| | August 4, 2015

I have been trying to learn how to implement skeletal animation (skinning) from COLLADA files using ASSIMP and glm. But after a week of being stuck reading different tutorials and posts I have found here and there I still can not get even the simplest animation to work correctly. My results are just bones that look like they are incorrectly translated and rotates around wrong axes.

Left image is my result at key frame 2. Right image is from blender at key frame 2
This mesh have three bones starting at origo and going straight up through a tube, they then swing from side to side when the animation is played. LEFT: My result at key frame 2. RIGHT: Blenders result at key frame 2.

So now I have two questions related to the animation matrices and hopefully someone might help me understand what I am doing wrong.

First, when I read the bone data ( or joints but I called it bone in my code so I will refer to it as bone here also ) I get the bones matrix from mOffsetMatrix. I am not 100% sure if this matrix contains a bones transform relative to its parent bone or if it contains the final position. I have tried to use it as both relative and absolute transform but my final result is still wrong so I dont know which is right.

If the mOffsetMatrix contains the relative transform of a bone then I have to calculate the absolute transform by multiplying it with the transform of its parent bone? I tried doing this with the following code.

// struct for storing bones (joints)
struct Bone
{
    std::string name;
    glm::mat4 relativeTransform;
    glm::mat4 absoluteTransform;
    int parent; // id to parent bone
};

std::vector<Bone> skeleton; // contains all the bones

for (int i = 0; i < skeleton.size(); ++i)
{
  // if bone have no parent, relative transform is absolute transform
  skeleton[i].absoluteTransform = skeleton[i].relativeTransform;

  // if bone have parent, add parent absolute transform
  if (skeleton[i].parent != -1)
  {
    skeleton[i].absoluteTransform = skeleton[skeleton[i].parent].absoluteTransform * skeleton[i].relativeTransform;
  }
}

Skeleton is a vector containing all of the bones where parent bones always appear before its children so as long as I start calculating from the beginning of the list it should be correct. Also i should mention that i have a function that identify bone parents so the parent id is correct.

Then in regards of the animation step. For now I try to only animate individual key frames so I do not currently do any interpolation, this is so that incorrect interpolation does not affect my result. But I dont know if i calculate the current animation transforms correct. In the end it is the bonesAnimation.current matrices that I calculate for each bone/frame and send to my shaders.

// struct containing keyframe data for each bone
struct BoneAnimationChannel
{
    std::string name;
    std::vector<float> positionTime;
    std::vector<float> rotationTime;
    std::vector<float> scalingTime;
    std::vector<glm::vec3> positionKeys;
    std::vector<glm::quat> rotationKeys;
    std::vector<glm::vec3> scalingKeys;
    glm::mat4 current; // current transform to be used
};
std::vector<BoneAnimationChannel> bonesAnimation; // contain animation data for all bones

I store the animation data for each bone in a struct and have one instance of the struct for each bone in the vector bonesAnimation. Then i calculate the current transform for each bone at a certain time using the following code.

// update animation
std::vector<glm::mat4> unMultiplied;
for (int i = 0; i < skeleton.size(); ++i)
{
  // current bone
  auto bone = boneAnimation[i];

  int id = 1; // id to keyframe

  glm::mat4 currentPose = glm::toMat4(bone->rotationKeys[id]);
  currentPose[3][0] = bone->positionKeys[id].x;
  currentPose[3][3] = bone->positionKeys[id].y;
  currentPose[3][2] = bone->positionKeys[id].z;
  currentPose = glm::scale(currentPose, bone->scalingKeys[id]);

  // add to list of animation transforms
  unMultiplied.push_back(currentPose);

  // bones parent id
  int parentId = skeleton[i].parent;

  // transform for current bone
  glm::mat4 result = unMultiplied[i]; // if no parent bone

  // if bone have parent, add parents transform
  if (parentId != -1)
  {
    result = unMultiplied[parentId] * unMultiplied[i];
  }

  boneAnimation[i].current = result;
}

It is possible that my problem lays elsewhere but I think that these two places are the most likely sources for my problems so I start by asking if these calculations look correct.
I have tried every different combination of matrices I can think of, I have even tried to invert them or transpose them to see if that is the problem but still I am completely stuck.

Maybe i even need some more transform other than the mOffsetMatrix for each bone and the position vector, scaling vector and rotation quaternion for the animation channels.

I hope this make some kinda of sense and i really appreciate any help i can get.

Thanks in advance :)

EDIT: This is the whole file where i load mesh and animations https://db.tt/EG965vuz, and in the end of this file is where i calculate current animation pose https://db.tt/cBbYZck2

One Response to “Skeletal animation with ASSIMP, COLLADA, and glm”

  1. If you were using assimp’s implementation (AssimpViewer source) as reference, then you probably too missed the fact that Assimp’s and glm use reverse matrix multiplication order. To clarify, we are talking about matrix-matrix multiplication, and the order is reverse relative to each other

    And we know that matrix multiplication is not commutative.

    So the line

    result = unMultiplied[parentId] * unMultiplied[i];
    

    becomes

    result = unMultiplied[i] * unMultiplied[parentId];
    

    And the final transform for a bone is given by

    bone.OffsetMatrix * result * GlobalInverseMeshTransform;
    

    Where GlobalInverseMeshTransform is the inverse of the root node’s transform.(Might be identity)


    Here you are reading outside of bounds. The second row should also be 0 – 3.
    Consider making it a simple function.(assimp mat4 to glm mat4).

    glm::mat4 rootTransform(
        m[0][1], m[0][2], m[0][3], m[0][4],
        m[1][1], m[1][2], m[1][3], m[1][4],
        m[2][1], m[2][2], m[2][3], m[2][4],
        m[3][1], m[3][2], m[3][3], m[3][4]);
    

    You are forgetting to invert the root transform

    anim.rootTransform = rootTransform; // should be glm::inverse( rootTransform );
    

    Are you sure this is getting called? Its not a good idea to compare floats like this.

    if (vertices[vertID].boneWeights[k] == 0.0f)
    {
        vertices[vertID].boneIndices[k] = i;
        vertices[vertID].boneWeights[k] = mesh->mBones[i]->mWeights[j].mWeight;
        break;
    }
    

    I am not sure whether the construction of the actual frame’s matrix is correct.
    This works:

    mat = glm::mat4_cast( currentrotation );
    mat[0][0] *= currentscale.x; mat[1][0] *= currentscale.x; mat[2][0] *= currentscale.x;
    mat[0][1] *= currentscale.y; mat[1][1] *= currentscale.y; mat[2][1] *= currentscale.y;
    mat[0][2] *= currentscale.z; mat[1][2] *= currentscale.z; mat[2][2] *= currentscale.z;
    mat[0][3] = currentposition.x; mat[1][3] = currentposition.y; mat[2][3] = currentposition.z;
    

    Again, you are missing the GlobalInverseMeshTransform

    eMeshes["test"]->animations[0].bones[i].current = eMeshes["test"]->skeleton[i].relativeTransform * result; // ... result * <youranim>.rootTransform;
    

    Apart from these errors, your code looks like a mess. You should make the animation routine as part of the model you are animating.
    Maybe with a function like this EvaluateAnimation( double time ); where time is the animation time you want to load.

    Also a map lookup at each access to an object will be a great performance hit later, even with unordered_map, so if you have long-term plans, I suggest you look for a better solution.

Leave a Reply