fix: use GLTFDocument to load GLB at runtime (no .import needed)

Godot headless/CLI can't import GLB files — the GLTF loader only works
in the editor GUI. Switched SkinnedPlayerModel to use GLTFDocument.append_from_buffer()
which parses GLB data from raw bytes at runtime.

Verified: GLB parses successfully, generates scene with MikuRig skeleton,
AnimationPlayer, and 7 animations (Crouch, Death, Idle, Idle_001, Jump, Run, Walk).

Also added characters/glb_loader.gd as a reusable utility for loading
any GLB file at runtime.
This commit is contained in:
2026-06-23 01:15:37 -04:00
parent 85066f71f3
commit 5e12502a18
7 changed files with 223 additions and 32 deletions
+50
View File
@@ -0,0 +1,50 @@
extends RefCounted
class_name GLBLoader
## Loads a GLB file at runtime using GLTFDocument.
## Usage:
## var loader = GLBLoader.new()
## var scene = loader.load("res://path/to/model.glb")
## if scene:
## get_tree().current_scene.add_child(scene)
static func load(glb_path: String) -> Node3D:
print("GLBLoader: loading " + glb_path)
# Check if file exists
if not FileAccess.file_exists(glb_path):
print("GLBLoader: file not found: " + glb_path)
return null
# Read file as bytes
var file = FileAccess.open(glb_path, FileAccess.READ)
if not file:
print("GLBLoader: failed to open file")
return null
var bytes = file.get_buffer(file.get_length())
file.close()
print("GLBLoader: read %d bytes" % bytes.size())
# Parse with GLTFDocument
var gltf = GLTFDocument.new()
var state = GLTFState.new()
var err = gltf.append_from_buffer(bytes, "", state)
if err != OK:
print("GLBLoader: failed to parse GLB, error: %d" % err)
return null
print("GLBLoader: parsed successfully")
print(" Animations: %d" % gltf.get_animation_count())
print(" Meshes: %d" % gltf.get_mesh_count())
# Generate scene
var scene = gltf.generate_scene(state)
if not scene:
print("GLBLoader: failed to generate scene")
return null
print("GLBLoader: generated scene: %s" % scene.name)
return scene
+46 -30
View File
@@ -21,17 +21,33 @@ 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)
print("SkinnedPlayerModel: loading %s via GLTFDocument" % path)
# Read GLB file as bytes
if not FileAccess.file_exists(path):
print("SkinnedPlayerModel: file not found: %s" % path)
return
var scene = loaded.instantiate()
var file = FileAccess.open(path, FileAccess.READ)
var bytes = file.get_buffer(file.get_length())
file.close()
# Parse with GLTFDocument (works at runtime without .import files)
var gltf = GLTFDocument.new()
var state = GLTFState.new()
var err = gltf.append_from_buffer(bytes, "", state)
if err != OK:
print("SkinnedPlayerModel: GLTFDocument parse failed: %d" % err)
return
print("SkinnedPlayerModel: parsed GLB, generating scene...")
var scene = gltf.generate_scene(state)
if not scene:
print("SkinnedPlayerModel: failed to instantiate %s" % path)
print("SkinnedPlayerModel: failed to generate scene")
return
add_child(scene)
print("SkinnedPlayerModel: scene added: %s" % scene.name)
if scale_factor != 1.0:
scene.scale = Vector3(scale_factor, scale_factor, scale_factor)
@@ -40,7 +56,6 @@ func load_model(path: String) -> void:
# 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]
@@ -48,36 +63,37 @@ func load_model(path: String) -> void:
if first_person_mode and _mesh_instance:
_setup_first_person()
# Print tree for debugging
print("SkinnedPlayerModel: scene tree:")
_print_tree(scene, 0)
skeleton = find_skeleton(self)
if not skeleton:
skeleton = find_skeleton(scene)
# Find skeleton
skeleton = _find_skeleton(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 not animation_player:
print("SkinnedPlayerModel: no AnimationPlayer in GLB, creating one...")
animation_player = AnimationPlayer.new()
animation_player.name = "AnimationPlayer"
if skeleton:
skeleton.add_child(animation_player)
else:
add_child(animation_player)
if skeleton and skeleton.animation_data:
var src_data = skeleton.animation_data
var anim_lib = AnimationLibrary.new()
if src_data.action:
var anim = src_data.action.copy()
# Find AnimationPlayer
animation_player = _find_animation_player(scene)
if animation_player:
var anim_list = animation_player.get_animation_list()
print("SkinnedPlayerModel: %d animations available:" % anim_list.size())
for anim_name in anim_list:
var a = animation_player.get_animation(anim_name)
print(" - %s (%.2fs, loop=%s)" % [anim_name, a.length, a.mode])
# Auto-play 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:
animation_player.play(anim_list[0])
print("SkinnedPlayerModel: playing '%s'" % anim_list[0])
else:
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
anim_lib.add_animation(anim.name, anim)
print("SkinnedPlayerModel: copied animation '%s' (%.2fs)" % [anim.name, anim.length])
@@ -136,7 +152,7 @@ func _print_tree(node: Node, depth: int) -> void:
for child in node.get_children():
_print_tree(child, depth + 1)
func find_skeleton(node: Node) -> Skeleton3D:
func _find_skeleton(node: Node) -> Skeleton3D:
if node is Skeleton3D:
return node
for child in node.get_children():
@@ -145,7 +161,7 @@ func find_skeleton(node: Node) -> Skeleton3D:
return result
return null
func find_animation_player_recursive(node: Node) -> AnimationPlayer:
func _find_animation_player(node: Node) -> AnimationPlayer:
if node is AnimationPlayer:
return node
for child in node.get_children():