Search code examples
godotgdscript

Godot - Bone doesn't rotate on the correct axis


I'm trying to rotate a bone in Godot by an angle and an axis I have already calculated. I've tested this angle and axis by rotating a Mesh Instance, and it works perfectly. I checked the calculations and they look right (the axis is found by taking the cross product of the two vectors making up the angle). However, when I try to rotate the bone by the same values, it rotates by the correct angle, but the axis is suddenly different (it should be rotating up, but it rotates to the left). Is this a problem with my code, or maybe is the 3d model/skeleton not made properly?

Here is my code:

# the vector is the Mesh Instance, and the rotation is correct
vector.rotate(axis, angle)

# the bone is rotated by the same values, but the axis is wrong
bonePose = skel.get_bone_pose(id)
bonePose = bonePose.rotated(axis, angle)
skel.set_bone_pose(id, bonePose)

Solution

  • You can write a global pose override:

    bonePose = skel.get_bone_global_pose(id)
    bonePose = bonePose.rotated(axis, angle)
    skel.set_bone_global_pose_override(id, bonePose, 1.0)
    

    The third parameter amount is a weight. If it is 0, it leaves the pose as is, if it is 1.0 it replaces it. Values in between are interpolations. There is also a fourth optional parameter persistent which specifies whether or not it should keep the pose override the next time skeleton is updated (it should compute the interpolation with the updated pose).

    If that works for you, great.


    The more fun to answer question for me, is how to do that without set_bone_global_pose_override. Consider this an academic excercise.

    The global pose of a bone is the global pose of the parent times the pose of the bone. Let us express that:

    b.global_pose = parent.global_pose * b.pose
    

    Actually, if there is a custom pose, then it is:

    b.global_pose = parent.global_pose * b.custom_pose * b.pose
    

    And if there is a rest pose, then it is:

    b.global_pose = parent.global_pose * b.rest_pose * b.custom_pose * b.pose
    

    And we want to modify the global pose, but we can only control the custom pose and the pose.

    In fact, there is no reason to modify pose, since applying a transformation to the pose is equivalent to setting a custom pose.

    Now, the question is what custom pose X can we set so it is equivalent to applying a transform T to the global pose:

    T * b.global_pose = parent.global_pose * b.rest_pose * X * b.pose
    

    Solve for X. Reminder: the transform multiplication is not commutative.

    Start by applying the invert of the global pose of the parent by both side (they cancel on the right):

    inverse(parent.global_pose) * T * b.global_pose = b.rest_pose * X * b.pose
    

    And then the rest pose by both sides:

    inverse(b.rest_pose) * inverse(parent.global_pose) * T * b.global_pose = X * b.pose
    

    And then both sides by the inverse of the pose:

    inverse(b.rest_pose) * inverse(parent.global_pose) * T * b.global_pose * inverse(b.pose) = X
    

    And that is how we need to compute the new custom pose. The code for that would be something like this:

    var inverse_rest = skel.get_bone_rest(id).affine_inverse()
    
    var parent = ske.get_bone_parent(id)
    var inverse_parent_global = Transform.IDENTITY
    if parent != -1:
        inverse_parent_global = skel.get_bone_global_pose(parent).affine_inverse()
    
    var global_pose = skel.get_bone_global_pose(id)
    
    var inverse_pose = skel.get_bone_pose(id).affine_inverse()
    
    var t = Transform.IDENTITY.rotated(axis, angle) # the operation you want to do
    
    var x = inverse_rest * inverse_parent_global * t * global_pose * inverse_pose
    
    skel.set_bone_custom_pose(id, x)
    

    Or if you want to set the pose instead of the custom pose (assuming the custom remains the identity trasnform):

    var inverse_rest = skel.get_bone_rest(id).affine_inverse()
    
    var parent = ske.get_bone_parent(id)
    var inverse_parent_global = Transform.IDENTITY
    if parent != -1:
        inverse_parent_global = skel.get_bone_global_pose(parent).affine_inverse()
    
    var global_pose = skel.get_bone_global_pose(id)
    
    var t = Transform.IDENTITY.rotated(axis, angle) # the operation you want to do
    
    var x = inverse_rest * inverse_parent_global * t * global_pose
    
    skel.set_bone_pose(id, x)