feat: add rigged+animated Miku player model with auto-rig pipeline
- Add miku_rigged_animated.glb (3.27MB) with 18-bone skeleton and 6 animations (Idle, Walk, Run, Jump, Crouch, Death) via Blender auto-rig script - Add tools/rig_and_animate.py: reusable Blender script for rigging any humanoid mesh with Mixamo-compatible bone naming - Update SkinnedPlayerModel: scale_factor, first-person mode, animation state matching (Idle/Walk/Run/Jump/Crouch/Death) - Update level_runtime.gd: use skinned model for local player, procedural humanoid for remote players - Update skin_manager.gd and test_level_builder.gd to use animated model - Fix Godot 4.2.1 'is not Type' syntax in 5 weapon files - Add editor/import_miku_to_tscn.gd for editor-based GLB import
This commit is contained in:
@@ -39,7 +39,7 @@ func _register_default_skins() -> void:
|
||||
var miku_skin = PlayerSkin.new()
|
||||
miku_skin.skin_name = "Miku"
|
||||
miku_skin.description = "Hatsune Miku — Virtual Idol"
|
||||
miku_skin.model_path = "res://assets/characters/skins/miku.glb"
|
||||
miku_skin.model_path = "res://assets/characters/skins/miku_rigged_animated.glb"
|
||||
miku_skin.color_tint = Color(0.0, 0.75, 0.75)
|
||||
miku_skin.is_unlocked = true
|
||||
skins["miku"] = miku_skin
|
||||
|
||||
@@ -5,10 +5,13 @@ class_name SkinnedPlayerModel
|
||||
## Replaces the procedural HumanoidModel for characters with custom skins.
|
||||
|
||||
@export var model_path: String = ""
|
||||
@export var scale_factor: float = 1.0
|
||||
@export var first_person_mode: bool = false # Hide head/torso for FPS view
|
||||
|
||||
var skeleton: Skeleton3D
|
||||
var animation_player: AnimationPlayer
|
||||
var _anim_debug_timer: float = 0.0
|
||||
var _mesh_instance: MeshInstance3D
|
||||
|
||||
func _ready() -> void:
|
||||
if model_path != "":
|
||||
@@ -17,58 +20,67 @@ func _ready() -> void:
|
||||
func load_model(path: String) -> void:
|
||||
for child in get_children():
|
||||
child.queue_free()
|
||||
|
||||
|
||||
var loaded = load(path)
|
||||
if not loaded:
|
||||
print("SkinnedPlayerModel: failed to load %s" % path)
|
||||
return
|
||||
|
||||
|
||||
var scene = loaded.instantiate()
|
||||
if not scene:
|
||||
print("SkinnedPlayerModel: failed to instantiate %s" % path)
|
||||
return
|
||||
|
||||
|
||||
add_child(scene)
|
||||
|
||||
# Debug: print the full scene tree
|
||||
|
||||
if scale_factor != 1.0:
|
||||
scene.scale = Vector3(scale_factor, scale_factor, scale_factor)
|
||||
print("SkinnedPlayerModel: applied scale %.2f" % scale_factor)
|
||||
|
||||
# Find mesh instance for first-person mode
|
||||
_mesh_instance = scene.find_child("Tda Miku for fbx_mesh", true, false)
|
||||
if not _mesh_instance:
|
||||
# Try to find any MeshInstance3D
|
||||
var mesh_instances = scene.find_children("*", "MeshInstance3D", true, false)
|
||||
if mesh_instances.size() > 0:
|
||||
_mesh_instance = mesh_instances[0]
|
||||
|
||||
if first_person_mode and _mesh_instance:
|
||||
_setup_first_person()
|
||||
|
||||
print("SkinnedPlayerModel: scene tree:")
|
||||
_print_tree(scene, 0)
|
||||
|
||||
# Find skeleton
|
||||
|
||||
skeleton = find_skeleton(self)
|
||||
if not skeleton:
|
||||
skeleton = find_skeleton(scene)
|
||||
|
||||
# Find existing AnimationPlayer in the GLB scene
|
||||
|
||||
if skeleton:
|
||||
print("SkinnedPlayerModel: found skeleton '%s' with %d bones" % [skeleton.name, skeleton.get_bone_count()])
|
||||
else:
|
||||
print("SkinnedPlayerModel: WARNING - no skeleton found")
|
||||
|
||||
animation_player = find_animation_player_recursive(scene)
|
||||
|
||||
# If no AnimationPlayer found, create one and add it to the armature
|
||||
|
||||
if not animation_player:
|
||||
print("SkinnedPlayerModel: no AnimationPlayer in GLB, creating one...")
|
||||
animation_player = AnimationPlayer.new()
|
||||
animation_player.name = "AnimationPlayer"
|
||||
|
||||
# Add to the skeleton/armature node if found, otherwise to self
|
||||
|
||||
if skeleton:
|
||||
skeleton.add_child(animation_player)
|
||||
print("SkinnedPlayerModel: added AnimationPlayer to skeleton")
|
||||
else:
|
||||
add_child(animation_player)
|
||||
print("SkinnedPlayerModel: added AnimationPlayer to self")
|
||||
|
||||
# Try to copy animations from the armature's animation_data
|
||||
|
||||
if skeleton and skeleton.animation_data:
|
||||
var src_data = skeleton.animation_data
|
||||
# Create animation library
|
||||
var anim_lib = AnimationLibrary.new()
|
||||
|
||||
# Copy the action from the armature
|
||||
|
||||
if src_data.action:
|
||||
var anim = src_data.action.copy()
|
||||
anim_lib.add_animation(anim.name, anim)
|
||||
print("SkinnedPlayerModel: copied animation '%s' (%.2fs)" % [anim.name, anim.length])
|
||||
|
||||
# Also check NLA tracks
|
||||
|
||||
if src_data.nla_tracks:
|
||||
for track in src_data.nla_tracks:
|
||||
for strip in track.strips:
|
||||
@@ -76,21 +88,22 @@ func load_model(path: String) -> void:
|
||||
var anim = strip.action.copy()
|
||||
anim_lib.add_animation(anim.name, anim)
|
||||
print("SkinnedPlayerModel: copied NLA animation '%s'" % anim.name)
|
||||
|
||||
|
||||
animation_player.add_animation_library("", anim_lib)
|
||||
else:
|
||||
print("SkinnedPlayerModel: WARNING - skeleton has no animation_data")
|
||||
|
||||
# List available animations
|
||||
print("SkinnedPlayerModel: WARNING - no animation_data found")
|
||||
|
||||
if animation_player:
|
||||
var anim_list = animation_player.get_animation_list()
|
||||
print("SkinnedPlayerModel: %d animations available:" % anim_list.size())
|
||||
for anim in anim_list:
|
||||
var a = animation_player.get_animation(anim)
|
||||
print(" - %s (%.2fs, loop=%s)" % [anim, a.length, a.loop_mode])
|
||||
|
||||
# Auto-play idle
|
||||
if animation_player.has_animation("Idle"):
|
||||
|
||||
if animation_player.has_animation("idle"):
|
||||
animation_player.play("idle")
|
||||
print("SkinnedPlayerModel: playing 'idle'")
|
||||
elif animation_player.has_animation("Idle"):
|
||||
animation_player.play("Idle")
|
||||
print("SkinnedPlayerModel: playing 'Idle'")
|
||||
elif anim_list.size() > 0:
|
||||
@@ -99,6 +112,19 @@ func load_model(path: String) -> void:
|
||||
else:
|
||||
print("SkinnedPlayerModel: ERROR - no AnimationPlayer available")
|
||||
|
||||
func _setup_first_person() -> void:
|
||||
# In first person, hide the head and upper body so only arms/hands/legs show
|
||||
# This creates the classic FPS arms-only view
|
||||
if not _mesh_instance:
|
||||
return
|
||||
print("SkinnedPlayerModel: setting up first-person mode")
|
||||
# The mesh has multiple sub-meshes (body parts). We need to hide the ones
|
||||
# that are above the chest (head, torso, skirt, etc.)
|
||||
# For now, we hide the entire mesh and rely on the arms being separate
|
||||
# A more sophisticated approach would hide specific bones
|
||||
_mesh_instance.visible = false
|
||||
print("SkinnedPlayerModel: mesh hidden for first-person (arms-only view needs separate arms model)")
|
||||
|
||||
func _print_tree(node: Node, depth: int) -> void:
|
||||
var indent = " ".repeat(depth)
|
||||
var extra = ""
|
||||
@@ -158,15 +184,29 @@ func _process(delta: float) -> void:
|
||||
var vel = sm.player.velocity
|
||||
speed = Vector2(vel.x, vel.z).length()
|
||||
|
||||
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 target_anim = "Idle"
|
||||
match state:
|
||||
"ground", "idle":
|
||||
if speed > 1.0:
|
||||
if is_dead:
|
||||
target_anim = "Death"
|
||||
elif is_crouching:
|
||||
target_anim = "Crouch"
|
||||
else:
|
||||
var walk_speed = 10.0
|
||||
if sm and sm.params:
|
||||
walk_speed = sm.params.walk_speed
|
||||
match state:
|
||||
"ground", "idle":
|
||||
if speed > walk_speed * 1.2:
|
||||
target_anim = "Run"
|
||||
elif speed > 0.5:
|
||||
target_anim = "Walk"
|
||||
"slide", "wall_run", "grapple", "dash":
|
||||
target_anim = "Run"
|
||||
"slide", "wall_run", "grapple", "dash":
|
||||
target_anim = "Run"
|
||||
_:
|
||||
target_anim = "Idle"
|
||||
"air":
|
||||
target_anim = "Jump"
|
||||
_:
|
||||
target_anim = "Idle"
|
||||
|
||||
if animation_player.current_animation != target_anim or not animation_player.is_playing():
|
||||
play_animation(target_anim)
|
||||
|
||||
Reference in New Issue
Block a user