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:
@@ -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
|
||||||
@@ -21,17 +21,33 @@ func load_model(path: String) -> void:
|
|||||||
for child in get_children():
|
for child in get_children():
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
|
|
||||||
var loaded = load(path)
|
print("SkinnedPlayerModel: loading %s via GLTFDocument" % path)
|
||||||
if not loaded:
|
|
||||||
print("SkinnedPlayerModel: failed to load %s" % path)
|
# Read GLB file as bytes
|
||||||
|
if not FileAccess.file_exists(path):
|
||||||
|
print("SkinnedPlayerModel: file not found: %s" % path)
|
||||||
return
|
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:
|
if not scene:
|
||||||
print("SkinnedPlayerModel: failed to instantiate %s" % path)
|
print("SkinnedPlayerModel: failed to generate scene")
|
||||||
return
|
return
|
||||||
|
|
||||||
add_child(scene)
|
add_child(scene)
|
||||||
|
print("SkinnedPlayerModel: scene added: %s" % scene.name)
|
||||||
|
|
||||||
if scale_factor != 1.0:
|
if scale_factor != 1.0:
|
||||||
scene.scale = Vector3(scale_factor, scale_factor, scale_factor)
|
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
|
# 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)
|
||||||
if not _mesh_instance:
|
if not _mesh_instance:
|
||||||
# Try to find any MeshInstance3D
|
|
||||||
var mesh_instances = scene.find_children("*", "MeshInstance3D", true, false)
|
var mesh_instances = scene.find_children("*", "MeshInstance3D", true, false)
|
||||||
if mesh_instances.size() > 0:
|
if mesh_instances.size() > 0:
|
||||||
_mesh_instance = mesh_instances[0]
|
_mesh_instance = mesh_instances[0]
|
||||||
@@ -48,36 +63,37 @@ func load_model(path: String) -> void:
|
|||||||
if first_person_mode and _mesh_instance:
|
if first_person_mode and _mesh_instance:
|
||||||
_setup_first_person()
|
_setup_first_person()
|
||||||
|
|
||||||
|
# Print tree for debugging
|
||||||
print("SkinnedPlayerModel: scene tree:")
|
print("SkinnedPlayerModel: scene tree:")
|
||||||
_print_tree(scene, 0)
|
_print_tree(scene, 0)
|
||||||
|
|
||||||
skeleton = find_skeleton(self)
|
# Find skeleton
|
||||||
if not skeleton:
|
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()])
|
||||||
else:
|
else:
|
||||||
print("SkinnedPlayerModel: WARNING - no skeleton found")
|
print("SkinnedPlayerModel: WARNING - no skeleton found")
|
||||||
|
|
||||||
animation_player = find_animation_player_recursive(scene)
|
# Find AnimationPlayer
|
||||||
|
animation_player = _find_animation_player(scene)
|
||||||
if not animation_player:
|
if animation_player:
|
||||||
print("SkinnedPlayerModel: no AnimationPlayer in GLB, creating one...")
|
var anim_list = animation_player.get_animation_list()
|
||||||
animation_player = AnimationPlayer.new()
|
print("SkinnedPlayerModel: %d animations available:" % anim_list.size())
|
||||||
animation_player.name = "AnimationPlayer"
|
for anim_name in anim_list:
|
||||||
|
var a = animation_player.get_animation(anim_name)
|
||||||
if skeleton:
|
print(" - %s (%.2fs, loop=%s)" % [anim_name, a.length, a.mode])
|
||||||
skeleton.add_child(animation_player)
|
# Auto-play idle
|
||||||
else:
|
if animation_player.has_animation("idle"):
|
||||||
add_child(animation_player)
|
animation_player.play("idle")
|
||||||
|
print("SkinnedPlayerModel: playing 'idle'")
|
||||||
if skeleton and skeleton.animation_data:
|
elif animation_player.has_animation("Idle"):
|
||||||
var src_data = skeleton.animation_data
|
animation_player.play("Idle")
|
||||||
var anim_lib = AnimationLibrary.new()
|
print("SkinnedPlayerModel: playing 'Idle'")
|
||||||
|
elif anim_list.size() > 0:
|
||||||
if src_data.action:
|
animation_player.play(anim_list[0])
|
||||||
var anim = src_data.action.copy()
|
print("SkinnedPlayerModel: playing '%s'" % anim_list[0])
|
||||||
|
else:
|
||||||
|
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
|
||||||
anim_lib.add_animation(anim.name, anim)
|
anim_lib.add_animation(anim.name, anim)
|
||||||
print("SkinnedPlayerModel: copied animation '%s' (%.2fs)" % [anim.name, anim.length])
|
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():
|
for child in node.get_children():
|
||||||
_print_tree(child, depth + 1)
|
_print_tree(child, depth + 1)
|
||||||
|
|
||||||
func find_skeleton(node: Node) -> Skeleton3D:
|
func _find_skeleton(node: Node) -> Skeleton3D:
|
||||||
if node is Skeleton3D:
|
if node is Skeleton3D:
|
||||||
return node
|
return node
|
||||||
for child in node.get_children():
|
for child in node.get_children():
|
||||||
@@ -145,7 +161,7 @@ func find_skeleton(node: Node) -> Skeleton3D:
|
|||||||
return result
|
return result
|
||||||
return null
|
return null
|
||||||
|
|
||||||
func find_animation_player_recursive(node: Node) -> AnimationPlayer:
|
func _find_animation_player(node: Node) -> AnimationPlayer:
|
||||||
if node is AnimationPlayer:
|
if node is AnimationPlayer:
|
||||||
return node
|
return node
|
||||||
for child in node.get_children():
|
for child in node.get_children():
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
print("=== GLTFDocument API Test ===")
|
||||||
|
|
||||||
|
var path = "res://assets/characters/skins/miku_rigged_animated.glb"
|
||||||
|
|
||||||
|
var file = FileAccess.open(path, FileAccess.READ)
|
||||||
|
var bytes = file.get_buffer(file.get_length())
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
var gltf = GLTFDocument.new()
|
||||||
|
var state = GLTFState.new()
|
||||||
|
|
||||||
|
var err = gltf.append_from_buffer(bytes, "", state)
|
||||||
|
print("append_from_buffer: %d" % err)
|
||||||
|
|
||||||
|
if err == OK:
|
||||||
|
# List all methods
|
||||||
|
var methods = gltf.get_method_list()
|
||||||
|
print("Total methods: %d" % methods.size())
|
||||||
|
for m in methods:
|
||||||
|
var n = m.name
|
||||||
|
if n.contains("anim") or n.contains("mesh") or n.contains("scene") or n.contains("generate") or n.contains("get_") or n.contains("instantiate"):
|
||||||
|
print(" %s" % n)
|
||||||
|
|
||||||
|
# Try generate_scene
|
||||||
|
if gltf.has_method("generate_scene"):
|
||||||
|
var scene = gltf.generate_scene(state)
|
||||||
|
if scene:
|
||||||
|
print("Scene: %s" % scene.name)
|
||||||
|
get_root().add_child(scene)
|
||||||
|
else:
|
||||||
|
print("generate_scene returned null")
|
||||||
|
else:
|
||||||
|
print("No generate_scene method")
|
||||||
|
|
||||||
|
# Try instantiate
|
||||||
|
if gltf.has_method("instantiate"):
|
||||||
|
var node = gltf.instantiate(state)
|
||||||
|
if node:
|
||||||
|
print("Instantiated: %s" % node.name)
|
||||||
|
get_root().add_child(node)
|
||||||
|
else:
|
||||||
|
print("instantiate returned null")
|
||||||
|
else:
|
||||||
|
print("No instantiate method")
|
||||||
|
|
||||||
|
quit()
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
print("=== GLTFDocument Test ===")
|
||||||
|
|
||||||
|
var path = "res://assets/characters/skins/miku_rigged_animated.glb"
|
||||||
|
|
||||||
|
if not FileAccess.file_exists(path):
|
||||||
|
print("File not found")
|
||||||
|
quit()
|
||||||
|
return
|
||||||
|
|
||||||
|
var file = FileAccess.open(path, FileAccess.READ)
|
||||||
|
var bytes = file.get_buffer(file.get_length())
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
print("Read %d bytes" % bytes.size())
|
||||||
|
|
||||||
|
var gltf = GLTFDocument.new()
|
||||||
|
var state = GLTFState.new()
|
||||||
|
|
||||||
|
var err = gltf.append_from_buffer(bytes, "", state)
|
||||||
|
print("append_from_buffer returned: %d" % err)
|
||||||
|
|
||||||
|
if err == OK:
|
||||||
|
print("Animations: %d" % gltf.get_animation_count())
|
||||||
|
print("Meshes: %d" % gltf.get_mesh_count())
|
||||||
|
|
||||||
|
var scene = gltf.generate_scene(state)
|
||||||
|
if scene:
|
||||||
|
print("Scene: %s" % scene.name)
|
||||||
|
get_root().add_child(scene)
|
||||||
|
else:
|
||||||
|
print("Failed to generate scene")
|
||||||
|
else:
|
||||||
|
print("Failed to parse")
|
||||||
|
|
||||||
|
quit()
|
||||||
@@ -436,6 +436,7 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
skinned.scale_factor = 1.0 # Already scaled to 1.8m in Blender
|
skinned.scale_factor = 1.0 # Already scaled to 1.8m in Blender
|
||||||
skinned.position = Vector3.ZERO # Model origin = player origin
|
skinned.position = Vector3.ZERO # Model origin = player origin
|
||||||
player.add_child(skinned)
|
player.add_child(skinned)
|
||||||
|
print("TestLevelBuilder: added SkinnedModel for player 1")
|
||||||
else:
|
else:
|
||||||
# Default: procedural humanoid
|
# Default: procedural humanoid
|
||||||
var humanoid = load("res://characters/humanoid_model.gd").new()
|
var humanoid = load("res://characters/humanoid_model.gd").new()
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
print("=== Test GLB Load ===")
|
||||||
|
|
||||||
|
var path = "res://assets/characters/skins/miku_rigged_animated.glb"
|
||||||
|
|
||||||
|
# Read GLB
|
||||||
|
var file = FileAccess.open(path, FileAccess.READ)
|
||||||
|
var bytes = file.get_buffer(file.get_length())
|
||||||
|
file.close()
|
||||||
|
print("Read %d bytes" % bytes.size())
|
||||||
|
|
||||||
|
# Parse
|
||||||
|
var gltf = GLTFDocument.new()
|
||||||
|
var state = GLTFState.new()
|
||||||
|
var err = gltf.append_from_buffer(bytes, "", state)
|
||||||
|
print("Parse result: %d" % err)
|
||||||
|
|
||||||
|
if err == OK:
|
||||||
|
var scene = gltf.generate_scene(state)
|
||||||
|
if scene:
|
||||||
|
print("Scene: %s" % scene.name)
|
||||||
|
print("Children:")
|
||||||
|
for child in scene.get_children():
|
||||||
|
print(" %s (%s)" % [child.name, child.get_class()])
|
||||||
|
if child is Skeleton3D:
|
||||||
|
print(" Bones: %d" % child.get_bone_count())
|
||||||
|
if child is AnimationPlayer:
|
||||||
|
print(" Animations: %s" % child.get_animation_list())
|
||||||
|
get_root().add_child(scene)
|
||||||
|
print("Scene added to tree!")
|
||||||
|
else:
|
||||||
|
print("Failed to generate scene")
|
||||||
|
|
||||||
|
quit()
|
||||||
@@ -148,9 +148,10 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
var skinned = load("res://characters/skinned_player_model.gd").new()
|
var skinned = load("res://characters/skinned_player_model.gd").new()
|
||||||
skinned.name = "SkinnedModel"
|
skinned.name = "SkinnedModel"
|
||||||
skinned.model_path = "res://assets/characters/skins/miku_rigged_animated.glb"
|
skinned.model_path = "res://assets/characters/skins/miku_rigged_animated.glb"
|
||||||
skinned.position = Vector3(0, 0.0, 0) # Model origin aligns with player origin
|
skinned.position = Vector3(0, 0.0, 0)
|
||||||
skinned.first_person_mode = true # Hide body in FPS view, show only arms
|
skinned.first_person_mode = true
|
||||||
player.add_child(skinned)
|
player.add_child(skinned)
|
||||||
|
print("LevelRuntime: added SkinnedModel for local player")
|
||||||
else:
|
else:
|
||||||
# Remote players: use procedural humanoid
|
# Remote players: use procedural humanoid
|
||||||
var humanoid = load("res://characters/humanoid_model.gd").new()
|
var humanoid = load("res://characters/humanoid_model.gd").new()
|
||||||
|
|||||||
Reference in New Issue
Block a user