Search code examples
animation3dassimp

Assimp animation data gives unexpected values


I'm using Assimp to load in animation data from an fbx file, but when I load the fbx with Blender and compare the keyframe values between Blender and the values Assimp provides, they're not the same.

According to the Assimp documentation

All 3d data is local to the coordinate space of the node’s parent, that means in the same space as the node’s transformation matrix.

This makes it sound like the animation data is in bone space, which is the same space that animation data is stored and shown in Blender's graph editor, however the data doesn't match. For example, in Blender, one channel maps keyframe values starting at -2.12, rising up to 1.96, and then descending back down again over the course of 30 frames.

Blender animation curve

However, Assimp returns the following keyframe values for the same animation channel:

Time:  0  Value: 2.34362
Time:  1  Value: 2.04054
Time:  2  Value: 1.73746
Time:  3  Value: 1.43438
Time:  4  Value: 1.13131
Time:  5  Value: 0.828227
Time:  6  Value: 0.525148
Time:  7  Value: 0.22207
Time:  8  Value: -0.0225141
Time:  9  Value: -0.267098
Time: 10  Value: -0.511682
Time: 11  Value: -0.756265
Time: 12  Value: -1.00085
Time: 13  Value: -1.24543
Time: 14  Value: -1.49002
Time: 15  Value: -1.7346
Time: 16  Value: -1.72213
...
Time: 30  Value: 2.34362

When I investigate further, these values that Assimp returns appear to be mapped to the model space, as can be seen here. Blender model space position

This discovery seemed to bring me one step closer to correcting the animation data as I should only need to transform the animation data from model space to bone space. Fortunately Assimp provides a mOffsetMatrix property on each bone which is documented as

Matrix that transforms from bone space to mesh space in bind pose. This matrix describes the position of the mesh in the local space of this bone when the skeleton was bound.

According to this, mOffsetMatrix seems to be the inverse of what I want, so I should only need to transform the animation data by the inverse of this matrix. On the contrary, transforming by the inverse matrix continues to give me invalid data, however to my surprise transforming the animation data by mOffsetMatrix as is (and not inverted) gives me the correct values:

Time:  0  Value: -2.12155
Time:  1  Value: -1.81847
Time:  2  Value: -1.51539
Time:  3  Value: -1.21231
Time:  4  Value: -0.909236
Time:  5  Value: -0.606157
Time:  6  Value: -0.303079
Time:  7  Value: 0
Time:  8  Value: 0.244584
Time:  9  Value: 0.489167
Time: 10  Value: 0.733751
Time: 11  Value: 0.978335
Time: 12  Value: 1.22292
Time: 13  Value: 1.4675
Time: 14  Value: 1.71209
Time: 15  Value: 1.95667
Time: 16  Value: 1.9442
...
Time: 30  Value: -2.12155

This appeared to be my answer, but of course that'd be too easy. This transformation gives me the correct animation values only for animations applied to root bones. Animation data for child bones are completely incorrect and I cannot for the life of me figure out what space that animation data is in. I've tried all sorts of combinations of combining the matrices from the parent nodes create a matrix to transform the animation data into bone space, but none of the data appears remotely correct. Even when comparing the animation values that Assimp returns against the model-space values as I did with the root bones, the values are not correct, implying the animation data that Assimpt returns is neither in bone space nor model space.

The documentation of Assimp seems incorrect on both counts of what space the animation data is in and what mOffsetMatrix represents unless I'm completely misunderstanding what it's saying. But here is the puzzle: In what space is Assimp's animation data, and how do I transform it to bone-space?


Solution

  • The documentation is half correct. All animation data is relative to the parent bone/node, but the mOffsetMatrix property on aiBone objects holds the transformation from mesh space to bone space (as opposed to bone space to mesh space as the documentation indicates).

    To transform the animation data from the parent bone's space to the local bone's space, the data should be transformed from the parent's bone space to mesh space, then from mesh space to the current bone's space. This can be done by finding the parent bone and creating a compound matrix, i.e. aiMatrix4x4 parentBoneToChildBone(bone.mOffsetMatrix * aiMatrix4x4(parentBone.mOffsetMatrix).Inverse()); then use this matrix to transform the animation data. (Note .Inverse() modifies the original matrix so a copy should be made before inverting.)

    If the bone in question does not have a parent bone, then you can simply transform the animation data by bone.mOffsetMatrix. Since the parent of a root bone should be the armature (which probably is also in the same space as the mesh), and the animation data is relative to the bone's parent (which in this case should be the armature/mesh space), we can use mOffsetMatrix to transform the animation data directly into the bone space.

    const aiMatrix4x4& modelToBone(bone.mOffsetMatrix); // Renamed for clarity...
    aiMatrix4x4 parentToBone;
    const auto parentBoneI = bonesByName.find(bone.mNode->mParent->mName.C_Str());
    if (parentBoneI != bonesByName.cend())
    {
        aiMatrix4x4 parentToModel(parentBoneI->second->mOffsetMatrix);
        parentToModel.Inverse();
        parentToBone = modelToBone * parentToModel;
    }
    else
    {
        parentToBone = modelToBone;
    }
    
    // ...
    
    // Convert position data from parent's bone space to the current bone's space...
    const aiVectorKey& positionKey = channel.mPositionKeys[keyIndex];
    const aiVector3D position(parentToBone * positionKey.mValue);
    
    // Convert rotation data into bone space...
    const aiQuatKey& rotationKey = channel.mRotationKeys[keyIndex];
    const aiQuaternion rotation(aiQuaternion(aiMatrix3x3(parentToBone)) * rotationKey.mValue);