Feat/14 movement overhaul #20
@@ -139,14 +139,15 @@ func load_model(path: String) -> void:
|
||||
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
|
||||
|
||||
func _ensure_locomotion_animations(ap: AnimationPlayer) -> void:
|
||||
## Creates procedural Walk, Run, Jump, and Idle animations if missing from the GLB.
|
||||
## These use the bone names discovered in the imported skeleton.
|
||||
## Track paths are relative to the AnimationPlayer's parent (MikuRig node).
|
||||
## 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 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")
|
||||
@@ -165,8 +166,16 @@ func _ensure_locomotion_animations(ap: AnimationPlayer) -> void:
|
||||
# and Skeleton3D is a child of MikuRig. The GLB's own animations use this path format.
|
||||
var prefix: String = "MikuRig/Skeleton3D:"
|
||||
|
||||
if not ap.has_animation("Walk"):
|
||||
print("SkinnedPlayerModel: creating Walk animation")
|
||||
# Always replace animations that have near-zero motion (Blender GLTF track stripping)
|
||||
var broken_walk := _is_animation_broken(ap, "Walk")
|
||||
var broken_run := _is_animation_broken(ap, "Run")
|
||||
var broken_jump := _is_animation_broken(ap, "Jump")
|
||||
var broken_idle := _is_animation_broken(ap, "Idle")
|
||||
if not broken_idle:
|
||||
broken_idle = _is_animation_broken(ap, "idle")
|
||||
|
||||
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,
|
||||
@@ -177,8 +186,8 @@ func _ensure_locomotion_animations(ap: AnimationPlayer) -> void:
|
||||
)
|
||||
_add_animation_to_player(ap, "Walk", anim)
|
||||
|
||||
if not ap.has_animation("Run"):
|
||||
print("SkinnedPlayerModel: creating Run animation")
|
||||
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,
|
||||
@@ -189,34 +198,112 @@ func _ensure_locomotion_animations(ap: AnimationPlayer) -> void:
|
||||
)
|
||||
_add_animation_to_player(ap, "Run", anim)
|
||||
|
||||
if not ap.has_animation("Jump"):
|
||||
print("SkinnedPlayerModel: creating Jump animation")
|
||||
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_bone_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) }], prefix)
|
||||
_add_bone_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) }], prefix)
|
||||
_add_bone_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) }], prefix)
|
||||
_add_bone_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) }], prefix)
|
||||
_add_bone_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) }], prefix)
|
||||
_add_bone_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) }], prefix)
|
||||
_add_bone_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) }], prefix)
|
||||
_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)
|
||||
|
||||
if not ap.has_animation("Idle"):
|
||||
print("SkinnedPlayerModel: creating Idle animation")
|
||||
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
|
||||
_add_bone_track(anim, prefix + spine, [{ "time": 0.0, "value": Vector3(0, 0.005, 0) }, { "time": 1.0, "value": Vector3(0, -0.005, 0) }, { "time": 2.0, "value": Vector3(0, 0.005, 0) }], prefix)
|
||||
_add_bone_track(anim, prefix + head, [{ "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) }], prefix)
|
||||
_add_bone_track(anim, prefix + left_upper_arm, [{ "time": 0.0, "value": Vector3(0, 0, 0.1) }, { "time": 1.0, "value": Vector3(0, 0, 0.12) }, { "time": 2.0, "value": Vector3(0, 0, 0.1) }], prefix)
|
||||
_add_bone_track(anim, prefix + right_upper_arm, [{ "time": 0.0, "value": Vector3(0, 0, -0.1) }, { "time": 1.0, "value": Vector3(0, 0, -0.12) }, { "time": 2.0, "value": Vector3(0, 0, -0.1) }], prefix)
|
||||
_add_bone_track(anim, prefix + hips, [{ "time": 0.0, "value": Vector3(0, 0, 0) }, { "time": 1.0, "value": Vector3(0, 0.003, 0) }, { "time": 2.0, "value": Vector3(0, 0, 0) }], prefix)
|
||||
# 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)
|
||||
|
||||
func _is_animation_broken(ap: AnimationPlayer, anim_name: String) -> bool:
|
||||
## Returns true if the animation has near-zero motion (Blender GLTF track stripping).
|
||||
## Checks the maximum delta between any two keyframe values on any track.
|
||||
if not ap.has_animation(anim_name):
|
||||
return true
|
||||
var anim = ap.get_animation(anim_name)
|
||||
if anim.get_track_count() == 0:
|
||||
return true
|
||||
# Check max delta across all tracks
|
||||
var max_delta: float = 0.0
|
||||
for t in range(anim.get_track_count()):
|
||||
var key_count = anim.track_get_key_count(t)
|
||||
if key_count < 2:
|
||||
continue
|
||||
var path = String(anim.track_get_path(t))
|
||||
var is_rotation_track = path.ends_with(":rotation")
|
||||
for i in range(1, key_count):
|
||||
var v = anim.track_get_key_value(t, i)
|
||||
if v is Vector3:
|
||||
var prev_v = anim.track_get_key_value(t, i - 1)
|
||||
var delta: float
|
||||
if is_rotation_track:
|
||||
# For rotation tracks, sum absolute euler angle differences
|
||||
delta = abs(v.x - prev_v.x) + abs(v.y - prev_v.y) + abs(v.z - prev_v.z)
|
||||
else:
|
||||
delta = v.distance_to(prev_v)
|
||||
max_delta = max(max_delta, delta)
|
||||
# Threshold: rotation < 0.02 rad total (~1.1 deg) or position < 0.005 units
|
||||
return max_delta < 0.02
|
||||
|
||||
func _get_bone_name(bone_name: String) -> String:
|
||||
## Returns the bone name as stored in the skeleton (uses cache).
|
||||
if _bone_cache.has(bone_name):
|
||||
@@ -283,8 +370,8 @@ func _create_locomotion_anim(
|
||||
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])
|
||||
|
||||
# Hips bounce
|
||||
_add_bone_track_catmull(anim, prefix + hips, [t1, t2, t3, t4, t5], [
|
||||
# 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])
|
||||
|
||||
@@ -292,14 +379,33 @@ func _create_locomotion_anim(
|
||||
|
||||
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)
|
||||
for i in range(times.size()):
|
||||
anim.track_insert_key(track_idx, times[i], Vector3(x_vals[i], y_vals[i], z_vals[i]))
|
||||
|
||||
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)
|
||||
for i in range(times.size()):
|
||||
anim.track_insert_key(track_idx, times[i], Vector3(x_vals[i], y_vals[i], z_vals[i]))
|
||||
|
||||
func _add_bone_track(anim: Animation, bone_path: String, keyframes: Array, _prefix: String = "") -> void:
|
||||
## Adds a value track from an array of {time, value} dictionaries.
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user