Skeletal animation with Assimp and glm

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,
scene->mAnimations[i]->mChannels[j]->mRotationKeys[l].mValue.x,
scene->mAnimations[i]->mChannels[j]->mRotationKeys[l].mValue.y,
scene->mAnimations[i]->mChannels[j]->mRotationKeys[l].mValue.z);


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
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 -
animations[currentAnimation].channels[nodeId].scalingKeys[index].time;
double factor = (animationTime - animations[currentAnimation].channels[nodeId].scalingKeys[index].time) / delta;
scl =     glm::mix(animations[currentAnimation].channels[nodeId].scalingKeys[index].values,
animations[currentAnimation].channels[nodeId].scalingKeys[nextIndex].values,
(float)factor);
}
// 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 -
animations[currentAnimation].channels[nodeId].rotationKeys[index].time;
double factor = (animationTime -     animations[currentAnimation].channels[nodeId].rotationKeys[index].time) / delta;
rot =     glm::mix(animations[currentAnimation].channels[nodeId].rotationKeys[index].values,
animations[currentAnimation].channels[nodeId].rotationKeys[nextIndex].values,
(float)factor);
}
// 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 -
animations[currentAnimation].channels[nodeId].locationKeys[index].time;
double factor = (animationTime -     animations[currentAnimation].channels[nodeId].locationKeys[index].time) / delta;
loc =     glm::mix(animations[currentAnimation].channels[nodeId].locationKeys[index].values,
animations[currentAnimation].channels[nodeId].locationKeys[nextIndex].values,
(float)factor);
}

// 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?

• Your quaternion to matrix code might be a bit off, since AssImp's matrices are "flipped". Jul 8, 2016 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.

• 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 Dec 20, 2013 at 15:52
• @Manvis You are correct, I should have double checked. Edited answer. Dec 20, 2013 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?

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(
currentMesh->mBones[j]->mOffsetMatrix);
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