I'm trying to implement skeletal animation using assimp and glm. Everything seems to work, except for rotations.

This is the code I use when packing assimp data into my own engine's format. I THINK it should be correct, but since I've already run into problems with matrices (assimp uses row major, glm - column major), I'd rather post this, too:

for(uint32_t l = 0; l < tempChannel.numRotationKeys; ++l){
    quatKey tempKey;
    tempKey.values =  glm::quat(scene->mAnimations[i]->mChannels[j]->mRotationKeys[l].mValue.w,

And this is the code that performs the actual transformations (quite a bit of it is based on http://ogldev.atspace.co.uk/www/tutorial38/tutorial38.html), but modifications (e.g. storing a list, where each child element is placed after a parent and keeps a parent's id, as was suggested here: http://blog.tojicode.com/2011/10/building-game-part-3-skinning-animation.html) have been made. Scaling and location transforms seem to be working fine, but rotations don't:

for(unsigned int i = 0; i < skeleton.size(); ++i){
    finalTransforms[i] = calcFinalTransform(time, i);

glm::mat4 SkeletalMeshObject::calcFinalTransform(double animationTime, unsigned int currentNode){
    // Find the node where the keyframes for the current bone are stored
    // Return -1, if not found
    int nodeId = findNodeAnimation(currentAnimation, skeleton[currentNode].name);

    glm::mat4 nodeTransform(skeleton[currentNode].offset);
    // If this node isn't animated, pass the default offset to it
    if(nodeId >= 0){
        unsigned int index;
        unsigned int nextIndex;
        // Interpolate scaling
        glm::vec3 scl;
        if(animations[currentAnimation].channels[nodeId].scalingKeys.size() == 1){
            scl = animations[currentAnimation].channels[nodeId].scalingKeys[0].values;
        } else {
            index = findKey(animationTime, KeyType::Scaling, nodeId);
            nextIndex = index + 1;
            double delta = animations[currentAnimation].channels[nodeId].scalingKeys[nextIndex].time -
            double factor = (animationTime - animations[currentAnimation].channels[nodeId].scalingKeys[index].time) / delta;
            scl =     glm::mix(animations[currentAnimation].channels[nodeId].scalingKeys[index].values,
        // Interpolate rotation
        glm::quat rot;
        if(animations[currentAnimation].channels[nodeId].rotationKeys.size() == 1){
            rot = animations[currentAnimation].channels[nodeId].rotationKeys[0].values;
        } else {
            index = findKey(animationTime, KeyType::Rotation, nodeId);
            nextIndex = index + 1;
            double delta =     animations[currentAnimation].channels[nodeId].rotationKeys[nextIndex].time -
            double factor = (animationTime -     animations[currentAnimation].channels[nodeId].rotationKeys[index].time) / delta;
            rot =     glm::mix(animations[currentAnimation].channels[nodeId].rotationKeys[index].values,
        // Interpolate location
        glm::vec3 loc;
        if(animations[currentAnimation].channels[nodeId].locationKeys.size() == 1){
            loc = animations[currentAnimation].channels[nodeId].locationKeys[0].values;
        } else {
            index = findKey(animationTime, KeyType::Location, nodeId);
            nextIndex = index + 1;
            double delta =     animations[currentAnimation].channels[nodeId].locationKeys[nextIndex].time -
            double factor = (animationTime -     animations[currentAnimation].channels[nodeId].locationKeys[index].time) / delta;
            loc =     glm::mix(animations[currentAnimation].channels[nodeId].locationKeys[index].values,

        // Get final transform
        nodeTransform = glm::translate(loc) * glm::toMat4(rot) * glm::scale(scl);

    // Store the node's transform 
    locRotScaleTransforms[currentNode] = nodeTransform;

    // Get the id of the parent
    int parentId = skeleton[currentNode].parent;
    // Hierarchial transform
    glm::mat4 hierarchyTransform = nodeTransform;
    while(parentId >= 0){
        hierarchyTransform = locRotScaleTransforms[parentId] * hierarchyTransform;
        parentId = skeleton[parentId].parent;

    // Apply inverse bind position matrix
    hierarchyTransform *= skeleton[currentNode].ibp;

    return hierarchyTransform;

Here's how the animation looks in blender: And here's in my engine. Couldn't capture a gif from my window, so I'll describe it. First, it rotates along multiple axes (bones in the original animation were constrained to z axis), and as you can also see, it distorts. The frame seen below is almost at an end of animation, when distortions are most visible:

What can I do to make it look right?

  • \$\begingroup\$ Your quaternion to matrix code might be a bit off, since AssImp's matrices are "flipped". \$\endgroup\$ – Stefnotch Jul 8 '16 at 14:24

First thing I see is that you shouldn't read the quaternion in reverse order.

Also you shouldn't use glm::mix, use glm::slerp instead.

And here is how I construct the bone transform:

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;

Also I would check all my matrix multiplication, it should be in reverse order to how would you do it with Assimp matrices.

I base all this on personal experience from implementing an animated mesh with assimp and glm myself.(which now works) But it might not be the correct way of doing things.

| improve this answer | |
  • \$\begingroup\$ I'm almost sure I'm reading the quaternion in the right order. Here's how glm defines the constructor: glm.g-truc.net/0.9.4/api/a00076_source.html#l00068 \$\endgroup\$ – Manvis Dec 20 '13 at 15:52
  • \$\begingroup\$ @Manvis You are correct, I should have double checked. Edited answer. \$\endgroup\$ – akaltar Dec 20 '13 at 15:58

I think the missing piece is parents starting transform. Meaning the bone was not at the origin to start with, so it should not be animated as it is. I saw you are using parents "animation" transform in the end withing while, but where is the starting point of the bone?

I am saving the transforms while loading the mesh like this:

void ModelAsset::createMeshes(const aiScene *scene, aiNode *aiNode, glm::mat4 parentTransform) {
    parentTransform = parentTransform * GLMConverter::AssimpToGLM(aiNode->mTransformation);

    for (unsigned int i = 0; i < aiNode->mNumMeshes; ++i) {
            aiMesh *currentMesh;
            currentMesh = scene->mMeshes[aiNode->mMeshes[i]];
            for (unsigned int j = 0; j < currentMesh->mNumBones; ++j) {
                meshOffsetmap[currentMesh->mBones[j]->mName.C_Str()] = GLMConverter::AssimpToGLM(
                std::string boneName =  currentMesh->mBones[j]->mName.C_Str();
                boneName += "_parent";
                meshOffsetmap[boneName] = parentTransform;

//some other code

    for (unsigned int i = 0; i < aiNode->mNumChildren; ++i) {
        createMeshes(scene, aiNode->mChildren[i], parentTransform);

and when I am calculating the animation:

    transforms[boneNode->boneID] =
            globalInverseTransform * meshOffsetmap.at(boneNode->name + "_parent") * nodeTransform * meshOffsetmap.at(boneNode->name)

If you would like to check out t Full source code can be found at https://github.com/enginmanap/limonEngine/blob/master/src/Assets/ModelAsset.cpp

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.