Feat/14 movement overhaul #20

Merged
Dotts merged 86 commits from feat/14-movement-overhaul into main 2026-07-17 10:44:23 -07:00
Showing only changes of commit f776fe59e7 - Show all commits
+52 -228
View File
@@ -168,31 +168,31 @@ func _create_walk_anim() -> Animation:
# Add rotation tracks for each bone using type=1 (ROTATION) # Add rotation tracks for each bone using type=1 (ROTATION)
_add_rot_track(anim, "LeftUpperLeg", [ _add_rot_track(anim, "LeftUpperLeg", [
{"t": 0.0, "euler": Vector3(-0.6, 0, 0)}, {"t": 0.0, "rot": Vector3(-0.6, 0, 0)},
{"t": cycle * 0.5, "euler": Vector3(0.6, 0, 0)}, {"t": cycle * 0.5, "rot": Vector3(0.6, 0, 0)},
{"t": cycle, "euler": Vector3(-0.6, 0, 0)} {"t": cycle, "rot": Vector3(-0.6, 0, 0)}
]) ])
_add_rot_track(anim, "RightUpperLeg", [ _add_rot_track(anim, "RightUpperLeg", [
{"t": 0.0, "euler": Vector3(0.6, 0, 0)}, {"t": 0.0, "rot": Vector3(0.6, 0, 0)},
{"t": cycle * 0.5, "euler": Vector3(-0.6, 0, 0)}, {"t": cycle * 0.5, "rot": Vector3(-0.6, 0, 0)},
{"t": cycle, "euler": Vector3(0.6, 0, 0)} {"t": cycle, "rot": Vector3(0.6, 0, 0)}
]) ])
_add_rot_track(anim, "LeftUpperArm", [ _add_rot_track(anim, "LeftUpperArm", [
{"t": 0.0, "euler": Vector3(-0.4, 0, 0)}, {"t": 0.0, "rot": Vector3(-0.4, 0, 0)},
{"t": cycle * 0.5, "euler": Vector3(0.4, 0, 0)}, {"t": cycle * 0.5, "rot": Vector3(0.4, 0, 0)},
{"t": cycle, "euler": Vector3(-0.4, 0, 0)} {"t": cycle, "rot": Vector3(-0.4, 0, 0)}
]) ])
_add_rot_track(anim, "RightUpperArm", [ _add_rot_track(anim, "RightUpperArm", [
{"t": 0.0, "euler": Vector3(0.4, 0, 0)}, {"t": 0.0, "rot": Vector3(0.4, 0, 0)},
{"t": cycle * 0.5, "euler": Vector3(-0.4, 0, 0)}, {"t": cycle * 0.5, "rot": Vector3(-0.4, 0, 0)},
{"t": cycle, "euler": Vector3(0.4, 0, 0)} {"t": cycle, "rot": Vector3(0.4, 0, 0)}
]) ])
_add_rot_track(anim, "Spine", [ _add_rot_track(anim, "Spine", [
{"t": 0.0, "euler": Vector3(0, 0, 0)}, {"t": 0.0, "rot": Vector3(0, 0, 0)},
{"t": cycle * 0.25, "euler": Vector3(0, 0.1, 0)}, {"t": cycle * 0.25, "rot": Vector3(0, 0.1, 0)},
{"t": cycle * 0.5, "euler": Vector3(0, 0, 0)}, {"t": cycle * 0.5, "rot": Vector3(0, 0, 0)},
{"t": cycle * 0.75, "euler": Vector3(0, -0.1, 0)}, {"t": cycle * 0.75, "rot": Vector3(0, -0.1, 0)},
{"t": cycle, "euler": Vector3(0, 0, 0)} {"t": cycle, "rot": Vector3(0, 0, 0)}
]) ])
return anim return anim
@@ -207,24 +207,24 @@ func _create_jump_anim() -> Animation:
anim.loop_mode = Animation.LOOP_LINEAR anim.loop_mode = Animation.LOOP_LINEAR
_add_rot_track(anim, "LeftUpperArm", [ _add_rot_track(anim, "LeftUpperArm", [
{"t": 0.0, "euler": Vector3(0, 0, 0)}, {"t": 0.0, "rot": Vector3(0, 0, 0)},
{"t": 0.5, "euler": Vector3(-1.2, 0, 0)}, {"t": 0.5, "rot": Vector3(-1.2, 0, 0)},
{"t": 1.0, "euler": Vector3(0, 0, 0)} {"t": 1.0, "rot": Vector3(0, 0, 0)}
]) ])
_add_rot_track(anim, "RightUpperArm", [ _add_rot_track(anim, "RightUpperArm", [
{"t": 0.0, "euler": Vector3(0, 0, 0)}, {"t": 0.0, "rot": Vector3(0, 0, 0)},
{"t": 0.5, "euler": Vector3(-1.2, 0, 0)}, {"t": 0.5, "rot": Vector3(-1.2, 0, 0)},
{"t": 1.0, "euler": Vector3(0, 0, 0)} {"t": 1.0, "rot": Vector3(0, 0, 0)}
]) ])
_add_rot_track(anim, "LeftUpperLeg", [ _add_rot_track(anim, "LeftUpperLeg", [
{"t": 0.0, "euler": Vector3(0, 0, 0)}, {"t": 0.0, "rot": Vector3(0, 0, 0)},
{"t": 0.5, "euler": Vector3(-0.3, 0, 0)}, {"t": 0.5, "rot": Vector3(-0.3, 0, 0)},
{"t": 1.0, "euler": Vector3(0, 0, 0)} {"t": 1.0, "rot": Vector3(0, 0, 0)}
]) ])
_add_rot_track(anim, "RightUpperLeg", [ _add_rot_track(anim, "RightUpperLeg", [
{"t": 0.0, "euler": Vector3(0, 0, 0)}, {"t": 0.0, "rot": Vector3(0, 0, 0)},
{"t": 0.5, "euler": Vector3(-0.3, 0, 0)}, {"t": 0.5, "rot": Vector3(-0.3, 0, 0)},
{"t": 1.0, "euler": Vector3(0, 0, 0)} {"t": 1.0, "rot": Vector3(0, 0, 0)}
]) ])
return anim return anim
@@ -234,34 +234,35 @@ func _create_idle_anim() -> Animation:
anim.loop_mode = Animation.LOOP_LINEAR anim.loop_mode = Animation.LOOP_LINEAR
_add_rot_track(anim, "Spine", [ _add_rot_track(anim, "Spine", [
{"t": 0.0, "euler": Vector3(0, 0, 0)}, {"t": 0.0, "rot": Vector3(0, 0, 0)},
{"t": 1.0, "euler": Vector3(0.02, 0, 0)}, {"t": 1.0, "rot": Vector3(0.02, 0, 0)},
{"t": 2.0, "euler": Vector3(0, 0, 0)} {"t": 2.0, "rot": Vector3(0, 0, 0)}
]) ])
_add_rot_track(anim, "Head", [ _add_rot_track(anim, "Head", [
{"t": 0.0, "euler": Vector3(0, 0, 0)}, {"t": 0.0, "rot": Vector3(0, 0, 0)},
{"t": 1.0, "euler": Vector3(0.03, 0.02, 0)}, {"t": 1.0, "rot": Vector3(0.03, 0.02, 0)},
{"t": 2.0, "euler": Vector3(0, 0, 0)} {"t": 2.0, "rot": Vector3(0, 0, 0)}
]) ])
_add_rot_track(anim, "LeftUpperArm", [ _add_rot_track(anim, "LeftUpperArm", [
{"t": 0.0, "euler": Vector3(0, 0, 0)}, {"t": 0.0, "rot": Vector3(0, 0, 0)},
{"t": 1.0, "euler": Vector3(0.02, 0, 0.05)}, {"t": 1.0, "rot": Vector3(0.02, 0, 0.05)},
{"t": 2.0, "euler": Vector3(0, 0, 0)} {"t": 2.0, "rot": Vector3(0, 0, 0)}
]) ])
_add_rot_track(anim, "RightUpperArm", [ _add_rot_track(anim, "RightUpperArm", [
{"t": 0.0, "euler": Vector3(0, 0, 0)}, {"t": 0.0, "rot": Vector3(0, 0, 0)},
{"t": 1.0, "euler": Vector3(0.02, 0, -0.05)}, {"t": 1.0, "rot": Vector3(0.02, 0, -0.05)},
{"t": 2.0, "euler": Vector3(0, 0, 0)} {"t": 2.0, "rot": Vector3(0, 0, 0)}
]) ])
return anim return anim
func _add_rot_track(anim: Animation, bone_name: String, keyframes: Array) -> void: func _add_rot_track(anim: Animation, bone_name: String, keyframes: Array) -> void:
var track = anim.add_track(0) # TYPE_VALUE = 0 (required in Godot 4.2.1) var track = anim.add_track(Animation.TYPE_ROTATION_3D) # TYPE_ROTATION_3D = 2
anim.track_set_path(track, "MikuRig/Skeleton3D:" + bone_name) anim.track_set_path(track, "MikuRig/Skeleton3D:" + bone_name)
anim.track_set_interpolation_type(track, Animation.INTERPOLATION_LINEAR) anim.track_set_interpolation_type(track, Animation.INTERPOLATION_LINEAR)
for kf in keyframes: for kf in keyframes:
# Use Vector3 euler angles - Godot 4.2.1 accepts both Vector3 and Quaternion # Rotation tracks require Quaternion values
anim.track_insert_key(track, kf["t"], kf["euler"]) var quat = Quaternion.from_euler(kf["rot"])
anim.track_insert_key(track, kf["t"], quat)
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).
@@ -277,21 +278,15 @@ func _is_animation_broken(ap: AnimationPlayer, anim_name: String) -> bool:
var key_count = anim.track_get_key_count(t) var key_count = anim.track_get_key_count(t)
if key_count < 2: if key_count < 2:
continue continue
var path = String(anim.track_get_path(t))
var is_rotation_track = path.ends_with(":rotation")
for i in range(1, key_count): for i in range(1, key_count):
var v = anim.track_get_key_value(t, i) var v = anim.track_get_key_value(t, i)
if v is Vector3: var prev = anim.track_get_key_value(t, i - 1)
var prev_v = anim.track_get_key_value(t, i - 1) if v is Vector3 and prev is Vector3:
var delta: float max_delta = max(max_delta, v.distance_to(prev))
if is_rotation_track: elif v is Quaternion and prev is Quaternion:
# For rotation tracks, sum absolute euler angle differences max_delta = max(max_delta, v.angle_to(prev))
delta = abs(v.x - prev_v.x) + abs(v.y - prev_v.y) + abs(v.z - prev_v.z) # Threshold: < 0.005 units of motion
else: return max_delta < 0.005
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: func _get_bone_name(bone_name: String) -> String:
## Returns the bone name as stored in the skeleton (uses cache). ## Returns the bone name as stored in the skeleton (uses cache).
@@ -299,177 +294,6 @@ func _get_bone_name(bone_name: String) -> String:
return bone_name return bone_name
return bone_name return bone_name
# ═══════════════════════════════════════════════════════════════════════════════
# CODE-DRIVEN ANIMATION SYSTEM
# Godot 4.2.1 AnimationPlayer cannot rotate bones via ":rotation" track paths
# on runtime-loaded GLB skeletons. We apply rotations directly each frame.
# ═══════════════════════════════════════════════════════════════════════════════
func _create_locomotion_data(loop: bool, speed_mult: float = 1.0) -> Dictionary:
## Creates a walk/run animation data structure.
## Returns: {length, loop, tracks: [{bone, keyframes: [{t, rot: Vector3}]}]}
var cycle: float = 1.0 / locomotion_cycle_speed * speed_mult
var t1: float = 0.0
var t2: float = cycle * 0.25
var t3: float = cycle * 0.5
var t4: float = cycle * 0.75
var t5: float = cycle
var tracks: Array = []
# Legs: opposite phase walk cycle
_add_catmull_rot_track(tracks, "LeftUpperLeg", [t1, t2, t3, t4, t5],
[-0.6, -0.1, 0.6, -0.1, -0.6], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
_add_catmull_rot_track(tracks, "LeftLowerLeg", [t1, t2, t3, t4, t5],
[0.1, 0.5, 0.8, 0.3, 0.1], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
_add_catmull_rot_track(tracks, "RightUpperLeg", [t1, t2, t3, t4, t5],
[0.6, -0.1, -0.6, -0.1, 0.6], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
_add_catmull_rot_track(tracks, "RightLowerLeg", [t1, t2, t3, t4, t5],
[0.3, 0.1, 0.1, 0.5, 0.3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
# Arms: opposite to legs
_add_catmull_rot_track(tracks, "LeftUpperArm", [t1, t2, t3, t4, t5],
[0.5, 0.0, -0.5, 0.0, 0.5], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
_add_catmull_rot_track(tracks, "LeftLowerArm", [t1, t2, t3, t4, t5],
[-0.8, -0.3, -0.2, -0.5, -0.8], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
_add_catmull_rot_track(tracks, "RightUpperArm", [t1, t2, t3, t4, t5],
[-0.5, 0.0, 0.5, 0.0, -0.5], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
_add_catmull_rot_track(tracks, "RightLowerArm", [t1, t2, t3, t4, t5],
[-0.2, -0.5, -0.8, -0.3, -0.2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
# Spine twist
_add_catmull_rot_track(tracks, "Spine", [t1, t2, t3, t4, t5],
[0.0, 0.05, 0.0, -0.05, 0.0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
# Hips bounce (position offset)
_add_catmull_pos_track(tracks, "Hips", [t1, t2, t3, t4, t5],
[0, 0, 0, 0, 0], [0, 0.02, 0, 0.02, 0], [0, 0, 0, 0, 0])
return {"length": cycle, "loop": loop, "tracks": tracks}
func _create_jump_data() -> Dictionary:
## Creates jump animation data.
var tracks: Array = []
var t1: float = 0.0
var t2: float = 0.5
var t3: float = 1.0
_add_keyframe_rot_track(tracks, "LeftUpperArm", [
{"t": t1, "rot": Vector3(-0.8, 0, 0.3)},
{"t": t2, "rot": Vector3(-1.2, 0, 0.5)},
{"t": t3, "rot": Vector3(-0.8, 0, 0.3)}
])
_add_keyframe_rot_track(tracks, "RightUpperArm", [
{"t": t1, "rot": Vector3(-0.8, 0, -0.3)},
{"t": t2, "rot": Vector3(-1.2, 0, -0.5)},
{"t": t3, "rot": Vector3(-0.8, 0, -0.3)}
])
_add_keyframe_rot_track(tracks, "LeftLowerArm", [
{"t": t1, "rot": Vector3(0, 0, 0)},
{"t": t2, "rot": Vector3(-0.4, 0, 0)},
{"t": t3, "rot": Vector3(0, 0, 0)}
])
_add_keyframe_rot_track(tracks, "RightLowerArm", [
{"t": t1, "rot": Vector3(0, 0, 0)},
{"t": t2, "rot": Vector3(-0.4, 0, 0)},
{"t": t3, "rot": Vector3(0, 0, 0)}
])
_add_keyframe_rot_track(tracks, "LeftUpperLeg", [
{"t": t1, "rot": Vector3(0, 0, 0)},
{"t": t2, "rot": Vector3(-0.3, 0, 0)},
{"t": t3, "rot": Vector3(0, 0, 0)}
])
_add_keyframe_rot_track(tracks, "RightUpperLeg", [
{"t": t1, "rot": Vector3(0, 0, 0)},
{"t": t2, "rot": Vector3(-0.3, 0, 0)},
{"t": t3, "rot": Vector3(0, 0, 0)}
])
_add_keyframe_rot_track(tracks, "Spine", [
{"t": t1, "rot": Vector3(0, 0, 0)},
{"t": t2, "rot": Vector3(0.1, 0, 0)},
{"t": t3, "rot": Vector3(0, 0, 0)}
])
return {"length": 1.0, "loop": true, "tracks": tracks}
func _create_idle_data() -> Dictionary:
## Creates idle animation data.
var tracks: Array = []
var t1: float = 0.0
var t2: float = 1.0
var t3: float = 2.0
_add_keyframe_rot_track(tracks, "Spine", [
{"t": t1, "rot": Vector3(0, 0, 0)},
{"t": t2, "rot": Vector3(0.02, 0, 0)},
{"t": t3, "rot": Vector3(0, 0, 0)}
])
_add_keyframe_rot_track(tracks, "Head", [
{"t": t1, "rot": Vector3(0, 0, 0)},
{"t": t2, "rot": Vector3(0.03, 0.02, 0)},
{"t": t3, "rot": Vector3(0, 0, 0)}
])
_add_keyframe_rot_track(tracks, "LeftUpperArm", [
{"t": t1, "rot": Vector3(0, 0, 0.05)},
{"t": t2, "rot": Vector3(0.02, 0, 0.08)},
{"t": t3, "rot": Vector3(0, 0, 0.05)}
])
_add_keyframe_rot_track(tracks, "RightUpperArm", [
{"t": t1, "rot": Vector3(0, 0, -0.05)},
{"t": t2, "rot": Vector3(0.02, 0, -0.08)},
{"t": t3, "rot": Vector3(0, 0, -0.05)}
])
_add_keyframe_pos_track(tracks, "Hips", [
{"t": t1, "pos": Vector3(0, 0, 0)},
{"t": t2, "pos": Vector3(0, 0.005, 0)},
{"t": t3, "pos": Vector3(0, 0, 0)}
])
return {"length": 2.0, "loop": true, "tracks": tracks}
func _add_catmull_rot_track(tracks: Array, bone_name: String, times: Array, x: Array, y: Array, z: Array) -> void:
var kf: Array = []
for i in range(times.size()):
kf.append({"t": times[i], "rot": Vector3(x[i], y[i], z[i])})
tracks.append({"bone": bone_name, "keyframes": kf})
func _add_catmull_pos_track(tracks: Array, bone_name: String, times: Array, x: Array, y: Array, z: Array) -> void:
var kf: Array = []
for i in range(times.size()):
kf.append({"t": times[i], "pos": Vector3(x[i], y[i], z[i])})
tracks.append({"bone": bone_name, "keyframes": kf})
func _add_keyframe_rot_track(tracks: Array, bone_name: String, keyframes: Array) -> void:
var kf: Array = []
for k in keyframes:
kf.append({"t": k["t"], "rot": k["rot"]})
tracks.append({"bone": bone_name, "keyframes": kf})
func _add_keyframe_pos_track(tracks: Array, bone_name: String, keyframes: Array) -> void:
var kf: Array = []
for k in keyframes:
kf.append({"t": k["t"], "pos": k["pos"]})
tracks.append({"bone": bone_name, "keyframes": kf})
func _add_animation_to_player(ap: AnimationPlayer, anim_name: String, anim: Animation) -> void:
## Adds an animation to the AnimationPlayer via its default library.
var lib_name: String = ""
if not ap.has_animation_library(lib_name):
ap.add_animation_library(lib_name, AnimationLibrary.new())
var lib = ap.get_animation_library(lib_name)
lib.add_animation(anim_name, anim)
print("SkinnedPlayerModel: added '%s' (%d tracks)" % [anim_name, anim.get_track_count()])
func _update_skin() -> void:
## Force skeleton skin update by recomputing all bone transforms.
## GLTF runtime loaded models need this because AnimationPlayer's internal
## bone pose updates don't always trigger the visual pipeline's dirty flags.
## force_update_all_bone_transforms() recomputes the full bone hierarchy
## and emits bone_pose_changed, which causes MeshInstance to update vertex buffers.
skeleton.force_update_all_bone_transforms()
func _setup_first_person() -> void: func _setup_first_person() -> void:
# In first person, hide the head and upper body so only arms/hands/legs show # In first person, hide the head and upper body so only arms/hands/legs show
# This creates the classic FPS arms-only view # This creates the classic FPS arms-only view