fix: replace stripped GLB animations with procedural rotation-based animations
Blender's GLTF exporter aggressively strips animation tracks that are near rest pose. The Miku GLB has 54 tracks per animation but all keyframe values are essentially rest pose (max delta 0.008 units), causing the model to appear frozen in T-pose despite the AnimationPlayer playing animations correctly. This fix: - Adds _is_animation_broken() to detect near-zero motion in animations - Changes _ensure_locomotion_animations() to replace broken animations (not just create when missing) - Uses TYPE_VALUE tracks with ':rotation' suffix for proper bone rotation - Fixes hips bounce to use position track instead of rotation - Adds _add_rotation_track, _add_position_track, _add_position_track_catmull helper functions
This commit is contained in:
@@ -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