feat(miku): proper rigged model from Sketchfab + Blender

Pipeline:
1. Downloaded TDA-style Miku model from Sketchfab (19MB FBX, 38K verts)
2. Imported to Blender, scaled to Godot humanoid proportions (1.8u tall)
3. Created clean 17-bone humanoid armature (Hips/Spine/Chest/Head/Arms/Legs)
4. Auto-weight-painted mesh to armature
5. Added Idle (breath bounce) and Run (cycle) animations
6. Exported as GLB with embedded animations (3MB)

New:
- SkinnedPlayerModel class: loads GLB with own armature+animations
- Player 1 spawns with Miku model, others get procedural humanoid
- Completely separate from procedural model system

Files:
- assets/characters/skins/miku_rigged_final.glb (3MB, 48K verts, 17 bones, 2 anims)
- characters/skinned_player_model.gd
- Removed old procedural GLB attempts
This commit is contained in:
2026-06-22 17:12:24 -04:00
parent b0fdc508f1
commit ae8231d31d
7 changed files with 101 additions and 19 deletions
+88
View File
@@ -0,0 +1,88 @@
extends Node3D
class_name SkinnedPlayerModel
## A player model loaded from a GLB file with its own armature and animations.
## Replaces the procedural HumanoidModel for characters with custom skins.
@export var model_path: String = ""
var skeleton: Skeleton3D
var animation_player: AnimationPlayer
var animation_tree: AnimationTree
func _ready() -> void:
if model_path != "":
load_model(model_path)
func load_model(path: String) -> void:
# Clear existing children
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)
# Find skeleton and animation player
skeleton = find_skeleton(scene)
animation_player = find_animation_player(scene)
if animation_player:
print("SkinnedPlayerModel: loaded '%s' with %d animations" % [
path, animation_player.get_animation_list().size()
])
for anim in animation_player.get_animation_list():
print(" - %s" % anim)
else:
print("SkinnedPlayerModel: no animation player found")
func find_skeleton(node: Node) -> Skeleton3D:
if node is Skeleton3D:
return node
for child in node.get_children():
var result = find_skeleton(child)
if result:
return result
return null
func find_animation_player(node: Node) -> AnimationPlayer:
if node is AnimationPlayer:
return node
for child in node.get_children():
var result = find_animation_player(child)
if result:
return result
return null
func play_animation(name: String) -> void:
if animation_player and animation_player.has_animation(name):
animation_player.play(name)
else:
print("SkinnedPlayerModel: animation '%s' not found" % name)
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
if not animation_player:
return
match state:
"ground", "idle":
if speed > 1.0:
play_animation("Run")
else:
play_animation("Idle")
"air":
play_animation("Idle") # TODO: add jump/fall anim
"crouch":
play_animation("Idle") # TODO: add crouch anim
"slide":
play_animation("Run") # TODO: add slide anim
_:
play_animation("Idle")