Better understanding of computing skinning matrices
See original GitHub issueFirst of all, if this is not related at all to glTF & skinning and is more related to blender then i apologize and this issue can be closed.
I am currently trying to understand how the ‘final joint matrix’ (i.e. the matrix that each influenced vertex is multiplied by) is computed
My current knowledge of how it’s calculated:
jointMatrix = globalMatrix * inverseBindMatrix
where globalMatrix
is the joint’s local matrix multiplied by it’s parent global matrix
and inverseBindMatrix = invert(globalMatrix)
Now consider a simple setup with just 2 joints: Joint 1: Local matrix = Identity matrix (as i want this joint to have a location of (0, 0, 0), no rotation and a scale of (1, 1, 1) Joint 2: Local matrix = Translation: (100, 50, 50), no rotation, scale(1, 1, 1) of course stored in a matrix but i just wrote it out this way for clarity.
Joint 1 has no parent as it’s the root joint, Joint 2 has Joint 1 as it’s parent
Now as i understand when a joint in rotated it uses it’s local translation as the origin of rotation instead of (0, 0, 0) and here’s where my confusion comes from because the math doesn’t seem to add up
Say Joint 2 was rotated by 45 degrees in the x axis, what i’d expect the final joint matrix to do when influenced vertices are multiplied by it is rotate each vertex 45 degrees in the x axis around the bone’s local translation (i.e. 100, 50, 50 in this case) that seems to be happening when i rotate the bone in blender but the resulting matrix when i do these computations doesn’t seem to take that into account and instead assumes rotation around (0, 0, 0)
Here’s sample code i wrote that would compute the final joint matrix for the above example:
import lombok.Getter;
import lombok.Setter;
import org.joml.Matrix4f;
public class JointTest {
public static void main(String[] args) {
Joint joint1 = new Joint();
// identity matrix as root should be at position (0, 0, 0), have no rotation or scale
joint1.setLocalMatrix(
new Matrix4f()
);
Joint joint2 = new Joint();
// joint 2 should have a position of (100, 50, 50) which should be it's rotation origin
joint2.setLocalMatrix(
new Matrix4f().translate(100, 50, 50)
);
joint2.setParent(joint1);
joint2.computeAndSetIBM();
Matrix4f animationMatrix = joint2.getLocalMatrix()
.rotateX((float) Math.PI / 4f, new Matrix4f());
joint2.setLocalMatrix(animationMatrix);
Matrix4f globalMatrix = joint2.computeGlobalMatrix();
Matrix4f inverseBindMatrix = joint2.getInverseBindMatrix();
Matrix4f finalJointMatrix = globalMatrix.mul(inverseBindMatrix, new Matrix4f());
System.out.println(finalJointMatrix);
}
@Getter
@Setter
private static class Joint {
// Matrix4f is from JOML (column-major)
private Matrix4f localMatrix;
private Matrix4f inverseBindMatrix;
private Joint parent;
public void computeAndSetIBM() {
Matrix4f global = computeGlobalMatrix();
inverseBindMatrix = global.invert(new Matrix4f());
}
public Matrix4f computeGlobalMatrix() {
Matrix4f globalMatrix = new Matrix4f(localMatrix);
if (parent != null) {
return globalMatrix.mul(parent.computeGlobalMatrix(), new Matrix4f());
}
return globalMatrix;
}
}
}
The result is:
1.000E+0 0.000E+0 0.000E+0 0.000E+0
0.000E+0 7.071E-1 -7.071E-1 5.000E+1
0.000E+0 7.071E-1 7.071E-1 -2.071E+1
0.000E+0 0.000E+0 0.000E+0 1.000E+0
Which if i understand correctly is not really a 45 degree rotation in the x axis around the origin (100, 50, 50), at least it does not seem correct but please do let me know if im wrong.
Issue Analytics
- State:
- Created a year ago
- Comments:8 (4 by maintainers)
Top GitHub Comments
The
globalTransformOfNode
refers to the node that the skin is attached to. This is also something that should always be the identity matrix in glTF 2.0.Trying to figure out solutions by “reverse engineering” the existing code might not be the best approach, because some aspects of the implementation may be confusing, and there are some ‘gotchas’ in the part that renders the glTF. For example, the “default material” in the current state does not include skinning functionality, so by default, you won’t see the skinned result if there is a default material. For PBR materials, it should work, but the PBR implementation itself is far from perfect.
But of course, you may give it a try and see whether you can gain helpful insights from it.
I agree with you that it’s not really the best approach, i’ll just take a quick look at it to see if i find anything useful from it. Thank you for all the replies, they actually helped quite a lot. I’ll close this issue as i have no other questions currently.