Feat/14 movement overhaul #20
+117
-136
@@ -23,17 +23,8 @@ var _anim_debug_timer: float = 0.0
|
|||||||
var _mesh_instance: MeshInstance3D
|
var _mesh_instance: MeshInstance3D
|
||||||
var _bone_cache: Dictionary = {} # bone_name -> index
|
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)
|
## Bone indices for procedural animation (cached at load time)
|
||||||
var _bone_indices: Dictionary = {} # bone_name -> index
|
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:
|
func _ready() -> void:
|
||||||
if model_path != "":
|
if model_path != "":
|
||||||
@@ -44,12 +35,6 @@ func load_model(path: String) -> void:
|
|||||||
child.queue_free()
|
child.queue_free()
|
||||||
_bone_cache.clear()
|
_bone_cache.clear()
|
||||||
_bone_indices.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)
|
print("SkinnedPlayerModel: loading %s via GLTFDocument" % path)
|
||||||
|
|
||||||
# Read GLB file as bytes
|
# Read GLB file as bytes
|
||||||
@@ -92,14 +77,10 @@ func load_model(path: String) -> void:
|
|||||||
skeleton = _find_skeleton(scene)
|
skeleton = _find_skeleton(scene)
|
||||||
if skeleton:
|
if skeleton:
|
||||||
print("SkinnedPlayerModel: found skeleton '%s' with %d bones" % [skeleton.name, skeleton.get_bone_count()])
|
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()):
|
for i in range(skeleton.get_bone_count()):
|
||||||
_bone_cache[skeleton.get_bone_name(i)] = i
|
_bone_cache[skeleton.get_bone_name(i)] = i
|
||||||
_bone_indices[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:
|
else:
|
||||||
print("SkinnedPlayerModel: WARNING - no skeleton found")
|
print("SkinnedPlayerModel: WARNING - no skeleton found")
|
||||||
|
|
||||||
@@ -162,37 +143,124 @@ func load_model(path: String) -> void:
|
|||||||
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
|
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
|
||||||
|
|
||||||
func _ensure_locomotion_animations(ap: AnimationPlayer) -> void:
|
func _ensure_locomotion_animations(ap: AnimationPlayer) -> void:
|
||||||
## Creates procedural Walk, Run, Jump, and Idle animations.
|
## Creates proper Animation resources for Walk, Run, Jump, and Idle.
|
||||||
## Replaces GLB animations that were stripped by Blender's GLTF exporter
|
## Replaces GLB animations that were stripped by Blender's GLTF exporter.
|
||||||
## (tracks exist but contain near-zero motion — model appears to T-pose).
|
## Uses AnimationPlayer tracks (type=0 for position, type=1 for rotation).
|
||||||
## Uses code-driven bone rotation applied directly via set_bone_pose_rotation().
|
|
||||||
|
|
||||||
# Always replace all locomotion animations — GLB is known to be stripped
|
# Always replace all locomotion animations — GLB is known to be stripped
|
||||||
# by Blender's GLTF exporter (all tracks have near-zero motion)
|
_create_and_play_anim(ap, "Walk", _create_walk_anim())
|
||||||
var broken_walk := true
|
_create_and_play_anim(ap, "Run", _create_run_anim())
|
||||||
var broken_run := true
|
_create_and_play_anim(ap, "Jump", _create_jump_anim())
|
||||||
var broken_jump := true
|
_create_and_play_anim(ap, "Idle", _create_idle_anim())
|
||||||
var broken_idle := true
|
|
||||||
|
|
||||||
if broken_walk:
|
func _create_and_play_anim(ap: AnimationPlayer, name: String, anim: Animation) -> void:
|
||||||
print("SkinnedPlayerModel: Walk animation has near-zero motion — replacing with procedural")
|
var lib = ap.get_animation_library("")
|
||||||
_procedural_anims["Walk"] = _create_locomotion_data(true)
|
if lib.has_animation(name):
|
||||||
_add_animation_to_player(ap, "Walk", Animation.new()) # placeholder for AP list
|
lib.remove_animation(name)
|
||||||
|
lib.add_animation(name, anim)
|
||||||
|
|
||||||
if broken_run:
|
func _create_walk_anim() -> Animation:
|
||||||
print("SkinnedPlayerModel: Run animation has near-zero motion — replacing with procedural")
|
var anim = Animation.new()
|
||||||
_procedural_anims["Run"] = _create_locomotion_data(true, 1.8)
|
anim.length = 1.0
|
||||||
_add_animation_to_player(ap, "Run", Animation.new())
|
anim.loop_mode = Animation.LOOP_LINEAR
|
||||||
|
var cycle = 1.0 / locomotion_cycle_speed
|
||||||
|
anim.length = cycle
|
||||||
|
|
||||||
if broken_jump:
|
# Add rotation tracks for each bone using type=1 (ROTATION)
|
||||||
print("SkinnedPlayerModel: Jump animation has near-zero motion — replacing with procedural")
|
_add_rot_track(anim, "LeftUpperLeg", cycle, [
|
||||||
_procedural_anims["Jump"] = _create_jump_data()
|
{"t": 0.0, "q": Quaternion.from_euler(Vector3(-0.6, 0, 0))},
|
||||||
_add_animation_to_player(ap, "Jump", Animation.new())
|
{"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:
|
func _create_run_anim() -> Animation:
|
||||||
print("SkinnedPlayerModel: Idle animation has near-zero motion — replacing with procedural")
|
var anim = _create_walk_anim()
|
||||||
_procedural_anims["Idle"] = _create_idle_data()
|
anim.length = 0.5 # faster
|
||||||
_add_animation_to_player(ap, "Idle", Animation.new())
|
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:
|
func _is_animation_broken(ap: AnimationPlayer, anim_name: String) -> bool:
|
||||||
## Returns true if the animation has near-zero motion (Blender GLTF track stripping).
|
## 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)
|
print("SkinnedPlayerModel: WARNING - '%s' not available (using fallback)" % anim_name)
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
if not skeleton:
|
if not animation_player:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get movement state from parent PlayerMovementController
|
# Get movement state from parent PlayerMovementController
|
||||||
@@ -503,101 +571,14 @@ func _process(delta: float) -> void:
|
|||||||
_:
|
_:
|
||||||
target_anim = "Idle"
|
target_anim = "Idle"
|
||||||
|
|
||||||
# Switch animation if needed
|
if animation_player.current_animation != target_anim or not animation_player.is_playing():
|
||||||
if target_anim != _current_anim_name:
|
if animation_player.has_animation(target_anim):
|
||||||
_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
|
|
||||||
animation_player.play(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
|
# Debug output
|
||||||
_anim_debug_timer += delta
|
_anim_debug_timer += delta
|
||||||
if _anim_debug_timer > 2.0:
|
if _anim_debug_timer > 2.0:
|
||||||
_anim_debug_timer = 0.0
|
_anim_debug_timer = 0.0
|
||||||
if animation_player.is_playing():
|
if animation_player.is_playing():
|
||||||
print("SkinnedPlayerModel: playing '%s' (pos: %.2f)" % [animation_player.current_animation, animation_player.current_animation_position])
|
print("SkinnedPlayerModel: playing '%s' (pos: %.2f)" % [animation_player.current_animation, animation_player.current_animation_position])
|
||||||
|
pass # end of _process
|
||||||
# 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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user