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

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.