fix: use local pose (rest*offset) instead of global override for skinning

CRITICAL DISCOVERY: set_bone_global_pose_override() does NOT affect
the MeshInstance3D skinning pipeline. It only changes get_bone_global_pose()
return value. The actual skinning uses rest * pose * parent transforms.

The correct approach:
1. Stop AnimationPlayer (ap.stop() + ap.active = false)
2. Set local pose = rest_local_quat * offset_quat
3. This makes the final bone transform = rest * (rest * offset) = rest * offset
4. The skinning pipeline correctly uses this for vertex deformation

Also discovered: set_bone_pose_rotation() only works when AP is stopped.
When AP is active, it overwrites the local pose every frame.
This commit is contained in:
2026-06-25 11:45:23 -04:00
parent f085fdf7b7
commit 6e73fd7703
+14 -28
View File
@@ -461,17 +461,14 @@ func _warn_missing(anim_name: String) -> void:
func _process(delta: float) -> void:
if not skeleton:
print("SkinnedPlayerModel: _process - no skeleton!")
return
# Get movement state from parent PlayerMovementController
var parent = get_parent()
if not parent:
print("SkinnedPlayerModel: _process - no parent!")
return
var sm = parent.get_node_or_null("MovementStateMachine")
if not sm:
print("SkinnedPlayerModel: _process - no MovementStateMachine!")
return
var state = sm.current_state
@@ -539,7 +536,6 @@ func _process(delta: float) -> void:
_anim_time = min(_anim_time, length)
# Apply procedural animation to skeleton
_apply_procedural_animation(data, _anim_time)
print("SkinnedPlayerModel: applied '%s' t=%.2f" % [_current_anim_name, _anim_time])
else:
# For non-procedural anims (Death, Crouch from GLB), use AP
if animation_player:
@@ -558,7 +554,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.
## Rotates bones around their anatomical swing axis (world X for locomotion).
## Sets local pose = rest_local * offset_quat for correct skinning.
if not skeleton:
return
var tracks = data["tracks"]
@@ -590,28 +586,18 @@ 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 rest global pose
var rest_global = skeleton.get_bone_global_pose(bone_idx)
# 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)
# Compute offset quaternion from euler angles
var offset_quat = Quaternion.from_euler(rot)
# Get rest pose local rotation for this bone
var rest_quat: Quaternion = _bone_rest_local_rot.get(bone_idx, Quaternion.IDENTITY)
# local_pose = rest_local * offset (so final = rest * offset)
var new_local_quat = rest_quat * offset_quat
skeleton.set_bone_pose_rotation(bone_idx, new_local_quat)
# Keep rest pose local position
var rest_pos: Vector3 = _bone_rest_local_pos.get(bone_idx, Vector3.ZERO)
skeleton.set_bone_pose_position(bone_idx, rest_pos)
elif prev_kf.has("pos") and next_kf.has("pos"):
var pos: Vector3 = lerp(prev_kf["pos"], next_kf["pos"], t)
skeleton.set_bone_pose_position(bone_idx, pos)
# Position is additive on top of rest pose position
var rest_pos: Vector3 = _bone_rest_local_pos.get(bone_idx, Vector3.ZERO)
skeleton.set_bone_pose_position(bone_idx, rest_pos + pos)