Feat/14 movement overhaul #20
+117
-136
@@ -23,17 +23,8 @@ 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
|
||||
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:
|
||||
if model_path != "":
|
||||
@@ -44,12 +35,6 @@ func load_model(path: String) -> void:
|
||||
child.queue_free()
|
||||
_bone_cache.clear()
|
||||
_bone_indices.clear()
|
||||
_procedural_anims.clear()
|
||||
_bone_rest_local_rot.clear()
|
||||
_bone_rest_local_pos.clear()
|
||||
_current_anim_name = ""
|
||||
_anim_time = 0.0
|
||||
|
||||
print("SkinnedPlayerModel: loading %s via GLTFDocument" % path)
|
||||
|
||||
# Read GLB file as bytes
|
||||
@@ -92,14 +77,10 @@ func load_model(path: String) -> void:
|
||||
skeleton = _find_skeleton(scene)
|
||||
if skeleton:
|
||||
print("SkinnedPlayerModel: found skeleton '%s' with %d bones" % [skeleton.name, skeleton.get_bone_count()])
|
||||
# Cache bone indices for fast lookups in procedural animations
|
||||
# Cache bone indices for fast lookups
|
||||
for i in range(skeleton.get_bone_count()):
|
||||
_bone_cache[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:
|
||||
print("SkinnedPlayerModel: WARNING - no skeleton found")
|
||||
|
||||
@@ -162,37 +143,124 @@ 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.
|
||||
## Replaces GLB animations that were stripped by Blender's GLTF exporter
|
||||
## (tracks exist but contain near-zero motion — model appears to T-pose).
|
||||
## Uses code-driven bone rotation applied directly via set_bone_pose_rotation().
|
||||
## 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
|
||||
# by Blender's GLTF exporter (all tracks have near-zero motion)
|
||||
var broken_walk := true
|
||||
var broken_run := true
|
||||
var broken_jump := true
|
||||
var broken_idle := true
|
||||
_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())
|
||||
|
||||
if broken_walk:
|
||||
print("SkinnedPlayerModel: Walk animation has near-zero motion — replacing with procedural")
|
||||
_procedural_anims["Walk"] = _create_locomotion_data(true)
|
||||
_add_animation_to_player(ap, "Walk", Animation.new()) # placeholder for AP list
|
||||
func _create_and_play_anim(ap: AnimationPlayer, name: String, anim: Animation) -> void:
|
||||
var lib = ap.get_animation_library("")
|
||||
if lib.has_animation(name):
|
||||
lib.remove_animation(name)
|
||||
lib.add_animation(name, anim)
|
||||
|
||||
if broken_run:
|
||||
print("SkinnedPlayerModel: Run animation has near-zero motion — replacing with procedural")
|
||||
_procedural_anims["Run"] = _create_locomotion_data(true, 1.8)
|
||||
_add_animation_to_player(ap, "Run", Animation.new())
|
||||
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
|
||||
|
||||
if broken_jump:
|
||||
print("SkinnedPlayerModel: Jump animation has near-zero motion — replacing with procedural")
|
||||
_procedural_anims["Jump"] = _create_jump_data()
|
||||
_add_animation_to_player(ap, "Jump", Animation.new())
|
||||
# Add rotation tracks for each bone using type=1 (ROTATION)
|
||||
_add_rot_track(anim, "LeftUpperLeg", cycle, [
|
||||
{"t": 0.0, "q": Quaternion.from_euler(Vector3(-0.6, 0, 0))},
|
||||
{"t": cycle * 0.5, "q": Quaternion.from_euler(Vector3(0.6, 0, 0))},
|
||||
{"t": cycle, "q": Quaternion.from_euler(Vector3(-0.6, 0, 0))}
|
||||
])
|
||||
_add_rot_track(anim, "RightUpperLeg", cycle, [
|
||||
{"t": 0.0, "q": Quaternion.from_euler(Vector3(0.6, 0, 0))},
|
||||
{"t": cycle * 0.5, "q": Quaternion.from_euler(Vector3(-0.6, 0, 0))},
|
||||
{"t": cycle, "q": Quaternion.from_euler(Vector3(0.6, 0, 0))}
|
||||
])
|
||||
_add_rot_track(anim, "LeftUpperArm", cycle, [
|
||||
{"t": 0.0, "q": Quaternion.from_euler(Vector3(-0.4, 0, 0))},
|
||||
{"t": cycle * 0.5, "q": Quaternion.from_euler(Vector3(0.4, 0, 0))},
|
||||
{"t": cycle, "q": Quaternion.from_euler(Vector3(-0.4, 0, 0))}
|
||||
])
|
||||
_add_rot_track(anim, "RightUpperArm", cycle, [
|
||||
{"t": 0.0, "q": Quaternion.from_euler(Vector3(0.4, 0, 0))},
|
||||
{"t": cycle * 0.5, "q": Quaternion.from_euler(Vector3(-0.4, 0, 0))},
|
||||
{"t": cycle, "q": Quaternion.from_euler(Vector3(0.4, 0, 0))}
|
||||
])
|
||||
_add_rot_track(anim, "Spine", cycle, [
|
||||
{"t": 0.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))},
|
||||
{"t": cycle * 0.25, "q": Quaternion.from_euler(Vector3(0, 0.1, 0))},
|
||||
{"t": cycle * 0.5, "q": Quaternion.from_euler(Vector3(0, 0, 0))},
|
||||
{"t": cycle * 0.75, "q": Quaternion.from_euler(Vector3(0, -0.1, 0))},
|
||||
{"t": cycle, "q": Quaternion.from_euler(Vector3(0, 0, 0))}
|
||||
])
|
||||
return anim
|
||||
|
||||
if broken_idle:
|
||||
print("SkinnedPlayerModel: Idle animation has near-zero motion — replacing with procedural")
|
||||
_procedural_anims["Idle"] = _create_idle_data()
|
||||
_add_animation_to_player(ap, "Idle", Animation.new())
|
||||
func _create_run_anim() -> Animation:
|
||||
var anim = _create_walk_anim()
|
||||
anim.length = 0.5 # faster
|
||||
return anim
|
||||
|
||||
func _create_jump_anim() -> Animation:
|
||||
var anim = Animation.new()
|
||||
anim.length = 1.0
|
||||
anim.loop_mode = Animation.LOOP_LINEAR
|
||||
|
||||
_add_rot_track(anim, "LeftUpperArm", 1.0, [
|
||||
{"t": 0.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))},
|
||||
{"t": 0.5, "q": Quaternion.from_euler(Vector3(-1.2, 0, 0))},
|
||||
{"t": 1.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))}
|
||||
])
|
||||
_add_rot_track(anim, "RightUpperArm", 1.0, [
|
||||
{"t": 0.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))},
|
||||
{"t": 0.5, "q": Quaternion.from_euler(Vector3(-1.2, 0, 0))},
|
||||
{"t": 1.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))}
|
||||
])
|
||||
_add_rot_track(anim, "LeftUpperLeg", 1.0, [
|
||||
{"t": 0.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))},
|
||||
{"t": 0.5, "q": Quaternion.from_euler(Vector3(-0.3, 0, 0))},
|
||||
{"t": 1.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))}
|
||||
])
|
||||
_add_rot_track(anim, "RightUpperLeg", 1.0, [
|
||||
{"t": 0.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))},
|
||||
{"t": 0.5, "q": Quaternion.from_euler(Vector3(-0.3, 0, 0))},
|
||||
{"t": 1.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))}
|
||||
])
|
||||
return anim
|
||||
|
||||
func _create_idle_anim() -> Animation:
|
||||
var anim = Animation.new()
|
||||
anim.length = 2.0
|
||||
anim.loop_mode = Animation.LOOP_LINEAR
|
||||
|
||||
_add_rot_track(anim, "Spine", 2.0, [
|
||||
{"t": 0.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))},
|
||||
{"t": 1.0, "q": Quaternion.from_euler(Vector3(0.02, 0, 0))},
|
||||
{"t": 2.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))}
|
||||
])
|
||||
_add_rot_track(anim, "Head", 2.0, [
|
||||
{"t": 0.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))},
|
||||
{"t": 1.0, "q": Quaternion.from_euler(Vector3(0.03, 0.02, 0))},
|
||||
{"t": 2.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))}
|
||||
])
|
||||
_add_rot_track(anim, "LeftUpperArm", 2.0, [
|
||||
{"t": 0.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))},
|
||||
{"t": 1.0, "q": Quaternion.from_euler(Vector3(0.02, 0, 0.05))},
|
||||
{"t": 2.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))}
|
||||
])
|
||||
_add_rot_track(anim, "RightUpperArm", 2.0, [
|
||||
{"t": 0.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))},
|
||||
{"t": 1.0, "q": Quaternion.from_euler(Vector3(0.02, 0, -0.05))},
|
||||
{"t": 2.0, "q": Quaternion.from_euler(Vector3(0, 0, 0))}
|
||||
])
|
||||
return anim
|
||||
|
||||
func _add_rot_track(anim: Animation, bone_name: String, length: float, keyframes: Array) -> void:
|
||||
var track = anim.add_track(1) # TYPE_ROTATION
|
||||
anim.track_set_path(track, "MikuRig/Skeleton3D:" + bone_name)
|
||||
anim.track_set_interpolation_type(track, Animation.INTERPOLATION_LINEAR)
|
||||
for kf in keyframes:
|
||||
anim.track_insert_key(track, kf["t"], kf["q"])
|
||||
|
||||
func _is_animation_broken(ap: AnimationPlayer, anim_name: String) -> bool:
|
||||
## Returns true if the animation has near-zero motion (Blender GLTF track stripping).
|
||||
@@ -460,7 +528,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 skeleton:
|
||||
if not animation_player:
|
||||
return
|
||||
|
||||
# Get movement state from parent PlayerMovementController
|
||||
@@ -503,101 +571,14 @@ func _process(delta: float) -> void:
|
||||
_:
|
||||
target_anim = "Idle"
|
||||
|
||||
# Switch animation if needed
|
||||
if target_anim != _current_anim_name:
|
||||
_current_anim_name = target_anim
|
||||
_anim_time = 0.0
|
||||
# For procedural anims: stop AP completely so it doesn't overwrite our poses
|
||||
# For GLB anims (Death, Crouch): play on AP
|
||||
if _procedural_anims.has(target_anim):
|
||||
if animation_player:
|
||||
animation_player.stop()
|
||||
animation_player.active = false
|
||||
# Reset all bones to rest pose before applying procedural animation
|
||||
if skeleton:
|
||||
# Clear any previous global pose overrides
|
||||
for i in range(skeleton.get_bone_count()):
|
||||
skeleton.set_bone_global_pose_override(i, Transform3D(), 0.0, true)
|
||||
skeleton.set_bone_pose_rotation(i, _bone_rest_local_rot.get(i, Quaternion.IDENTITY))
|
||||
skeleton.set_bone_pose_position(i, _bone_rest_local_pos.get(i, Vector3.ZERO))
|
||||
elif animation_player and animation_player.has_animation(target_anim):
|
||||
animation_player.active = true
|
||||
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)
|
||||
|
||||
# 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:
|
||||
_anim_time = min(_anim_time, length)
|
||||
# Apply procedural animation to skeleton
|
||||
_apply_procedural_animation(data, _anim_time)
|
||||
else:
|
||||
# For non-procedural anims (Death, Crouch from GLB), use AP
|
||||
if animation_player:
|
||||
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.
|
||||
## Sets local pose = rest_local * offset_quat for correct skinning.
|
||||
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)
|
||||
# Compute offset quaternion from euler angles
|
||||
var offset_quat = Quaternion.from_euler(rot)
|
||||
# Get rest pose local rotation for this bone
|
||||
var rest_quat: Quaternion = _bone_rest_local_rot.get(bone_idx, Quaternion.IDENTITY)
|
||||
# local_pose = rest_local * offset (so final = rest * offset)
|
||||
var new_local_quat = rest_quat * offset_quat
|
||||
skeleton.set_bone_pose_rotation(bone_idx, new_local_quat)
|
||||
# Keep rest pose local position
|
||||
var rest_pos: Vector3 = _bone_rest_local_pos.get(bone_idx, Vector3.ZERO)
|
||||
skeleton.set_bone_pose_position(bone_idx, rest_pos)
|
||||
elif prev_kf.has("pos") and next_kf.has("pos"):
|
||||
var pos: Vector3 = lerp(prev_kf["pos"], next_kf["pos"], t)
|
||||
# Position is additive on top of rest pose position
|
||||
var rest_pos: Vector3 = _bone_rest_local_pos.get(bone_idx, Vector3.ZERO)
|
||||
skeleton.set_bone_pose_position(bone_idx, rest_pos + pos)
|
||||
pass # end of _process
|
||||
|
||||
Reference in New Issue
Block a user