fix: use world-X rotation for locomotion swing via global pose override

After extensive testing, discovered that:
1. set_bone_pose_rotation() sets ABSOLUTE local rotation, not offset
2. Bones have complex non-identity rest poses (e.g. LeftUpperArm euler=(-0.161,-2.760,-2.760))
3. Setting local pose to identity collapses the model
4. Correct approach: use set_bone_global_pose_override() with rotation
   applied around world X axis (forward/back swing) pre-multiplied on
   the rest pose basis

Also cache rest pose local transforms (rotation + position) and use them
when resetting bones or applying position offsets.
This commit is contained in:
2026-06-25 11:33:49 -04:00
parent 35decd65b3
commit e4ea177eaf
+31 -9
View File
@@ -32,6 +32,8 @@ var _procedural_anims: Dictionary = {} # name -> {length, loop, tracks}
## Bone indices for procedural animation (cached at load time)
var _bone_indices: Dictionary = {} # bone_name -> index
var _bone_rest_local_rot: Dictionary = {} # bone_idx -> Quaternion (rest local rotation)
var _bone_rest_local_pos: Dictionary = {} # bone_idx -> Vector3 (rest local position)
func _ready() -> void:
if model_path != "":
@@ -43,6 +45,8 @@ func load_model(path: String) -> void:
_bone_cache.clear()
_bone_indices.clear()
_procedural_anims.clear()
_bone_rest_local_rot.clear()
_bone_rest_local_pos.clear()
_current_anim_name = ""
_anim_time = 0.0
@@ -92,6 +96,10 @@ func load_model(path: String) -> void:
for i in range(skeleton.get_bone_count()):
_bone_cache[skeleton.get_bone_name(i)] = i
_bone_indices[skeleton.get_bone_name(i)] = i
# Cache rest pose local transforms
var rest = skeleton.get_bone_rest(i)
_bone_rest_local_rot[i] = rest.basis.get_rotation_quaternion()
_bone_rest_local_pos[i] = rest.origin
else:
print("SkinnedPlayerModel: WARNING - no skeleton found")
@@ -508,9 +516,8 @@ func _process(delta: float) -> void:
# Reset all bones to rest pose before applying procedural animation
if skeleton:
for i in range(skeleton.get_bone_count()):
skeleton.set_bone_pose_position(i, Vector3.ZERO)
skeleton.set_bone_pose_rotation(i, Quaternion.IDENTITY)
skeleton.set_bone_pose_scale(i, Vector3.ONE)
skeleton.set_bone_pose_rotation(i, _bone_rest_local_rot.get(i, Quaternion.IDENTITY))
skeleton.set_bone_pose_position(i, _bone_rest_local_pos.get(i, Vector3.ZERO))
elif animation_player and animation_player.has_animation(target_anim):
animation_player.active = true
animation_player.play(target_anim)
@@ -545,7 +552,7 @@ func _process(delta: float) -> void:
func _apply_procedural_animation(data: Dictionary, time: float) -> void:
## Applies procedural animation data to the skeleton at the given time.
## Uses global pose override to set bone transforms relative to their rest pose.
## Rotates bones around their anatomical swing axis (world X for locomotion).
if not skeleton:
return
var tracks = data["tracks"]
@@ -577,12 +584,27 @@ func _apply_procedural_animation(data: Dictionary, time: float) -> void:
if prev_kf.has("rot") and next_kf.has("rot"):
var rot: Vector3 = lerp(prev_kf["rot"], next_kf["rot"], t)
# Get the bone's rest global pose
# Get rest global pose
var rest_global = skeleton.get_bone_global_pose(bone_idx)
# Apply rotation offset around world axes (pre-multiply)
# For locomotion: X = forward/back swing, Y = twist, Z = lateral
var world_basis = Basis(Quaternion.from_euler(rot))
var new_global = Transform3D(world_basis * rest_global.basis, rest_global.origin)
# Apply rotation around the bone's swing axis
# rot.x = forward/back swing (rotation around world X)
# rot.y = twist (rotation around bone's forward axis)
# rot.z = lateral (rotation around world Z)
var new_basis = rest_global.basis
if abs(rot.x) > 0.001:
# Forward/back swing: rotate around world X axis
var swing = Basis(Vector3(1, 0, 0), rot.x)
new_basis = swing * new_basis
if abs(rot.y) > 0.001:
# Twist: rotate around the bone's Y axis (in world space)
var bone_y = new_basis.y
var twist = Basis(bone_y, rot.y)
new_basis = twist * new_basis
if abs(rot.z) > 0.001:
# Lateral: rotate around world Z
var lateral = Basis(Vector3(0, 0, 1), rot.z)
new_basis = lateral * new_basis
var new_global = Transform3D(new_basis, rest_global.origin)
skeleton.set_bone_global_pose_override(bone_idx, new_global, 1.0, true)
elif prev_kf.has("pos") and next_kf.has("pos"):
var pos: Vector3 = lerp(prev_kf["pos"], next_kf["pos"], t)