fix: replace AnimationPlayer tracks with code-driven bone rotation system
Godot 4.2.1 AnimationPlayer cannot rotate runtime-loaded GLB bones via ':rotation' track paths. The AnimationPlayer only updates bone positions, not rotations, on runtime GLB skeletons. This fix replaces the entire animation approach: - Remove AnimationPlayer track-based rotation attempts - Add code-driven system that stores animation keyframe data in dictionaries and applies bone rotations directly via skeleton.set_bone_pose_rotation() each frame - _is_animation_broken() detects Blender GLTF track stripping - _ensure_locomotion_animations() replaces broken GLB anims with procedural data - _apply_procedural_animation() interpolates euler angles, converts to Quaternion via Quaternion.from_euler(), and applies per bone
This commit is contained in:
+233
-227
@@ -23,6 +23,16 @@ var _anim_debug_timer: float = 0.0
|
||||
var _mesh_instance: MeshInstance3D
|
||||
var _bone_cache: Dictionary = {} # bone_name -> index
|
||||
|
||||
## Code-driven animation system — applies bone rotations directly each frame.
|
||||
## Godot 4.2.1 AnimationPlayer cannot rotate runtime-loaded GLB bones via tracks,
|
||||
## so we interpolate and apply poses manually using set_bone_pose_rotation().
|
||||
var _current_anim_name: String = ""
|
||||
var _anim_time: float = 0.0
|
||||
var _procedural_anims: Dictionary = {} # name -> {length, loop, tracks}
|
||||
|
||||
## Bone indices for procedural animation (cached at load time)
|
||||
var _bone_indices: Dictionary = {} # bone_name -> index
|
||||
|
||||
func _ready() -> void:
|
||||
if model_path != "":
|
||||
load_model(model_path)
|
||||
@@ -31,6 +41,10 @@ func load_model(path: String) -> void:
|
||||
for child in get_children():
|
||||
child.queue_free()
|
||||
_bone_cache.clear()
|
||||
_bone_indices.clear()
|
||||
_procedural_anims.clear()
|
||||
_current_anim_name = ""
|
||||
_anim_time = 0.0
|
||||
|
||||
print("SkinnedPlayerModel: loading %s via GLTFDocument" % path)
|
||||
|
||||
@@ -77,6 +91,7 @@ func load_model(path: String) -> void:
|
||||
# Cache bone indices for fast lookups in procedural animations
|
||||
for i in range(skeleton.get_bone_count()):
|
||||
_bone_cache[skeleton.get_bone_name(i)] = i
|
||||
_bone_indices[skeleton.get_bone_name(i)] = i
|
||||
else:
|
||||
print("SkinnedPlayerModel: WARNING - no skeleton found")
|
||||
|
||||
@@ -142,29 +157,7 @@ func _ensure_locomotion_animations(ap: AnimationPlayer) -> void:
|
||||
## Creates procedural Walk, Run, Jump, and Idle animations.
|
||||
## Replaces GLB animations that were stripped by Blender's GLTF exporter
|
||||
## (tracks exist but contain near-zero motion — model appears to T-pose).
|
||||
## These procedural animations use rotation tracks for visible skeletal motion.
|
||||
|
||||
# Bone names match the GLB skeleton exactly
|
||||
var hips: String = _get_bone_name("Hips")
|
||||
var spine: String = _get_bone_name("Spine")
|
||||
var chest: String = _get_bone_name("Chest")
|
||||
var head: String = _get_bone_name("Head")
|
||||
var left_upper_arm: String = _get_bone_name("LeftUpperArm")
|
||||
var left_lower_arm: String = _get_bone_name("LeftLowerArm")
|
||||
var _left_hand: String = _get_bone_name("LeftHand")
|
||||
var right_upper_arm: String = _get_bone_name("RightUpperArm")
|
||||
var right_lower_arm: String = _get_bone_name("RightLowerArm")
|
||||
var _right_hand: String = _get_bone_name("RightHand")
|
||||
var left_upper_leg: String = _get_bone_name("LeftUpperLeg")
|
||||
var left_lower_leg: String = _get_bone_name("LeftLowerLeg")
|
||||
var left_foot: String = _get_bone_name("LeftFoot")
|
||||
var right_upper_leg: String = _get_bone_name("RightUpperLeg")
|
||||
var right_lower_leg: String = _get_bone_name("RightLowerLeg")
|
||||
var right_foot: String = _get_bone_name("RightFoot")
|
||||
|
||||
# Use "MikuRig/Skeleton3D:" prefix — AnimationPlayer is sibling of MikuRig,
|
||||
# and Skeleton3D is a child of MikuRig. The GLB's own animations use this path format.
|
||||
var prefix: String = "MikuRig/Skeleton3D:"
|
||||
## Uses code-driven bone rotation applied directly via set_bone_pose_rotation().
|
||||
|
||||
# Always replace animations that have near-zero motion (Blender GLTF track stripping)
|
||||
var broken_walk := _is_animation_broken(ap, "Walk")
|
||||
@@ -176,103 +169,23 @@ func _ensure_locomotion_animations(ap: AnimationPlayer) -> void:
|
||||
|
||||
if broken_walk:
|
||||
print("SkinnedPlayerModel: Walk animation has near-zero motion — replacing with procedural")
|
||||
var anim = _create_locomotion_anim(
|
||||
prefix, true,
|
||||
left_upper_leg, left_lower_leg, left_foot,
|
||||
right_upper_leg, right_lower_leg, right_foot,
|
||||
left_upper_arm, left_lower_arm,
|
||||
right_upper_arm, right_lower_arm,
|
||||
spine, hips
|
||||
)
|
||||
_add_animation_to_player(ap, "Walk", anim)
|
||||
_procedural_anims["Walk"] = _create_locomotion_data(true)
|
||||
_add_animation_to_player(ap, "Walk", Animation.new()) # placeholder for AP list
|
||||
|
||||
if broken_run:
|
||||
print("SkinnedPlayerModel: Run animation has near-zero motion — replacing with procedural")
|
||||
var anim = _create_locomotion_anim(
|
||||
prefix, true,
|
||||
left_upper_leg, left_lower_leg, left_foot,
|
||||
right_upper_leg, right_lower_leg, right_foot,
|
||||
left_upper_arm, left_lower_arm,
|
||||
right_upper_arm, right_lower_arm,
|
||||
spine, hips, 1.8 # faster cycle for run
|
||||
)
|
||||
_add_animation_to_player(ap, "Run", anim)
|
||||
_procedural_anims["Run"] = _create_locomotion_data(true, 1.8)
|
||||
_add_animation_to_player(ap, "Run", Animation.new())
|
||||
|
||||
if broken_jump:
|
||||
print("SkinnedPlayerModel: Jump animation has near-zero motion — replacing with procedural")
|
||||
var anim = Animation.new()
|
||||
anim.length = 1.0
|
||||
anim.loop_mode = Animation.LOOP_LINEAR
|
||||
# Arms raised, knees slightly bent, brief hop
|
||||
_add_rotation_track(anim, prefix + left_upper_arm, [
|
||||
{"time": 0.0, "value": Vector3(-0.8, 0, 0.3)},
|
||||
{"time": 0.5, "value": Vector3(-1.2, 0, 0.5)},
|
||||
{"time": 1.0, "value": Vector3(-0.8, 0, 0.3)}
|
||||
])
|
||||
_add_rotation_track(anim, prefix + right_upper_arm, [
|
||||
{"time": 0.0, "value": Vector3(-0.8, 0, -0.3)},
|
||||
{"time": 0.5, "value": Vector3(-1.2, 0, -0.5)},
|
||||
{"time": 1.0, "value": Vector3(-0.8, 0, -0.3)}
|
||||
])
|
||||
_add_rotation_track(anim, prefix + left_lower_arm, [
|
||||
{"time": 0.0, "value": Vector3(0, 0, 0)},
|
||||
{"time": 0.5, "value": Vector3(-0.4, 0, 0)},
|
||||
{"time": 1.0, "value": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_rotation_track(anim, prefix + right_lower_arm, [
|
||||
{"time": 0.0, "value": Vector3(0, 0, 0)},
|
||||
{"time": 0.5, "value": Vector3(-0.4, 0, 0)},
|
||||
{"time": 1.0, "value": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_rotation_track(anim, prefix + left_upper_leg, [
|
||||
{"time": 0.0, "value": Vector3(0, 0, 0)},
|
||||
{"time": 0.5, "value": Vector3(-0.3, 0, 0)},
|
||||
{"time": 1.0, "value": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_rotation_track(anim, prefix + right_upper_leg, [
|
||||
{"time": 0.0, "value": Vector3(0, 0, 0)},
|
||||
{"time": 0.5, "value": Vector3(-0.3, 0, 0)},
|
||||
{"time": 1.0, "value": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_rotation_track(anim, prefix + spine, [
|
||||
{"time": 0.0, "value": Vector3(0, 0, 0)},
|
||||
{"time": 0.5, "value": Vector3(0.1, 0, 0)},
|
||||
{"time": 1.0, "value": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_animation_to_player(ap, "Jump", anim)
|
||||
_procedural_anims["Jump"] = _create_jump_data()
|
||||
_add_animation_to_player(ap, "Jump", Animation.new())
|
||||
|
||||
if broken_idle:
|
||||
print("SkinnedPlayerModel: Idle animation has near-zero motion — replacing with procedural")
|
||||
var anim = Animation.new()
|
||||
anim.length = 2.0
|
||||
anim.loop_mode = Animation.LOOP_LINEAR
|
||||
# Gentle breathing/sway — use rotation tracks for visible motion
|
||||
_add_rotation_track(anim, prefix + spine, [
|
||||
{"time": 0.0, "value": Vector3(0, 0, 0)},
|
||||
{"time": 1.0, "value": Vector3(0.02, 0, 0)},
|
||||
{"time": 2.0, "value": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_rotation_track(anim, prefix + head, [
|
||||
{"time": 0.0, "value": Vector3(0, 0, 0)},
|
||||
{"time": 1.0, "value": Vector3(0.03, 0.02, 0)},
|
||||
{"time": 2.0, "value": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_rotation_track(anim, prefix + left_upper_arm, [
|
||||
{"time": 0.0, "value": Vector3(0, 0, 0.05)},
|
||||
{"time": 1.0, "value": Vector3(0.02, 0, 0.08)},
|
||||
{"time": 2.0, "value": Vector3(0, 0, 0.05)}
|
||||
])
|
||||
_add_rotation_track(anim, prefix + right_upper_arm, [
|
||||
{"time": 0.0, "value": Vector3(0, 0, -0.05)},
|
||||
{"time": 1.0, "value": Vector3(0.02, 0, -0.08)},
|
||||
{"time": 2.0, "value": Vector3(0, 0, -0.05)}
|
||||
])
|
||||
_add_position_track(anim, prefix + hips, [
|
||||
{"time": 0.0, "value": Vector3(0, 0, 0)},
|
||||
{"time": 1.0, "value": Vector3(0, 0.005, 0)},
|
||||
{"time": 2.0, "value": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_animation_to_player(ap, "Idle", anim)
|
||||
_procedural_anims["Idle"] = _create_idle_data()
|
||||
_add_animation_to_player(ap, "Idle", Animation.new())
|
||||
|
||||
func _is_animation_broken(ap: AnimationPlayer, anim_name: String) -> bool:
|
||||
## Returns true if the animation has near-zero motion (Blender GLTF track stripping).
|
||||
@@ -310,107 +223,159 @@ func _get_bone_name(bone_name: String) -> String:
|
||||
return bone_name
|
||||
return bone_name
|
||||
|
||||
func _create_locomotion_anim(
|
||||
prefix: String,
|
||||
loop: bool,
|
||||
l_leg_upper: String, l_leg_lower: String, _l_foot: String,
|
||||
r_leg_upper: String, r_leg_lower: String, _r_foot: String,
|
||||
l_arm_upper: String, l_arm_lower: String,
|
||||
r_arm_upper: String, r_arm_lower: String,
|
||||
spine: String, hips: String,
|
||||
speed_mult: float = 1.0
|
||||
) -> Animation:
|
||||
var anim = Animation.new()
|
||||
var cycle: float = 1.0 / locomotion_cycle_speed * speed_mult
|
||||
anim.length = cycle
|
||||
anim.loop_mode = Animation.LOOP_LINEAR if loop else Animation.LOOP_NONE
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CODE-DRIVEN ANIMATION SYSTEM
|
||||
# Godot 4.2.1 AnimationPlayer cannot rotate bones via ":rotation" track paths
|
||||
# on runtime-loaded GLB skeletons. We apply rotations directly each frame.
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
func _create_locomotion_data(loop: bool, speed_mult: float = 1.0) -> Dictionary:
|
||||
## Creates a walk/run animation data structure.
|
||||
## Returns: {length, loop, tracks: [{bone, keyframes: [{t, rot: Vector3}]}]}
|
||||
var cycle: float = 1.0 / locomotion_cycle_speed * speed_mult
|
||||
var t1: float = 0.0
|
||||
var t2: float = cycle * 0.25
|
||||
var t3: float = cycle * 0.5
|
||||
var t4: float = cycle * 0.75
|
||||
var t5: float = cycle
|
||||
|
||||
var tracks: Array = []
|
||||
|
||||
# Legs: opposite phase walk cycle
|
||||
_add_bone_track_catmull(anim, prefix + l_leg_upper, [t1, t2, t3, t4, t5], [
|
||||
-0.6, -0.1, 0.6, -0.1, -0.6 # swing forward/back
|
||||
], [0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
|
||||
_add_bone_track_catmull(anim, prefix + l_leg_lower, [t1, t2, t3, t4, t5], [
|
||||
0.1, 0.5, 0.8, 0.3, 0.1 # bend on back swing
|
||||
], [0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
|
||||
_add_bone_track_catmull(anim, prefix + r_leg_upper, [t1, t2, t3, t4, t5], [
|
||||
0.6, -0.1, -0.6, -0.1, 0.6 # opposite phase
|
||||
], [0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
|
||||
_add_bone_track_catmull(anim, prefix + r_leg_lower, [t1, t2, t3, t4, t5], [
|
||||
0.3, 0.1, 0.1, 0.5, 0.3 # bend on back swing
|
||||
], [0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
_add_catmull_rot_track(tracks, "LeftUpperLeg", [t1, t2, t3, t4, t5],
|
||||
[-0.6, -0.1, 0.6, -0.1, -0.6], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
|
||||
_add_catmull_rot_track(tracks, "LeftLowerLeg", [t1, t2, t3, t4, t5],
|
||||
[0.1, 0.5, 0.8, 0.3, 0.1], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
|
||||
_add_catmull_rot_track(tracks, "RightUpperLeg", [t1, t2, t3, t4, t5],
|
||||
[0.6, -0.1, -0.6, -0.1, 0.6], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
|
||||
_add_catmull_rot_track(tracks, "RightLowerLeg", [t1, t2, t3, t4, t5],
|
||||
[0.3, 0.1, 0.1, 0.5, 0.3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
|
||||
|
||||
# Arms: opposite to legs
|
||||
_add_bone_track_catmull(anim, prefix + l_arm_upper, [t1, t2, t3, t4, t5], [
|
||||
0.5, 0.0, -0.5, 0.0, 0.5
|
||||
], [0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
|
||||
_add_bone_track_catmull(anim, prefix + l_arm_lower, [t1, t2, t3, t4, t5], [
|
||||
-0.8, -0.3, -0.2, -0.5, -0.8
|
||||
], [0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
|
||||
_add_bone_track_catmull(anim, prefix + r_arm_upper, [t1, t2, t3, t4, t5], [
|
||||
-0.5, 0.0, 0.5, 0.0, -0.5
|
||||
], [0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
|
||||
_add_bone_track_catmull(anim, prefix + r_arm_lower, [t1, t2, t3, t4, t5], [
|
||||
-0.2, -0.5, -0.8, -0.3, -0.2
|
||||
], [0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
_add_catmull_rot_track(tracks, "LeftUpperArm", [t1, t2, t3, t4, t5],
|
||||
[0.5, 0.0, -0.5, 0.0, 0.5], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
|
||||
_add_catmull_rot_track(tracks, "LeftLowerArm", [t1, t2, t3, t4, t5],
|
||||
[-0.8, -0.3, -0.2, -0.5, -0.8], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
|
||||
_add_catmull_rot_track(tracks, "RightUpperArm", [t1, t2, t3, t4, t5],
|
||||
[-0.5, 0.0, 0.5, 0.0, -0.5], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
|
||||
_add_catmull_rot_track(tracks, "RightLowerArm", [t1, t2, t3, t4, t5],
|
||||
[-0.2, -0.5, -0.8, -0.3, -0.2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
|
||||
|
||||
# Spine twist
|
||||
_add_bone_track_catmull(anim, prefix + spine, [t1, t2, t3, t4, t5], [
|
||||
0.0, 0.05, 0.0, -0.05, 0.0
|
||||
], [0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
_add_catmull_rot_track(tracks, "Spine", [t1, t2, t3, t4, t5],
|
||||
[0.0, 0.05, 0.0, -0.05, 0.0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
|
||||
|
||||
# Hips bounce — position offset (not rotation)
|
||||
_add_position_track_catmull(anim, prefix + hips, [t1, t2, t3, t4, t5], [
|
||||
0.0, 0.0, 0.0, 0.0, 0.0
|
||||
], [0.0, 0.02, 0.0, 0.02, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
# Hips bounce (position offset)
|
||||
_add_catmull_pos_track(tracks, "Hips", [t1, t2, t3, t4, t5],
|
||||
[0, 0, 0, 0, 0], [0, 0.02, 0, 0.02, 0], [0, 0, 0, 0, 0])
|
||||
|
||||
return anim
|
||||
return {"length": cycle, "loop": loop, "tracks": tracks}
|
||||
|
||||
func _add_bone_track_catmull(anim: Animation, bone_path: String, times: Array, x_vals: Array, y_vals: Array, z_vals: Array) -> void:
|
||||
## Adds a rotation track with Catmull-Rom-style keyframes from separate axis arrays.
|
||||
## Uses ":rotation" suffix on the path to target bone rotation.
|
||||
var track_idx = anim.add_track(Animation.TYPE_VALUE)
|
||||
anim.track_set_path(track_idx, bone_path + ":rotation")
|
||||
anim.track_set_interpolation_type(track_idx, Animation.INTERPOLATION_LINEAR)
|
||||
func _create_jump_data() -> Dictionary:
|
||||
## Creates jump animation data.
|
||||
var tracks: Array = []
|
||||
var t1: float = 0.0
|
||||
var t2: float = 0.5
|
||||
var t3: float = 1.0
|
||||
|
||||
_add_keyframe_rot_track(tracks, "LeftUpperArm", [
|
||||
{"t": t1, "rot": Vector3(-0.8, 0, 0.3)},
|
||||
{"t": t2, "rot": Vector3(-1.2, 0, 0.5)},
|
||||
{"t": t3, "rot": Vector3(-0.8, 0, 0.3)}
|
||||
])
|
||||
_add_keyframe_rot_track(tracks, "RightUpperArm", [
|
||||
{"t": t1, "rot": Vector3(-0.8, 0, -0.3)},
|
||||
{"t": t2, "rot": Vector3(-1.2, 0, -0.5)},
|
||||
{"t": t3, "rot": Vector3(-0.8, 0, -0.3)}
|
||||
])
|
||||
_add_keyframe_rot_track(tracks, "LeftLowerArm", [
|
||||
{"t": t1, "rot": Vector3(0, 0, 0)},
|
||||
{"t": t2, "rot": Vector3(-0.4, 0, 0)},
|
||||
{"t": t3, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_keyframe_rot_track(tracks, "RightLowerArm", [
|
||||
{"t": t1, "rot": Vector3(0, 0, 0)},
|
||||
{"t": t2, "rot": Vector3(-0.4, 0, 0)},
|
||||
{"t": t3, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_keyframe_rot_track(tracks, "LeftUpperLeg", [
|
||||
{"t": t1, "rot": Vector3(0, 0, 0)},
|
||||
{"t": t2, "rot": Vector3(-0.3, 0, 0)},
|
||||
{"t": t3, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_keyframe_rot_track(tracks, "RightUpperLeg", [
|
||||
{"t": t1, "rot": Vector3(0, 0, 0)},
|
||||
{"t": t2, "rot": Vector3(-0.3, 0, 0)},
|
||||
{"t": t3, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_keyframe_rot_track(tracks, "Spine", [
|
||||
{"t": t1, "rot": Vector3(0, 0, 0)},
|
||||
{"t": t2, "rot": Vector3(0.1, 0, 0)},
|
||||
{"t": t3, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
|
||||
return {"length": 1.0, "loop": true, "tracks": tracks}
|
||||
|
||||
func _create_idle_data() -> Dictionary:
|
||||
## Creates idle animation data.
|
||||
var tracks: Array = []
|
||||
var t1: float = 0.0
|
||||
var t2: float = 1.0
|
||||
var t3: float = 2.0
|
||||
|
||||
_add_keyframe_rot_track(tracks, "Spine", [
|
||||
{"t": t1, "rot": Vector3(0, 0, 0)},
|
||||
{"t": t2, "rot": Vector3(0.02, 0, 0)},
|
||||
{"t": t3, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_keyframe_rot_track(tracks, "Head", [
|
||||
{"t": t1, "rot": Vector3(0, 0, 0)},
|
||||
{"t": t2, "rot": Vector3(0.03, 0.02, 0)},
|
||||
{"t": t3, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_keyframe_rot_track(tracks, "LeftUpperArm", [
|
||||
{"t": t1, "rot": Vector3(0, 0, 0.05)},
|
||||
{"t": t2, "rot": Vector3(0.02, 0, 0.08)},
|
||||
{"t": t3, "rot": Vector3(0, 0, 0.05)}
|
||||
])
|
||||
_add_keyframe_rot_track(tracks, "RightUpperArm", [
|
||||
{"t": t1, "rot": Vector3(0, 0, -0.05)},
|
||||
{"t": t2, "rot": Vector3(0.02, 0, -0.08)},
|
||||
{"t": t3, "rot": Vector3(0, 0, -0.05)}
|
||||
])
|
||||
_add_keyframe_pos_track(tracks, "Hips", [
|
||||
{"t": t1, "pos": Vector3(0, 0, 0)},
|
||||
{"t": t2, "pos": Vector3(0, 0.005, 0)},
|
||||
{"t": t3, "pos": Vector3(0, 0, 0)}
|
||||
])
|
||||
|
||||
return {"length": 2.0, "loop": true, "tracks": tracks}
|
||||
|
||||
func _add_catmull_rot_track(tracks: Array, bone_name: String, times: Array, x: Array, y: Array, z: Array) -> void:
|
||||
var kf: Array = []
|
||||
for i in range(times.size()):
|
||||
anim.track_insert_key(track_idx, times[i], Vector3(x_vals[i], y_vals[i], z_vals[i]))
|
||||
kf.append({"t": times[i], "rot": Vector3(x[i], y[i], z[i])})
|
||||
tracks.append({"bone": bone_name, "keyframes": kf})
|
||||
|
||||
func _add_rotation_track(anim: Animation, bone_path: String, keyframes: Array) -> void:
|
||||
## Adds a rotation track from an array of {time, value} dictionaries.
|
||||
## Uses ":rotation" suffix on the path to target bone rotation.
|
||||
var track_idx = anim.add_track(Animation.TYPE_VALUE)
|
||||
anim.track_set_path(track_idx, bone_path + ":rotation")
|
||||
anim.track_set_interpolation_type(track_idx, Animation.INTERPOLATION_LINEAR)
|
||||
for kf in keyframes:
|
||||
anim.track_insert_key(track_idx, kf["time"], kf["value"])
|
||||
|
||||
func _add_position_track_catmull(anim: Animation, bone_path: String, times: Array, x_vals: Array, y_vals: Array, z_vals: Array) -> void:
|
||||
## Adds a position/value track with Catmull-Rom-style keyframes from separate axis arrays.
|
||||
## No ":rotation" suffix — targets bone position directly.
|
||||
var track_idx = anim.add_track(Animation.TYPE_VALUE)
|
||||
anim.track_set_path(track_idx, bone_path)
|
||||
anim.track_set_interpolation_type(track_idx, Animation.INTERPOLATION_LINEAR)
|
||||
func _add_catmull_pos_track(tracks: Array, bone_name: String, times: Array, x: Array, y: Array, z: Array) -> void:
|
||||
var kf: Array = []
|
||||
for i in range(times.size()):
|
||||
anim.track_insert_key(track_idx, times[i], Vector3(x_vals[i], y_vals[i], z_vals[i]))
|
||||
kf.append({"t": times[i], "pos": Vector3(x[i], y[i], z[i])})
|
||||
tracks.append({"bone": bone_name, "keyframes": kf})
|
||||
|
||||
func _add_keyframe_rot_track(tracks: Array, bone_name: String, keyframes: Array) -> void:
|
||||
var kf: Array = []
|
||||
for k in keyframes:
|
||||
kf.append({"t": k["t"], "rot": k["rot"]})
|
||||
tracks.append({"bone": bone_name, "keyframes": kf})
|
||||
|
||||
func _add_keyframe_pos_track(tracks: Array, bone_name: String, keyframes: Array) -> void:
|
||||
var kf: Array = []
|
||||
for k in keyframes:
|
||||
kf.append({"t": k["t"], "pos": k["pos"]})
|
||||
tracks.append({"bone": bone_name, "keyframes": kf})
|
||||
|
||||
|
||||
func _add_position_track(anim: Animation, bone_path: String, keyframes: Array) -> void:
|
||||
## Adds a position/value track from an array of {time, value} dictionaries.
|
||||
var track_idx = anim.add_track(Animation.TYPE_VALUE)
|
||||
anim.track_set_path(track_idx, bone_path)
|
||||
anim.track_set_interpolation_type(track_idx, Animation.INTERPOLATION_LINEAR)
|
||||
for kf in keyframes:
|
||||
anim.track_insert_key(track_idx, kf["time"], kf["value"])
|
||||
|
||||
func _add_animation_to_player(ap: AnimationPlayer, anim_name: String, anim: Animation) -> void:
|
||||
## Adds an animation to the AnimationPlayer via its default library.
|
||||
@@ -489,27 +454,9 @@ func _warn_missing(anim_name: String) -> void:
|
||||
print("SkinnedPlayerModel: WARNING - '%s' not available (using fallback)" % anim_name)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not animation_player:
|
||||
if not skeleton:
|
||||
return
|
||||
|
||||
# Debug output every 2 seconds (rate-limited to avoid log spam)
|
||||
_anim_debug_timer += delta
|
||||
if _anim_debug_timer > 2.0:
|
||||
_anim_debug_timer = 0.0
|
||||
if animation_player.is_playing():
|
||||
print("SkinnedPlayerModel: playing '%s' (pos: %.2f)" % [animation_player.current_animation, animation_player.current_animation_position])
|
||||
elif _last_played == "":
|
||||
print("SkinnedPlayerModel: NOT playing")
|
||||
|
||||
# Force skeleton skin update EVERY frame, deferred to ensure it runs
|
||||
# AFTER the AnimationPlayer has advanced its poses this frame.
|
||||
# GLTF runtime loaded models need this to emit bone_pose_changed,
|
||||
# which triggers the MeshInstance to re-compute its skin vertex buffers.
|
||||
# Without deferral, this runs before AnimationPlayer._process() (due to
|
||||
# tree order), so the MeshInstance would render with stale poses.
|
||||
if skeleton:
|
||||
call_deferred("_update_skin")
|
||||
|
||||
# Get movement state from parent PlayerMovementController
|
||||
var parent = get_parent()
|
||||
if not parent:
|
||||
@@ -550,19 +497,78 @@ func _process(delta: float) -> void:
|
||||
_:
|
||||
target_anim = "Idle"
|
||||
|
||||
if target_anim != _last_played:
|
||||
if animation_player.has_animation(target_anim):
|
||||
play_animation(target_anim)
|
||||
_last_played = target_anim
|
||||
# Switch animation if needed
|
||||
if target_anim != _current_anim_name:
|
||||
_current_anim_name = target_anim
|
||||
_anim_time = 0.0
|
||||
# Also play on AP for debug output
|
||||
if animation_player and animation_player.has_animation(target_anim):
|
||||
animation_player.play(target_anim)
|
||||
|
||||
# Advance animation time
|
||||
if _procedural_anims.has(_current_anim_name):
|
||||
var data = _procedural_anims[_current_anim_name]
|
||||
var length: float = data["length"]
|
||||
var loop: bool = data["loop"]
|
||||
_anim_time += delta
|
||||
if loop:
|
||||
_anim_time = fmod(_anim_time, length)
|
||||
else:
|
||||
_warn_missing(target_anim)
|
||||
# Try fallback to Idle — should always exist
|
||||
if animation_player.has_animation(target_anim.replace(target_anim, "Idle")):
|
||||
play_animation("Idle")
|
||||
_last_played = "Idle"
|
||||
elif _last_played == "" or not animation_player.is_playing():
|
||||
# Last resort: play first available animation
|
||||
var anim_list = animation_player.get_animation_list()
|
||||
if anim_list.size() > 0:
|
||||
play_animation(anim_list[0])
|
||||
_last_played = anim_list[0]
|
||||
_anim_time = min(_anim_time, length)
|
||||
# Apply procedural animation to skeleton
|
||||
_apply_procedural_animation(data, _anim_time)
|
||||
elif animation_player:
|
||||
# For non-procedural anims (Death, Crouch from GLB), just update skin
|
||||
call_deferred("_update_skin")
|
||||
# Debug output
|
||||
_anim_debug_timer += delta
|
||||
if _anim_debug_timer > 2.0:
|
||||
_anim_debug_timer = 0.0
|
||||
if animation_player.is_playing():
|
||||
print("SkinnedPlayerModel: playing '%s' (pos: %.2f)" % [animation_player.current_animation, animation_player.current_animation_position])
|
||||
|
||||
# Force skeleton skin update every frame (deferred to run after pose updates)
|
||||
if skeleton:
|
||||
call_deferred("_update_skin")
|
||||
|
||||
|
||||
func _apply_procedural_animation(data: Dictionary, time: float) -> void:
|
||||
## Applies procedural animation data to the skeleton at the given time.
|
||||
## Uses linear interpolation between keyframe values.
|
||||
if not skeleton:
|
||||
return
|
||||
var tracks = data["tracks"]
|
||||
for track in tracks:
|
||||
var bone_name = track["bone"]
|
||||
var bone_idx = _bone_indices.get(bone_name, -1)
|
||||
if bone_idx < 0:
|
||||
continue
|
||||
var keyframes = track["keyframes"]
|
||||
if keyframes.size() == 0:
|
||||
continue
|
||||
|
||||
# Find the two keyframes surrounding the current time
|
||||
var prev_kf = keyframes[0]
|
||||
var next_kf = keyframes[keyframes.size() - 1]
|
||||
for i in range(keyframes.size()):
|
||||
if keyframes[i]["t"] <= time:
|
||||
prev_kf = keyframes[i]
|
||||
if keyframes[i]["t"] >= time:
|
||||
next_kf = keyframes[i]
|
||||
break
|
||||
|
||||
# Interpolate between keyframes
|
||||
var segment_length: float = next_kf["t"] - prev_kf["t"]
|
||||
var t: float = 0.0
|
||||
if segment_length > 0.001:
|
||||
t = (time - prev_kf["t"]) / segment_length
|
||||
t = clamp(t, 0.0, 1.0)
|
||||
|
||||
if prev_kf.has("rot") and next_kf.has("rot"):
|
||||
var rot: Vector3 = lerp(prev_kf["rot"], next_kf["rot"], t)
|
||||
# Convert euler angles to Quaternion for set_bone_pose_rotation
|
||||
var quat = Quaternion.from_euler(rot)
|
||||
skeleton.set_bone_pose_rotation(bone_idx, quat)
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user