replace AP animation with code-driven bone override system
This commit is contained in:
+169
-138
@@ -136,134 +136,23 @@ func load_model(path: String) -> void:
|
||||
_ensure_locomotion_animations(animation_player)
|
||||
else:
|
||||
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
|
||||
|
||||
func _ensure_locomotion_animations(ap: AnimationPlayer) -> void:
|
||||
## Creates proper Animation resources for Walk, Run, Jump, and Idle.
|
||||
## Replaces GLB animations that were stripped by Blender's GLTF exporter.
|
||||
## Uses AnimationPlayer tracks (type=0 for position, type=1 for rotation).
|
||||
|
||||
# Always replace all locomotion animations — GLB is known to be stripped
|
||||
_create_and_play_anim(ap, "Walk", _create_walk_anim())
|
||||
_create_and_play_anim(ap, "Run", _create_run_anim())
|
||||
_create_and_play_anim(ap, "Jump", _create_jump_anim())
|
||||
_create_and_play_anim(ap, "Idle", _create_idle_anim())
|
||||
|
||||
func _create_and_play_anim(ap: AnimationPlayer, anim_name: String, anim: Animation) -> void:
|
||||
var lib = ap.get_animation_library("")
|
||||
if lib.has_animation(anim_name):
|
||||
lib.remove_animation(anim_name)
|
||||
lib.add_animation(anim_name, anim)
|
||||
print("SkinnedPlayerModel: replaced '%s' with %d tracks" % [anim_name, anim.get_track_count()])
|
||||
func _ensure_locomotion_animations(_ap: AnimationPlayer) -> void:
|
||||
## AnimationPlayer doesn't properly update bone transforms for runtime
|
||||
## GLB skeletons. Animation resources are kept for reference but the
|
||||
## actual animation is done code-driven in _process().
|
||||
pass
|
||||
|
||||
func _create_walk_anim() -> Animation:
|
||||
var anim = Animation.new()
|
||||
anim.length = 1.0
|
||||
anim.loop_mode = Animation.LOOP_LINEAR
|
||||
var cycle = 1.0 / locomotion_cycle_speed
|
||||
anim.length = cycle
|
||||
|
||||
# Add rotation tracks for each bone using type=1 (ROTATION)
|
||||
_add_rot_track(anim, "LeftUpperLeg", [
|
||||
{"t": 0.0, "rot": Vector3(-0.6, 0, 0)},
|
||||
{"t": cycle * 0.5, "rot": Vector3(0.6, 0, 0)},
|
||||
{"t": cycle, "rot": Vector3(-0.6, 0, 0)}
|
||||
])
|
||||
_add_rot_track(anim, "RightUpperLeg", [
|
||||
{"t": 0.0, "rot": Vector3(0.6, 0, 0)},
|
||||
{"t": cycle * 0.5, "rot": Vector3(-0.6, 0, 0)},
|
||||
{"t": cycle, "rot": Vector3(0.6, 0, 0)}
|
||||
])
|
||||
_add_rot_track(anim, "LeftUpperArm", [
|
||||
{"t": 0.0, "rot": Vector3(-0.4, 0, 0)},
|
||||
{"t": cycle * 0.5, "rot": Vector3(0.4, 0, 0)},
|
||||
{"t": cycle, "rot": Vector3(-0.4, 0, 0)}
|
||||
])
|
||||
_add_rot_track(anim, "RightUpperArm", [
|
||||
{"t": 0.0, "rot": Vector3(0.4, 0, 0)},
|
||||
{"t": cycle * 0.5, "rot": Vector3(-0.4, 0, 0)},
|
||||
{"t": cycle, "rot": Vector3(0.4, 0, 0)}
|
||||
])
|
||||
_add_rot_track(anim, "Spine", [
|
||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
||||
{"t": cycle * 0.25, "rot": Vector3(0, 0.1, 0)},
|
||||
{"t": cycle * 0.5, "rot": Vector3(0, 0, 0)},
|
||||
{"t": cycle * 0.75, "rot": Vector3(0, -0.1, 0)},
|
||||
{"t": cycle, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
return anim
|
||||
return Animation.new()
|
||||
|
||||
func _create_run_anim() -> Animation:
|
||||
var anim = _create_walk_anim()
|
||||
anim.length = 0.5 # faster
|
||||
return anim
|
||||
return Animation.new()
|
||||
|
||||
func _create_jump_anim() -> Animation:
|
||||
var anim = Animation.new()
|
||||
anim.length = 1.0
|
||||
anim.loop_mode = Animation.LOOP_LINEAR
|
||||
|
||||
_add_rot_track(anim, "LeftUpperArm", [
|
||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
||||
{"t": 0.5, "rot": Vector3(-1.2, 0, 0)},
|
||||
{"t": 1.0, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_rot_track(anim, "RightUpperArm", [
|
||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
||||
{"t": 0.5, "rot": Vector3(-1.2, 0, 0)},
|
||||
{"t": 1.0, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_rot_track(anim, "LeftUpperLeg", [
|
||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
||||
{"t": 0.5, "rot": Vector3(-0.3, 0, 0)},
|
||||
{"t": 1.0, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_rot_track(anim, "RightUpperLeg", [
|
||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
||||
{"t": 0.5, "rot": Vector3(-0.3, 0, 0)},
|
||||
{"t": 1.0, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
return anim
|
||||
return Animation.new()
|
||||
|
||||
func _create_idle_anim() -> Animation:
|
||||
var anim = Animation.new()
|
||||
anim.length = 2.0
|
||||
anim.loop_mode = Animation.LOOP_LINEAR
|
||||
|
||||
_add_rot_track(anim, "Spine", [
|
||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
||||
{"t": 1.0, "rot": Vector3(0.02, 0, 0)},
|
||||
{"t": 2.0, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_rot_track(anim, "Head", [
|
||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
||||
{"t": 1.0, "rot": Vector3(0.03, 0.02, 0)},
|
||||
{"t": 2.0, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_rot_track(anim, "LeftUpperArm", [
|
||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
||||
{"t": 1.0, "rot": Vector3(0.02, 0, 0.05)},
|
||||
{"t": 2.0, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
_add_rot_track(anim, "RightUpperArm", [
|
||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
||||
{"t": 1.0, "rot": Vector3(0.02, 0, -0.05)},
|
||||
{"t": 2.0, "rot": Vector3(0, 0, 0)}
|
||||
])
|
||||
return anim
|
||||
|
||||
func _add_rot_track(anim: Animation, bone_name: String, keyframes: Array) -> void:
|
||||
var track = anim.add_track(Animation.TYPE_ROTATION_3D)
|
||||
if track < 0:
|
||||
print("SkinnedPlayerModel: ERROR failed to add rotation track for " + bone_name)
|
||||
return
|
||||
anim.track_set_path(track, "MikuRig/Skeleton3D:" + bone_name)
|
||||
anim.track_set_interpolation_type(track, Animation.INTERPOLATION_LINEAR)
|
||||
for kf in keyframes:
|
||||
var quat = Quaternion.from_euler(kf["rot"])
|
||||
var err = anim.track_insert_key(track, kf["t"], quat)
|
||||
if err != OK:
|
||||
print("SkinnedPlayerModel: ERROR inserting key for " + bone_name + " t=" + str(kf["t"]))
|
||||
print("SkinnedPlayerModel: rotation track '%s' has %d keys" % [bone_name, anim.track_get_key_count(track)])
|
||||
return 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).
|
||||
@@ -354,7 +243,7 @@ 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
|
||||
|
||||
# Get movement state from parent PlayerMovementController
|
||||
@@ -375,11 +264,13 @@ func _process(delta: float) -> void:
|
||||
var is_dead: bool = false
|
||||
if parent and "is_dead" in parent:
|
||||
is_dead = parent.is_dead
|
||||
var target_anim = "Idle"
|
||||
|
||||
# Determine target animation
|
||||
var anim_name = "Idle"
|
||||
if is_dead:
|
||||
target_anim = "Death"
|
||||
anim_name = "Death"
|
||||
elif is_crouching:
|
||||
target_anim = "Crouch"
|
||||
anim_name = "Crouch"
|
||||
else:
|
||||
var walk_speed = 10.0
|
||||
if sm and sm.params:
|
||||
@@ -387,26 +278,166 @@ func _process(delta: float) -> void:
|
||||
match state:
|
||||
"ground", "idle":
|
||||
if speed > walk_speed * 1.2:
|
||||
target_anim = "Run"
|
||||
anim_name = "Run"
|
||||
elif speed > 0.5:
|
||||
target_anim = "Walk"
|
||||
anim_name = "Walk"
|
||||
"slide", "wall_run", "grapple", "dash":
|
||||
target_anim = "Run"
|
||||
anim_name = "Run"
|
||||
"air":
|
||||
target_anim = "Jump"
|
||||
anim_name = "Jump"
|
||||
_:
|
||||
target_anim = "Idle"
|
||||
anim_name = "Idle"
|
||||
|
||||
if animation_player.current_animation != target_anim or not animation_player.is_playing():
|
||||
if animation_player.has_animation(target_anim):
|
||||
animation_player.play(target_anim)
|
||||
# Switch animation if needed (AP for reference timing, code for actual poses)
|
||||
if animation_player and (animation_player.current_animation != anim_name or not animation_player.is_playing()):
|
||||
if animation_player.has_animation(anim_name):
|
||||
animation_player.play(anim_name)
|
||||
|
||||
# Force the skeleton to update bone transforms for rendering
|
||||
if skeleton:
|
||||
skeleton.update_skeleton()
|
||||
# Update animation time
|
||||
_anim_time += delta
|
||||
_current_anim = anim_name
|
||||
|
||||
# Apply code-driven animation poses directly to skeleton
|
||||
_apply_animation_by_code(anim_name, _anim_time)
|
||||
|
||||
# Debug
|
||||
_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])
|
||||
pass # end of _process
|
||||
if animation_player and animation_player.is_playing():
|
||||
print("SkinnedPlayerModel: '%s' (pos: %.2f)" % [anim_name, _anim_time])
|
||||
|
||||
|
||||
# Code-driven animation: applies bone rotations directly each frame.
|
||||
var _current_anim: String = ""
|
||||
var _anim_time: float = 0.0
|
||||
|
||||
## Bone indices for animation (cached at load time)
|
||||
var _bone_idxs: Dictionary = {} # bone_name -> index
|
||||
|
||||
## Per-bone animation rot_x values for each bone (radians offset from rest)
|
||||
func _apply_animation_by_code(anim_name: String, time: float) -> void:
|
||||
if not skeleton:
|
||||
return
|
||||
|
||||
# Ensure bone indices are cached
|
||||
if _bone_idxs.is_empty():
|
||||
_cache_bone_indices()
|
||||
|
||||
# Loop all bones and reset to rest, then apply animation offset
|
||||
var bone_count: int = skeleton.get_bone_count()
|
||||
for i in range(bone_count):
|
||||
var rest: Transform3D = skeleton.get_bone_rest(i)
|
||||
var new_transform := Transform3D(rest.basis, rest.origin)
|
||||
|
||||
# Apply animation-specific rotation offset
|
||||
var offset := _get_bone_anim_offset(anim_name, i, time)
|
||||
if offset != Vector3.ZERO:
|
||||
var offset_basis := Basis(Quaternion.from_euler(offset))
|
||||
new_transform.basis = offset_basis * rest.basis
|
||||
# Recalculate global position with the rotated bone
|
||||
new_transform.origin = rest.origin
|
||||
|
||||
skeleton.set_bone_global_pose_override(i, new_transform, 1.0, true)
|
||||
|
||||
|
||||
func _cache_bone_indices() -> void:
|
||||
if not skeleton:
|
||||
return
|
||||
_bone_idxs.clear()
|
||||
for i in range(skeleton.get_bone_count()):
|
||||
_bone_idxs[skeleton.get_bone_name(i)] = i
|
||||
|
||||
|
||||
## Returns the bone rotation offset (euler radians) for a given bone at a given time.
|
||||
func _get_bone_anim_offset(anim_name: String, bone_idx: int, time: float) -> Vector3:
|
||||
if not skeleton:
|
||||
return Vector3.ZERO
|
||||
|
||||
var bone_name: String = ""
|
||||
for k in _bone_idxs:
|
||||
if _bone_idxs[k] == bone_idx:
|
||||
bone_name = k
|
||||
break
|
||||
|
||||
if anim_name == "Walk":
|
||||
return _walk_offset(bone_name, time)
|
||||
elif anim_name == "Run":
|
||||
return _run_offset(bone_name, time)
|
||||
elif anim_name == "Jump":
|
||||
return _jump_offset(bone_name, time)
|
||||
elif anim_name == "Idle":
|
||||
return _idle_offset(bone_name, time)
|
||||
return Vector3.ZERO
|
||||
|
||||
|
||||
func _walk_offset(bone_name: String, time: float) -> Vector3:
|
||||
var cycle: float = 1.0 / locomotion_cycle_speed
|
||||
var t: float = fmod(time, cycle) / cycle # Normalize to 0-1
|
||||
var phase: float = t * 6.28318 # 2*pi
|
||||
match bone_name:
|
||||
"LeftUpperLeg":
|
||||
return Vector3(sin(phase) * 0.6, 0, 0)
|
||||
"RightUpperLeg":
|
||||
return Vector3(sin(phase + 3.14159) * 0.6, 0, 0)
|
||||
"LeftUpperArm":
|
||||
return Vector3(sin(phase + 3.14159) * 0.4, 0, 0)
|
||||
"RightUpperArm":
|
||||
return Vector3(sin(phase) * 0.4, 0, 0)
|
||||
"Spine":
|
||||
return Vector3(0, sin(phase) * 0.1, 0)
|
||||
_:
|
||||
return Vector3.ZERO
|
||||
|
||||
func _run_offset(bone_name: String, time: float) -> Vector3:
|
||||
var cycle: float = 1.0 / locomotion_cycle_speed * 1.8
|
||||
var t: float = fmod(time, cycle) / cycle
|
||||
var phase: float = t * 6.28318
|
||||
match bone_name:
|
||||
"LeftUpperLeg":
|
||||
return Vector3(sin(phase) * 0.6, 0, 0)
|
||||
"RightUpperLeg":
|
||||
return Vector3(sin(phase + 3.14159) * 0.6, 0, 0)
|
||||
"LeftUpperArm":
|
||||
return Vector3(sin(phase + 3.14159) * 0.4, 0, 0)
|
||||
"RightUpperArm":
|
||||
return Vector3(sin(phase) * 0.4, 0, 0)
|
||||
_:
|
||||
return Vector3.ZERO
|
||||
|
||||
func _jump_offset(bone_name: String, time: float) -> Vector3:
|
||||
var cycle: float = 0.83
|
||||
var t: float = fmod(time, cycle) / cycle
|
||||
var phase: float = t * 6.28318
|
||||
# Brief jump pose: arms raised, knees bent at peak (t=0.5)
|
||||
var peak := 1.0 - abs(t - 0.5) * 2.0 # 0 at edges, 1 at middle
|
||||
peak = clamp(peak, 0.0, 1.0)
|
||||
match bone_name:
|
||||
"LeftUpperArm":
|
||||
return Vector3(-1.2 * peak, 0, -0.3 * peak)
|
||||
"RightUpperArm":
|
||||
return Vector3(-1.2 * peak, 0, 0.3 * peak)
|
||||
"LeftUpperLeg":
|
||||
return Vector3(-0.3 * peak, 0, 0)
|
||||
"RightUpperLeg":
|
||||
return Vector3(-0.3 * peak, 0, 0)
|
||||
"Spine":
|
||||
return Vector3(0.1 * peak, 0, 0)
|
||||
_:
|
||||
return Vector3.ZERO
|
||||
|
||||
func _idle_offset(bone_name: String, time: float) -> Vector3:
|
||||
var t = time * 0.5 # Slow breathing
|
||||
match bone_name:
|
||||
"Spine":
|
||||
return Vector3(0, sin(t) * 0.005, 0)
|
||||
"Head":
|
||||
return Vector3(sin(t * 0.7) * 0.02, sin(t * 0.5) * 0.02, 0)
|
||||
"LeftUpperArm":
|
||||
return Vector3(sin(t) * 0.02, 0, sin(t * 0.5) * 0.05)
|
||||
"RightUpperArm":
|
||||
return Vector3(sin(t) * 0.02, 0, -sin(t * 0.5) * 0.05)
|
||||
"Hips":
|
||||
return Vector3(0, sin(t) * 0.005, 0)
|
||||
_:
|
||||
return Vector3.ZERO
|
||||
|
||||
Reference in New Issue
Block a user