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:
@@ -32,6 +32,8 @@ var _procedural_anims: Dictionary = {} # name -> {length, loop, tracks}
|
|||||||
|
|
||||||
## Bone indices for procedural animation (cached at load time)
|
## Bone indices for procedural animation (cached at load time)
|
||||||
var _bone_indices: Dictionary = {} # bone_name -> index
|
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:
|
func _ready() -> void:
|
||||||
if model_path != "":
|
if model_path != "":
|
||||||
@@ -43,6 +45,8 @@ func load_model(path: String) -> void:
|
|||||||
_bone_cache.clear()
|
_bone_cache.clear()
|
||||||
_bone_indices.clear()
|
_bone_indices.clear()
|
||||||
_procedural_anims.clear()
|
_procedural_anims.clear()
|
||||||
|
_bone_rest_local_rot.clear()
|
||||||
|
_bone_rest_local_pos.clear()
|
||||||
_current_anim_name = ""
|
_current_anim_name = ""
|
||||||
_anim_time = 0.0
|
_anim_time = 0.0
|
||||||
|
|
||||||
@@ -92,6 +96,10 @@ func load_model(path: String) -> void:
|
|||||||
for i in range(skeleton.get_bone_count()):
|
for i in range(skeleton.get_bone_count()):
|
||||||
_bone_cache[skeleton.get_bone_name(i)] = i
|
_bone_cache[skeleton.get_bone_name(i)] = i
|
||||||
_bone_indices[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:
|
else:
|
||||||
print("SkinnedPlayerModel: WARNING - no skeleton found")
|
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
|
# Reset all bones to rest pose before applying procedural animation
|
||||||
if skeleton:
|
if skeleton:
|
||||||
for i in range(skeleton.get_bone_count()):
|
for i in range(skeleton.get_bone_count()):
|
||||||
skeleton.set_bone_pose_position(i, Vector3.ZERO)
|
skeleton.set_bone_pose_rotation(i, _bone_rest_local_rot.get(i, Quaternion.IDENTITY))
|
||||||
skeleton.set_bone_pose_rotation(i, Quaternion.IDENTITY)
|
skeleton.set_bone_pose_position(i, _bone_rest_local_pos.get(i, Vector3.ZERO))
|
||||||
skeleton.set_bone_pose_scale(i, Vector3.ONE)
|
|
||||||
elif animation_player and animation_player.has_animation(target_anim):
|
elif animation_player and animation_player.has_animation(target_anim):
|
||||||
animation_player.active = true
|
animation_player.active = true
|
||||||
animation_player.play(target_anim)
|
animation_player.play(target_anim)
|
||||||
@@ -545,7 +552,7 @@ func _process(delta: float) -> void:
|
|||||||
|
|
||||||
func _apply_procedural_animation(data: Dictionary, time: float) -> void:
|
func _apply_procedural_animation(data: Dictionary, time: float) -> void:
|
||||||
## Applies procedural animation data to the skeleton at the given time.
|
## 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:
|
if not skeleton:
|
||||||
return
|
return
|
||||||
var tracks = data["tracks"]
|
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"):
|
if prev_kf.has("rot") and next_kf.has("rot"):
|
||||||
var rot: Vector3 = lerp(prev_kf["rot"], next_kf["rot"], t)
|
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)
|
var rest_global = skeleton.get_bone_global_pose(bone_idx)
|
||||||
# Apply rotation offset around world axes (pre-multiply)
|
# Apply rotation around the bone's swing axis
|
||||||
# For locomotion: X = forward/back swing, Y = twist, Z = lateral
|
# rot.x = forward/back swing (rotation around world X)
|
||||||
var world_basis = Basis(Quaternion.from_euler(rot))
|
# rot.y = twist (rotation around bone's forward axis)
|
||||||
var new_global = Transform3D(world_basis * rest_global.basis, rest_global.origin)
|
# 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)
|
skeleton.set_bone_global_pose_override(bone_idx, new_global, 1.0, true)
|
||||||
elif prev_kf.has("pos") and next_kf.has("pos"):
|
elif prev_kf.has("pos") and next_kf.has("pos"):
|
||||||
var pos: Vector3 = lerp(prev_kf["pos"], next_kf["pos"], t)
|
var pos: Vector3 = lerp(prev_kf["pos"], next_kf["pos"], t)
|
||||||
|
|||||||
Reference in New Issue
Block a user