Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

COLLADA: Inverse bind pose in the wrong space?

I'm working on writing my own COLLADA importer. I've gotten pretty far, loading meshes and materials and such. But I've hit a snag on animation, specifically: joint rotations.

The formula I'm using for skinning my meshes is straight-forward:

weighted;
for (i = 0; i < joint_influences; i++)
{
    weighted += 
        joint[joint_index[i]]->parent->local_matrix * 
        joint[joint_index[i]]->local_matrix * 
        skin->inverse_bind_pose[joint_index[i]] * 
        position * 
        skin->weight[j];
}
position = weighted;

And as far as the literature is concerned, this is the correct formula. Now, COLLADA specifies two types of rotations for the joints: local and global. You have to concatenate the rotations together to get the local transformation for the joint.

What the COLLADA documentation does not differentiate between is the joint's local rotation and the joint's global rotation. But in most of the models I've seen, rotations can have an id of either rotate (global) or jointOrient (local).

When I disregard the global rotations and only use the local ones, I get the bind pose for the model. But when I add the global rotations to the joint's local transformation, strange things start to happen.

This is without using global rotations:

Bind pose

And this is with global rotations:

Weird

In both screenshots I'm drawing the skeleton using lines, but in the first it's invisible because the joints are inside the mesh. In the second screenshot the vertices are all over the place!

For comparison, this is what the second screenshot should look like:

Collada viewer

It's hard to see, but you can see that the joints are in the correct position in the second screenshot.

But now the weird thing. If I disregard the inverse bind pose as specified by COLLADA and instead take the inverse of the joint's parent local transform times the joint's local transform, I get the following:

enter image description here

In this screenshot I'm drawing a line from each vertex to the joints that have influence. The fact that I get the bind pose is not so strange, because the formula now becomes:

world_matrix * inverse_world_matrix * position * weight

But it leads me to suspect that COLLADA's inverse bind pose is in the wrong space.

So my question is: in what space does COLLADA specifies its inverse bind pose? And how can I transform the inverse bind pose to the space I need?

like image 446
knight666 Avatar asked Sep 29 '11 10:09

knight666


1 Answers

I started by comparing my values to the ones I read from Assimp (an open source model loader). Stepping through the code I looked at where they built their bind matrices and their inverse bind matrices.

Eventually I ended up in SceneAnimator::GetBoneMatrices, which contains the following:

// Bone matrices transform from mesh coordinates in bind pose to mesh coordinates in skinned pose
// Therefore the formula is offsetMatrix * currentGlobalTransform * inverseCurrentMeshTransform
for( size_t a = 0; a < mesh->mNumBones; ++a)
{
    const aiBone* bone = mesh->mBones[a];
    const aiMatrix4x4& currentGlobalTransform
        = GetGlobalTransform( mBoneNodesByName[ bone->mName.data ]);
    mTransforms[a] = globalInverseMeshTransform * currentGlobalTransform * bone->mOffsetMatrix;
}

globalInverseMeshTransform is always identity, because the mesh doesn't transform anything. currentGlobalTransform is the bind matrix, the joint's parent's local matrices concatenated with the joint's local matrix. And mOffsetMatrix is the inverse bind matrix, which comes directly from the skin.

I checked the values of these matrices to my own (oh yes I compared them in a watch window) and they were exactly the same, off by maybe 0.0001% but that's insignificant. So why does Assimp's version work and mine doesn't even though the formula is the same?

Here's what I got:

Inversed!

When Assimp finally uploads the matrices to the skinning shader, they do the following:

helper->piEffect->SetMatrixTransposeArray( "gBoneMatrix", (D3DXMATRIX*)matrices, 60);

Waaaaait a second. They upload them transposed? It couldn't be that easy. No way.

enter image description here

Yup.

Something else I was doing wrong: I was converting the coordinates the right system (centimeters to meters) before applying the skinning matrices. That results in completely distorted models, because the matrices are designed for the original coordinate system.

FUTURE GOOGLERS

  • Read all the node transforms (rotate, translation, scale, etc.) in the order you receive them.
  • Concatenate them to a joint's local matrix.
  • Take the joint's parent and multiply it with the local matrix.
  • Store that as the bind matrix.
  • Read the skin information.
  • Store the joint's inverse bind pose matrix.
  • Store the joint weights for each vertex.
  • Multiply the bind matrix with the inverse bind pose matrix and transpose it, call it the skinning matrix.
  • Multiply the skinning matrix with the position times the joint weight and add it to the weighted position.
  • Use the weighted position to render.

Done!

like image 124
knight666 Avatar answered Sep 21 '22 07:09

knight666