Repository: godot
Issue: Skeleton3D.set_bone_pose() does not set the real pose transform
Labels: bug,topic:xr,topic:3d

### Tested versions

Reproducible in 4.3.stable

### System information

Godot v4.3.stable (77dcf97d8) - NixOS #1-NixOS SMP PREEMPT_DYNAMIC Thu Sep 12 09:13:13 UTC 2024 - X11 - Vulkan (Mobile) - integrated Intel(R) Graphics (ADL GT2) - 12th Gen Intel(R) Core(TM) i5-1240P (16 Threads)

### Issue description

This issue affects my implementation of XR Hand tracking in the addon https://github.com/Godot-Dojo/Godot-XR-AH

Because people's hands are different sizes, the OpenXR hand tracking interface outputs the XYZ coordinates of the joints of the  fingers wherever they are, and does not assume a particular skeleton size.  

Since chains of Skeleton3D bones (eg in the fingers from the metacarpals on up) are usually aligned along the Y-axis, I decided I should be able to apply a scaling in the Y-axis of each bone so it would fit the model's joint positions to the joint positions provided by the OpenXR interface.

Consider the index finger.  Suppose the proximal (first bone above your knuckle) bone length of the model is `a1` and the distal bone length of the model is `a2`, and suppose the corresponding bone lengths of your real hand are `b1` and `b2`.

Suppose the rotation between these two bones is `rot`.

To make the skeleton fit your real hand properly: 
  * proximal bone pose_transform `t1.basis = Scale(1,b1/a1,1)`
  * distal bone pose_transform `t2.basis = Scale(1,a1/b1,1) * rot * Scale(1,b2/a2,1)`

This left-multiplication of a Scale transform is necessary so that the scale of `t1 * t2` is `Scale(1,b2/a2,1)` to stretch the next bone only along its Y-axis.  

The function implementation of [Skeleton3D::set_bone_pose](https://github.com/godotengine/godot/blob/master/scene/3d/skeleton_3d.cpp#L841) is not going to properly extract this pre-scaling and preserve the transform that was set (see Steps to Reproduce)
```C++
void Skeleton3D::set_bone_pose(int p_bone, const Transform3D &p_pose) {
       <snip>
	bones[p_bone].pose_position = p_pose.origin;
	bones[p_bone].pose_rotation = p_pose.basis.get_rotation_quaternion();
	bones[p_bone].pose_scale = p_pose.basis.get_scale();
       <snip>add 
}
```

To put this another way, the `set_bone_pose()` function only allows scaled-conformal transforms, which could be argued is a reasonable limitation if nobody wants to have sheared transforms applied to their meshes.  Unfortunately, it is the`global_bone_pose` transforms that are applied to the mesh, and since the product of two scaled-conformal transforms is not always scaled-conformal, you cannot avoid having a sheared transform because it requires non-scaled-conformal transforms in the `bone_pose` values to avoid it. 

I don't know how to resolve this issue.  

Presently, there is no possible way to use the non-uniform `bone_pose_scale` feature on skeleton bones and get a reasonable result.   The [Bone Struct](https://github.com/godotengine/godot/blob/master/scene/3d/skeleton_3d.h#L95) has a `Transform3D pose_cache` attribute, but it is tightly coupled to the three components `pose_position`, `pose_rotation`, and `pose_scale`.  

Even if I could set that underlying transform directly, we could forget about animating it, since animations only apply to those three components.  

Alternatively, we could add another attribute called `pose_prescale` that allows us to undo the scale from the previous transform, which would add extra computations on all Skeleton3Ds that no one wants right now.

At the very least, this is a serious gotcha, not unlike using [Transform3D.inverse()](https://docs.godotengine.org/en/stable/classes/class_transform3d.html#class-transform3d-method-inverse) when you should use an `affine_inverse()`, except in this case there is no function to do what is expected.


### Steps to reproduce

I can replicate it with this simple bit of EditorScript code on a new Skeleton3D

The main issue is that if I call `set_bone_pose()` with a valid transform, the value of `get_bone_pose()` can be different.  This issue has caused be two days of debugging of my own transform code before I suspected this fault.

```GDScript
@tool
extends EditorScript

func _run():
	var s = Skeleton3D.new()
	s.add_bone("one")
	var rot = Basis(Vector3(1,0,0), deg_to_rad(10))
	var scaA = Basis.from_scale(Vector3(1,0.5,1))
	var scaB = Basis.from_scale(Vector3(1,2,1))

	print("Prescale comparison")
	var b = scaB*rot
	var t = Transform3D(b, Vector3(0,0,0))
	s.set_bone_pose(0, t)
	var r = s.get_bone_pose(0)
	print(r)
	print(t)

	print("Postscale comparison")
	var b1 = rot*scaB
	var t1 = Transform3D(b1, Vector3(0,0,0))
	s.set_bone_pose(0, t1)
	var r1 = s.get_bone_pose(0)
	print(r1)
	print(t1)
```

outputs:

```
Prescale comparison
[X: (1, 0, 0), Y: (0, 1.969615, 0.173648), Z: (0, -0.091709, 1.040217), O: (0, 0, 0)]
[X: (1, 0, 0), Y: (0, 1.969615, 0.173648), Z: (0, -0.347296, 0.984808), O: (0, 0, 0)]
Postscale comparison
[X: (1, 0, 0), Y: (0, 1.969615, 0.347296), Z: (0, -0.173648, 0.984808), O: (0, 0, 0)]
[X: (1, 0, 0), Y: (0, 1.969615, 0.347296), Z: (0, -0.173648, 0.984808), O: (0, 0, 0)]
```

### Minimal reproduction project (MRP)

see above