Quick answer: The final bone matrix is globalInverse * nodeGlobalTransform * boneOffsetMatrix. Skip the offset matrix or transpose wrong (Assimp is row-major; GLM is column-major) and the mesh explodes.
An Assimp-loaded character renders as a tangled mess instead of a clean bind pose. The bone transform math is missing a term or mismatched in matrix order.
The Three Matrices
- Bone offset matrix (
aiBone::mOffsetMatrix): mesh-space → bone-space at bind time. Per bone. - Node global transform: accumulated from the node hierarchy down to this bone, including animation.
- Global inverse transform: inverse of the root node’s transform — cancels the scene’s root orientation.
Combine in the Right Order
finalBoneMatrix = globalInverseTransform
* nodeGlobalTransform // hierarchy * animation
* boneOffsetMatrix;
Drop the offset matrix and bones rotate around the wrong origin — the “exploded mesh” look. Drop the global inverse and the whole model is misoriented.
Row-Major vs Column-Major
Assimp stores matrices row-major. GLM (and GLSL) are column-major. aiMatrix4x4 must be transposed when copied into a glm::mat4 — forget that and every transform is wrong.
Bones Without Vertices
Some intermediate nodes are bones with no offset matrix from any aiBone. Default their final matrix to identity-through-hierarchy — don’t leave them uninitialized.
Verifying
The model renders in a clean bind pose. Playing an animation deforms it correctly. Compare against the source file in Blender/the DCC tool to confirm orientation.
“globalInverse * nodeGlobal * offset, all transposed from Assimp’s row-major. Miss a term or the transpose and it explodes.”
Test with a single-bone mesh first — if one bone is right, the hierarchy math is just recursion on top.