|
|
@@ -5,13 +5,19 @@ class_name SkinnedPlayerModel
|
|
|
|
## Replaces the procedural HumanoidModel for characters with custom skins.
|
|
|
|
## Replaces the procedural HumanoidModel for characters with custom skins.
|
|
|
|
|
|
|
|
|
|
|
|
@export var model_path: String = ""
|
|
|
|
@export var model_path: String = ""
|
|
|
|
|
|
|
|
## Scale factor applied to the loaded scene. For the Miku GLB, 1.0 is correct (~1.6m).
|
|
|
|
|
|
|
|
## For models exported in centimeters (Mixamo/Blender default), set to 0.01.
|
|
|
|
@export var scale_factor: float = 1.0
|
|
|
|
@export var scale_factor: float = 1.0
|
|
|
|
@export var first_person_mode: bool = false # Hide head/torso for FPS view
|
|
|
|
@export var first_person_mode: bool = false # Hide head/torso for FPS view
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Bones per second for locomotion animations created at runtime.
|
|
|
|
|
|
|
|
@export var locomotion_cycle_speed: float = 4.0
|
|
|
|
|
|
|
|
|
|
|
|
var skeleton: Skeleton3D
|
|
|
|
var skeleton: Skeleton3D
|
|
|
|
var animation_player: AnimationPlayer
|
|
|
|
var animation_player: AnimationPlayer
|
|
|
|
var _anim_debug_timer: float = 0.0
|
|
|
|
var _anim_debug_timer: float = 0.0
|
|
|
|
var _mesh_instance: MeshInstance3D
|
|
|
|
var _mesh_instance: MeshInstance3D
|
|
|
|
|
|
|
|
var _bone_cache: Dictionary = {} # bone_name -> index
|
|
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
func _ready() -> void:
|
|
|
|
if model_path != "":
|
|
|
|
if model_path != "":
|
|
|
@@ -20,6 +26,7 @@ func _ready() -> void:
|
|
|
|
func load_model(path: String) -> void:
|
|
|
|
func load_model(path: String) -> void:
|
|
|
|
for child in get_children():
|
|
|
|
for child in get_children():
|
|
|
|
child.queue_free()
|
|
|
|
child.queue_free()
|
|
|
|
|
|
|
|
_bone_cache.clear()
|
|
|
|
|
|
|
|
|
|
|
|
print("SkinnedPlayerModel: loading %s via GLTFDocument" % path)
|
|
|
|
print("SkinnedPlayerModel: loading %s via GLTFDocument" % path)
|
|
|
|
|
|
|
|
|
|
|
@@ -49,9 +56,10 @@ func load_model(path: String) -> void:
|
|
|
|
add_child(scene)
|
|
|
|
add_child(scene)
|
|
|
|
print("SkinnedPlayerModel: scene added: %s" % scene.name)
|
|
|
|
print("SkinnedPlayerModel: scene added: %s" % scene.name)
|
|
|
|
|
|
|
|
|
|
|
|
if scale_factor != 1.0:
|
|
|
|
# Model scale — the Miku GLB is already exported at meter scale (~1.6m tall).
|
|
|
|
|
|
|
|
# For models exported in centimeters (common from Mixamo/Blender), set scale_factor to 0.01.
|
|
|
|
scene.scale = Vector3(scale_factor, scale_factor, scale_factor)
|
|
|
|
scene.scale = Vector3(scale_factor, scale_factor, scale_factor)
|
|
|
|
print("SkinnedPlayerModel: applied scale %.2f" % scale_factor)
|
|
|
|
print("SkinnedPlayerModel: applied scale %.4f" % scale_factor)
|
|
|
|
|
|
|
|
|
|
|
|
# Find mesh instance for first-person mode
|
|
|
|
# Find mesh instance for first-person mode
|
|
|
|
_mesh_instance = scene.find_child("Tda Miku for fbx_mesh", true, false)
|
|
|
|
_mesh_instance = scene.find_child("Tda Miku for fbx_mesh", true, false)
|
|
|
@@ -71,6 +79,9 @@ 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
|
|
|
|
|
|
|
|
for i in range(skeleton.get_bone_count()):
|
|
|
|
|
|
|
|
_bone_cache[skeleton.get_bone_name(i)] = i
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
print("SkinnedPlayerModel: WARNING - no skeleton found")
|
|
|
|
print("SkinnedPlayerModel: WARNING - no skeleton found")
|
|
|
|
|
|
|
|
|
|
|
@@ -93,73 +104,187 @@ func load_model(path: String) -> void:
|
|
|
|
animation_player.play(anim_list[0])
|
|
|
|
animation_player.play(anim_list[0])
|
|
|
|
print("SkinnedPlayerModel: playing '%s'" % anim_list[0])
|
|
|
|
print("SkinnedPlayerModel: playing '%s'" % anim_list[0])
|
|
|
|
|
|
|
|
|
|
|
|
# If the loaded animations are too subtle (only 1 track), create better ones
|
|
|
|
# Ensure fallback animations exist for states the movement system needs
|
|
|
|
if animation_player.has_animation("Idle"):
|
|
|
|
_ensure_locomotion_animations(animation_player)
|
|
|
|
var idle_anim = animation_player.get_animation("Idle")
|
|
|
|
|
|
|
|
if idle_anim.get_track_count() <= 1:
|
|
|
|
|
|
|
|
print("SkinnedPlayerModel: Idle has too few tracks, creating a proper one")
|
|
|
|
|
|
|
|
_create_idle_animation(animation_player)
|
|
|
|
|
|
|
|
animation_player.play("Idle")
|
|
|
|
|
|
|
|
print("SkinnedPlayerModel: playing newly created Idle")
|
|
|
|
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
|
|
|
|
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
|
|
|
|
|
|
|
|
|
|
|
|
func _create_idle_animation(ap: AnimationPlayer) -> void:
|
|
|
|
func _ensure_locomotion_animations(ap: AnimationPlayer) -> void:
|
|
|
|
# Create a proper idle animation with visible movement
|
|
|
|
## 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).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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 head: String = _get_bone_name("Head")
|
|
|
|
|
|
|
|
var left_upper_arm: String = _get_bone_name("LeftUpperArm")
|
|
|
|
|
|
|
|
var left_lower_arm: String = _get_bone_name("LeftLowerArm")
|
|
|
|
|
|
|
|
var left_hand: String = _get_bone_name("LeftHand")
|
|
|
|
|
|
|
|
var right_upper_arm: String = _get_bone_name("RightUpperArm")
|
|
|
|
|
|
|
|
var right_lower_arm: String = _get_bone_name("RightLowerArm")
|
|
|
|
|
|
|
|
var right_hand: String = _get_bone_name("RightHand")
|
|
|
|
|
|
|
|
var left_upper_leg: String = _get_bone_name("LeftUpperLeg")
|
|
|
|
|
|
|
|
var left_lower_leg: String = _get_bone_name("LeftLowerLeg")
|
|
|
|
|
|
|
|
var left_foot: String = _get_bone_name("LeftFoot")
|
|
|
|
|
|
|
|
var right_upper_leg: String = _get_bone_name("RightUpperLeg")
|
|
|
|
|
|
|
|
var right_lower_leg: String = _get_bone_name("RightLowerLeg")
|
|
|
|
|
|
|
|
var right_foot: String = _get_bone_name("RightFoot")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Use "MikuRig/Skeleton3D:" prefix — AnimationPlayer is sibling of MikuRig,
|
|
|
|
|
|
|
|
# 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")
|
|
|
|
|
|
|
|
var anim = _create_locomotion_anim(
|
|
|
|
|
|
|
|
prefix, true,
|
|
|
|
|
|
|
|
left_upper_leg, left_lower_leg, left_foot,
|
|
|
|
|
|
|
|
right_upper_leg, right_lower_leg, right_foot,
|
|
|
|
|
|
|
|
left_upper_arm, left_lower_arm,
|
|
|
|
|
|
|
|
right_upper_arm, right_lower_arm,
|
|
|
|
|
|
|
|
spine, hips
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
_add_animation_to_player(ap, "Walk", anim)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not ap.has_animation("Run"):
|
|
|
|
|
|
|
|
print("SkinnedPlayerModel: creating Run animation")
|
|
|
|
|
|
|
|
var anim = _create_locomotion_anim(
|
|
|
|
|
|
|
|
prefix, true,
|
|
|
|
|
|
|
|
left_upper_leg, left_lower_leg, left_foot,
|
|
|
|
|
|
|
|
right_upper_leg, right_lower_leg, right_foot,
|
|
|
|
|
|
|
|
left_upper_arm, left_lower_arm,
|
|
|
|
|
|
|
|
right_upper_arm, right_lower_arm,
|
|
|
|
|
|
|
|
spine, hips, 1.8 # faster cycle for run
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
_add_animation_to_player(ap, "Run", anim)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not ap.has_animation("Jump"):
|
|
|
|
|
|
|
|
print("SkinnedPlayerModel: creating Jump animation")
|
|
|
|
|
|
|
|
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_animation_to_player(ap, "Jump", anim)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not ap.has_animation("Idle"):
|
|
|
|
|
|
|
|
print("SkinnedPlayerModel: creating Idle animation")
|
|
|
|
var anim = Animation.new()
|
|
|
|
var anim = Animation.new()
|
|
|
|
anim.length = 2.0
|
|
|
|
anim.length = 2.0
|
|
|
|
anim.loop_mode = Animation.LOOP_LINEAR
|
|
|
|
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)
|
|
|
|
|
|
|
|
_add_animation_to_player(ap, "Idle", anim)
|
|
|
|
|
|
|
|
|
|
|
|
# Sway the spine (position track)
|
|
|
|
func _get_bone_name(name: String) -> String:
|
|
|
|
_create_bone_track(anim, "MikuRig/Skeleton3D:Spine",
|
|
|
|
## Returns the bone name as stored in the skeleton (uses cache).
|
|
|
|
[
|
|
|
|
if _bone_cache.has(name):
|
|
|
|
{ "time": 0.0, "value": Vector3(0, 0.1, 0) },
|
|
|
|
return name
|
|
|
|
{ "time": 0.5, "value": Vector3(0, -0.1, 0) },
|
|
|
|
return name
|
|
|
|
{ "time": 1.0, "value": Vector3(0, 0.1, 0) },
|
|
|
|
|
|
|
|
{ "time": 1.5, "value": Vector3(0, -0.1, 0) },
|
|
|
|
|
|
|
|
{ "time": 2.0, "value": Vector3(0, 0.1, 0) },
|
|
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Sway left arm
|
|
|
|
func _create_locomotion_anim(
|
|
|
|
_create_bone_track(anim, "MikuRig/Skeleton3D:LeftUpperArm",
|
|
|
|
prefix: String,
|
|
|
|
[
|
|
|
|
loop: bool,
|
|
|
|
{ "time": 0.0, "value": Vector3(0, 0, 0.2) },
|
|
|
|
l_leg_upper: String, l_leg_lower: String, l_foot: String,
|
|
|
|
{ "time": 1.0, "value": Vector3(0, 0, 0.4) },
|
|
|
|
r_leg_upper: String, r_leg_lower: String, r_foot: String,
|
|
|
|
{ "time": 2.0, "value": Vector3(0, 0, 0.2) },
|
|
|
|
l_arm_upper: String, l_arm_lower: String,
|
|
|
|
])
|
|
|
|
r_arm_upper: String, r_arm_lower: String,
|
|
|
|
|
|
|
|
spine: String, hips: String,
|
|
|
|
|
|
|
|
speed_mult: float = 1.0
|
|
|
|
|
|
|
|
) -> Animation:
|
|
|
|
|
|
|
|
var anim = Animation.new()
|
|
|
|
|
|
|
|
var cycle: float = 1.0 / locomotion_cycle_speed * speed_mult
|
|
|
|
|
|
|
|
anim.length = cycle
|
|
|
|
|
|
|
|
anim.loop_mode = Animation.LOOP_LINEAR if loop else Animation.LOOP_NONE
|
|
|
|
|
|
|
|
|
|
|
|
# Sway right arm
|
|
|
|
var t1: float = 0.0
|
|
|
|
_create_bone_track(anim, "MikuRig/Skeleton3D:RightUpperArm",
|
|
|
|
var t2: float = cycle * 0.25
|
|
|
|
[
|
|
|
|
var t3: float = cycle * 0.5
|
|
|
|
{ "time": 0.0, "value": Vector3(0, 0, -0.2) },
|
|
|
|
var t4: float = cycle * 0.75
|
|
|
|
{ "time": 1.0, "value": Vector3(0, 0, -0.4) },
|
|
|
|
var t5: float = cycle
|
|
|
|
{ "time": 2.0, "value": Vector3(0, 0, -0.2) },
|
|
|
|
|
|
|
|
])
|
|
|
|
# Legs: opposite phase walk cycle
|
|
|
|
|
|
|
|
_add_bone_track_catmull(anim, prefix + l_leg_upper, [t1, t2, t3, t4, t5], [
|
|
|
|
|
|
|
|
-0.6, -0.1, 0.6, -0.1, -0.6 # swing forward/back
|
|
|
|
|
|
|
|
], [0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_add_bone_track_catmull(anim, prefix + l_leg_lower, [t1, t2, t3, t4, t5], [
|
|
|
|
|
|
|
|
0.1, 0.5, 0.8, 0.3, 0.1 # bend on back swing
|
|
|
|
|
|
|
|
], [0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_add_bone_track_catmull(anim, prefix + r_leg_upper, [t1, t2, t3, t4, t5], [
|
|
|
|
|
|
|
|
0.6, -0.1, -0.6, -0.1, 0.6 # opposite phase
|
|
|
|
|
|
|
|
], [0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_add_bone_track_catmull(anim, prefix + r_leg_lower, [t1, t2, t3, t4, t5], [
|
|
|
|
|
|
|
|
0.3, 0.1, 0.1, 0.5, 0.3 # bend on back swing
|
|
|
|
|
|
|
|
], [0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Arms: opposite to legs
|
|
|
|
|
|
|
|
_add_bone_track_catmull(anim, prefix + l_arm_upper, [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], [0.0, 0.0, 0.0, 0.0, 0.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_add_bone_track_catmull(anim, prefix + l_arm_lower, [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], [0.0, 0.0, 0.0, 0.0, 0.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_add_bone_track_catmull(anim, prefix + r_arm_upper, [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], [0.0, 0.0, 0.0, 0.0, 0.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_add_bone_track_catmull(anim, prefix + r_arm_lower, [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], [0.0, 0.0, 0.0, 0.0, 0.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Spine twist
|
|
|
|
|
|
|
|
_add_bone_track_catmull(anim, prefix + 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], [0.0, 0.0, 0.0, 0.0, 0.0])
|
|
|
|
|
|
|
|
|
|
|
|
# Hips bounce
|
|
|
|
# Hips bounce
|
|
|
|
_create_bone_track(anim, "MikuRig/Skeleton3D:Hips",
|
|
|
|
_add_bone_track_catmull(anim, prefix + hips, [t1, t2, t3, t4, t5], [
|
|
|
|
[
|
|
|
|
0.0, 0.0, 0.0, 0.0, 0.0
|
|
|
|
{ "time": 0.0, "value": Vector3(0, 0, 0) },
|
|
|
|
], [0.0, 0.02, 0.0, 0.02, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0])
|
|
|
|
{ "time": 0.5, "value": Vector3(0, 0.03, 0) },
|
|
|
|
|
|
|
|
{ "time": 1.0, "value": Vector3(0, 0, 0) },
|
|
|
|
|
|
|
|
{ "time": 1.5, "value": Vector3(0, 0.03, 0) },
|
|
|
|
|
|
|
|
{ "time": 2.0, "value": Vector3(0, 0, 0) },
|
|
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Add animation to the player via library
|
|
|
|
return anim
|
|
|
|
var lib = ap.get_animation_library("")
|
|
|
|
|
|
|
|
lib.add_animation("Idle", anim)
|
|
|
|
|
|
|
|
print("SkinnedPlayerModel: created Idle animation with %d tracks" % anim.get_track_count())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _create_bone_track(anim: Animation, bone_path: String, keyframes: Array) -> void:
|
|
|
|
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.
|
|
|
|
var track_idx = anim.add_track(Animation.TYPE_VALUE)
|
|
|
|
var track_idx = anim.add_track(Animation.TYPE_VALUE)
|
|
|
|
anim.track_set_path(track_idx, bone_path)
|
|
|
|
anim.track_set_path(track_idx, bone_path)
|
|
|
|
anim.track_set_interpolation_type(track_idx, Animation.INTERPOLATION_LINEAR)
|
|
|
|
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.
|
|
|
|
|
|
|
|
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 kf in keyframes:
|
|
|
|
for kf in keyframes:
|
|
|
|
var time = kf["time"]
|
|
|
|
anim.track_insert_key(track_idx, kf["time"], kf["value"])
|
|
|
|
var value = kf["value"]
|
|
|
|
|
|
|
|
anim.track_insert_key(track_idx, time, value)
|
|
|
|
func _add_animation_to_player(ap: AnimationPlayer, 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(name, anim)
|
|
|
|
|
|
|
|
print("SkinnedPlayerModel: added '%s' (%d tracks)" % [name, anim.get_track_count()])
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
@@ -204,26 +329,41 @@ func _find_animation_player(node: Node) -> AnimationPlayer:
|
|
|
|
return null
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
|
|
func play_animation(anim_name: String) -> void:
|
|
|
|
func play_animation(anim_name: String) -> void:
|
|
|
|
|
|
|
|
"""Play an animation by name. Silently skips if the animation doesn't exist."""
|
|
|
|
if animation_player and animation_player.has_animation(anim_name):
|
|
|
|
if animation_player and animation_player.has_animation(anim_name):
|
|
|
|
|
|
|
|
if animation_player.current_animation != anim_name or not animation_player.is_playing():
|
|
|
|
animation_player.play(anim_name)
|
|
|
|
animation_player.play(anim_name)
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
print("SkinnedPlayerModel: animation '%s' not found" % anim_name)
|
|
|
|
# Animation missing — already covered by _ensure_locomotion_animations()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var _last_played: String = ""
|
|
|
|
|
|
|
|
var _missing_anim_warned: Array = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _warn_missing(anim_name: String) -> void:
|
|
|
|
|
|
|
|
## Warn once per missing animation, not every frame.
|
|
|
|
|
|
|
|
if _missing_anim_warned.has(anim_name):
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
_missing_anim_warned.append(anim_name)
|
|
|
|
|
|
|
|
print("SkinnedPlayerModel: WARNING - '%s' not available (using fallback)" % anim_name)
|
|
|
|
|
|
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
|
|
func _process(delta: float) -> void:
|
|
|
|
if not animation_player:
|
|
|
|
if not animation_player:
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Debug output every 2 seconds
|
|
|
|
# Debug output every 2 seconds (rate-limited to avoid log spam)
|
|
|
|
_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])
|
|
|
|
else:
|
|
|
|
elif _last_played == "":
|
|
|
|
print("SkinnedPlayerModel: NOT playing")
|
|
|
|
print("SkinnedPlayerModel: NOT playing")
|
|
|
|
|
|
|
|
|
|
|
|
# Get movement state
|
|
|
|
# Get movement state from parent PlayerMovementController
|
|
|
|
var sm = get_parent().get_node_or_null("MovementStateMachine")
|
|
|
|
var parent = get_parent()
|
|
|
|
|
|
|
|
if not parent:
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
var sm = parent.get_node_or_null("MovementStateMachine")
|
|
|
|
if not sm:
|
|
|
|
if not sm:
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
@@ -234,7 +374,9 @@ func _process(delta: float) -> void:
|
|
|
|
speed = Vector2(vel.x, vel.z).length()
|
|
|
|
speed = Vector2(vel.x, vel.z).length()
|
|
|
|
|
|
|
|
|
|
|
|
var is_crouching = sm.input_crouch if sm else false
|
|
|
|
var is_crouching = sm.input_crouch if sm else false
|
|
|
|
var is_dead = get_parent().is_dead if get_parent() and get_parent().has_method("get") else false
|
|
|
|
var is_dead: bool = false
|
|
|
|
|
|
|
|
if parent and "is_dead" in parent:
|
|
|
|
|
|
|
|
is_dead = parent.is_dead
|
|
|
|
var target_anim = "Idle"
|
|
|
|
var target_anim = "Idle"
|
|
|
|
if is_dead:
|
|
|
|
if is_dead:
|
|
|
|
target_anim = "Death"
|
|
|
|
target_anim = "Death"
|
|
|
@@ -257,5 +399,19 @@ func _process(delta: float) -> void:
|
|
|
|
_:
|
|
|
|
_:
|
|
|
|
target_anim = "Idle"
|
|
|
|
target_anim = "Idle"
|
|
|
|
|
|
|
|
|
|
|
|
if animation_player.current_animation != target_anim or not animation_player.is_playing():
|
|
|
|
if target_anim != _last_played:
|
|
|
|
|
|
|
|
if animation_player.has_animation(target_anim):
|
|
|
|
play_animation(target_anim)
|
|
|
|
play_animation(target_anim)
|
|
|
|
|
|
|
|
_last_played = target_anim
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
_warn_missing(target_anim)
|
|
|
|
|
|
|
|
# Try fallback to Idle — should always exist
|
|
|
|
|
|
|
|
if animation_player.has_animation(target_anim.replace(target_anim, "Idle")):
|
|
|
|
|
|
|
|
play_animation("Idle")
|
|
|
|
|
|
|
|
_last_played = "Idle"
|
|
|
|
|
|
|
|
elif _last_played == "" or not animation_player.is_playing():
|
|
|
|
|
|
|
|
# Last resort: play first available animation
|
|
|
|
|
|
|
|
var anim_list = animation_player.get_animation_list()
|
|
|
|
|
|
|
|
if anim_list.size() > 0:
|
|
|
|
|
|
|
|
play_animation(anim_list[0])
|
|
|
|
|
|
|
|
_last_played = anim_list[0]
|
|
|
|