Dealing with bones in Godot...

panicqpanicq Posts: 67Member
edited June 30 in Programming

Hey,

I'm asking for help because I can't find a proper way to deal with rest pose in godot.
My goal is to scale a bone from the Rest pose (why rest pose, because I want animations to play on top)

First thing, I wanted to simple display the rest pose bones and calculate the different bone sizes which are wrong compare to what blender gives me. (Btw: scaling the y basis of the bone rest transform won't do what I want as it's skewing the matrices down the chain)

        var start = GetBoneRest(id);
        var end = GetBoneRest(idEnd);

        GD.Print("Length: " + (end.origin - start.origin).Length());

And as stated here: https://godotengine.org/qa/7631/armature-differences-between-bones-custom_pose-transform

Rest * Custom * Pose -> this is giving me the right transform of the RestPose (even relatively speaking).

Any help is more than welcome, to be honest dealing with bones right now in Godot is a nightmare, it lacks an easy way to control bones programmatically, for instance in Unity you just manipulate Transforms as bones making life so much easier.

Comments

  • TwistedTwiglegTwistedTwigleg Posts: 2,641Admin

    Something that the documentation does not mention, and I have had to learn the hard way, is that Rest, Custom and Pose are all relative offsets to their parent bone’s transform. The reason the snippet of code you posted is not working is because it will just compare the difference between the offsets for each rest position, ultimately telling you the difference in length between each bone’s parent, if that makes sense.

    To get the lengths between the bones, you will need to take the local poses and convert them to global poses. In Godot 4.0, there will be a handy function to help with this, but for now, you’ll need to make a GDScript version to perform this conversion. The following code will take a local pose and convert it to a global pose:

    Transform Skeleton3D::global_pose_to_local_pose(int p_bone_idx, Transform p_global_pose) {
        if (bones[p_bone_idx].parent >= 0) {
            int parent_bone_idx = bones[p_bone_idx].parent;
            Transform conversion_transform = (bones[parent_bone_idx].pose_global * bones[p_bone_idx].rest);
            return conversion_transform.affine_inverse() * p_global_pose;
        } else {
            return p_global_pose;
        }
    }
    
    Transform Skeleton3D::local_pose_to_global_pose(int p_bone_idx, Transform p_local_pose) {
        if (bones[p_bone_idx].parent >= 0) {
            int parent_bone_idx = bones[p_bone_idx].parent;
            Transform conversion_transform = (bones[parent_bone_idx].pose_global * bones[p_bone_idx].rest);
            return conversion_transform * p_local_pose;
        } else {
            return p_local_pose;
        }
    }
    

    The code above is in C++ though, so you’ll need to convert it to GDScript. Then, once converted, you can compare the two transform's origins to get the distance between the bones. :smile:
    Just keep in mind, this is the just the global poses, not the global positions in the scene. Another conversion step is needed to take global poses and convert them to global positions, like those seen in Spatial nodes.


    Any help is more than welcome, to be honest dealing with bones right now in Godot is a nightmare, it lacks an easy way to control bones programmatically, for instance in Unity you just manipulate Transforms as bones making life so much easier.

    Agreed! I'm working on making this easier as part of the GSoC, along with new IK options, but it still will not be as straightforward as Unity. I was going to make a Bone3D node to help with this, but Juan/Reduz was heavily against the idea and so it had to be dropped from the proposal.
    I am working on making functions to make the conversions easier and more straightforward, so I'm hoping that will help make direct bone manipulation less of a hassle in Godot moving forward.

  • panicqpanicq Posts: 67Member
    edited June 30

    @TwistedTwigleg Thanks for your reply I will re-implement those helpers in C#. However I just realized that even having the length of the bone it will be a nightmare to scale a bone while keeping an animation playing on top of it. In a normal engine you would change the rest pose. Therefore how would you change the rest bone length of top bone of a chain without affecting the scale of the children but adjusting the positions correctly ?

    Nice to hear that you're part of the GSoC, I'll be looking on your improvements with attention.

  • TwistedTwiglegTwistedTwigleg Posts: 2,641Admin

    @panicq said:
    @TwistedTwigleg Thanks for your reply I will re-implement those helpers in C#. However I just realized that even having the length of the bone it will be a nightmare to scale a bone while keeping an animation playing on top of it. In a normal engine you would change the rest pose. Therefore how would you change the rest bone length of top bone of a chain without affecting the scale of the children but adjusting the positions correctly ?

    Hmm, that's a hard one, as rotation and scale are directly linked to each other in Godot. I think what you would have to do is change the scale of the (parent) bone to whatever you want, and then go through all of the children in the skeleton and scale them down relative to the scale increase. It's not a pretty solution though, and currently in Godot bones are not aware of their children (though I have changed this). This would, however, fix the issue, it is just not very elegant.

    What are you trying to achieve? Maybe there is a way to handle it without needing to scale the bones?

    Another way you might be able to work around the issue, though again it is not pretty, is to use set_global_pose_override (I think that's what its called) for all of the children bones. However, this would require you to calculate the positions for all of the child nodes and keep them updated, not to mention it would lose animation support, which defeats the point entirely. :expressionless:

    Nice to hear that you're part of the GSoC, I'll be looking on your improvements with attention.

    Thanks :smile:

  • panicqpanicq Posts: 67Member
    edited June 30

    However, this would require you to calculate the positions for all of the child nodes and keep them updated, not to mention it would lose animation support, which defeats the point entirely

    This is why at the beginning I tried to calculate the length of the bones and finally I realized I would lost animation support using SetGlobalPoseOverride.

    This would, however, fix the issue, it is just not very elegant.

    Honestly at this point I don't care, a lot of things are not elegant in Godot for now.

    However I really would love to here more details about your solution as for now if I'm executing the code below, the second bone isn't "canceling" the transform of the previous one :(

    I've made a 90° test skinned mesh and when scaling the first bone along its Y basis the second one scales on another basis, which leads me to think that the inverse is not being applied, or maybe I'm missing something.

    Transform t = GetBoneCustomPose(BoneChain[0]);
        t.basis.y = OriginalBonesTransform[0].basis.y * ScaleFactor;
        SetBoneCustomPose(BoneChain[0], t);
    
        Transform t2 = t.AffineInverse() * t;
        SetBoneCustomPose(BoneChain[1], t2);
    

    Hope you can help me with this simple configuration :)

    @TwistedTwigleg Thanks for your time.

  • TwistedTwiglegTwistedTwigleg Posts: 2,641Admin
    edited June 30

    I was thinking something like this, if applied to the snippet above (untested, pseudo code):

    Transform t = GetBoneCustomPose(BoneChain[0]);
    t.basis.y = OriginalBonesTransform[0].basis.y * ScaleFactor;
    SetBoneCustomPose(BoneChain[0], t);
    
    // the transform for the child bone(s) will need to be gotten and the
    // code below executed on them. I'm just using the second bone in
    // the chain as an example:
    Transform t2 = GetBoneCustomPose(BoneChain[1]);
    // Undo the scale by applying the inverse.
    // So if the bone is scaled by 2 on a certain axis, then the child bone
    // needs to be scaled by 0.5 on the same axis, undoing
    // the scale for that child bone.
    t2.basis.y = OriginalBonesTransform[1].basis.y * (Vector3(1, 1, 1) / ScaleFactor);
    SetBoneCustomPose(BoneChain[1], t2);
    

    That is what I was thinking of, more or less. Let me know if that doesn't work and/or doesn't help, and I'll see what I can do :smile:

  • panicqpanicq Posts: 67Member
    edited June 30

    @TwistedTwigleg Unfortunately it's not working. I've already tried something similar without success, this is because when bones are not aligned the bone matrix of the parent will propagate to its children but not to the local matrix of the children, so scaling the basis.y wont scale the child.basis.y unless their aligned :( Anyway I need to find how to apply the inverse transform of the parent to the child :( :

  • TwistedTwiglegTwistedTwigleg Posts: 2,641Admin

    Hmm, yeah, I see the issue. I have a few ideas on how it may be fixable. Let me see what I can do :smile:

  • TwistedTwiglegTwistedTwigleg Posts: 2,641Admin

    Well, I may have found a solution, with a minor technical limitation

    If you're bones use the Y axis as forward, then the following code should fix the issue (GDScript code):

    tool
    extends Skeleton
    
    export (Vector3) var scale_factor = Vector3(1, 1, 1);
    
    var scale_bone_name = "bone_2";
    var scale_bone_idx = -1;
    var scale_bone_cached_trans = Transform();
    
    var child_bone_idx = -1;
    var child_bone_cached_trans = Transform();
    
    func _ready():
        scale_bone_idx = find_bone(scale_bone_name);
        child_bone_idx = scale_bone_idx + 1; # Just for testing. I know this is the only child.
    
        scale_bone_cached_trans = get_bone_custom_pose(scale_bone_idx);
        child_bone_cached_trans = get_bone_custom_pose(child_bone_idx);
    
    func _process(delta):
        var scale_trans = get_bone_custom_pose(scale_bone_idx);
        scale_trans.basis.x = scale_bone_cached_trans.basis.x * scale_factor.x;
        scale_trans.basis.y = scale_bone_cached_trans.basis.y * scale_factor.y;
        scale_trans.basis.z = scale_bone_cached_trans.basis.z * scale_factor.z;
        set_bone_custom_pose(scale_bone_idx, scale_trans);
    
        var child_trans = get_bone_custom_pose(child_bone_idx);
    
        var child_scale_factor = Vector3.ONE / scale_factor;
        #child_scale_factor = child_scale_factor.rotated(Vector3.FORWARD, PI / 2.0);
        child_scale_factor = child_scale_factor.rotated(scale_trans.basis.z.normalized(), PI / 2.0);
        child_scale_factor = child_scale_factor.abs();
    
        child_trans.basis.x = child_bone_cached_trans.basis.x * child_scale_factor.x;
        child_trans.basis.y = child_bone_cached_trans.basis.y * child_scale_factor.y;
        child_trans.basis.z = child_bone_cached_trans.basis.z * child_scale_factor.z;
        print (child_bone_cached_trans.basis.get_scale(), " | ", child_trans.basis.get_scale());
        set_bone_custom_pose(child_bone_idx, child_trans);
    

    How this works is that it rotates the vector for the child bone by the rotation of the parent, making the axes it scales on correct, which in turn cancel the scale of the parent bone. However, this only works for bones that have +Y as the forward facing axis, as line 31 uses the perpendicular basis axis to perform the rotation for child bones. If your 3D models use a different axis, you will need to adjust line 31 so it uses the correct perpendicular axis for your 3D model.
    Hopefully that will work though!

  • panicqpanicq Posts: 67Member
    edited July 1

    @TwistedTwigleg Hey, Thanks for your answer and your time, however this only work if you know the angle between the bones and I'm anticipating the nightmare of finding the angle for any cases. This is not generic and also unfortunately complex for a simple tasks. After trying to find a suitable solution for many hours I'll stop working on that and wait until this nightmare is fixed. Even without a hierarchy it's so frustrating not being able to do simple matrix math on such a simple problem ... I'm probably missing something but honestly the doc is very poor and I don't have the energy to dive into the cpp sources... It's sad to see that Godot excels on some advance things (such as the new upcoming SDFgI) and is really late on common simple things such as Bones or RenderTextures...

    Thanks anyway

  • TwistedTwiglegTwistedTwigleg Posts: 2,641Admin

    Yeah, probably best for now. You might be able to get the angle using something like parent_bone_basis.y.angle_to(child_bone_basis.y), but as you mentioned, it is unfortunately complex for a relatively simple task.

    Hopefully things will get better in the future!

Leave a Comment

BoldItalicStrikethroughOrdered listUnordered list
Emoji
Image
Align leftAlign centerAlign rightToggle HTML viewToggle full pageToggle lights
Drop image/file