PC3 Phase 1.2: placeholder avatar asset pack + make the loader actually assemble
assets/avatars/catalog.json referenced 14 GLB files that were never in the repo, so the avatar system could not load a single avatar. And the loader's assembly path had never run against a real skeleton -- it called Skeleton3D.has_bone() (no such method in Godot 4) and hand-copied bones in a way that dropped the skin binding. - scripts/tools/gen_placeholder_avatars.gd: generates the pack headlessly via GLTFDocument -- one rigged/skinned base body (the full 24-bone canonical rig from a T-pose) + 13 primitive attachments. Crude on purpose; real art swaps in as a pure content change (catalog paths unchanged). - assets/avatars/*.glb: the generated pack (checked in). - avatar_loader.gd: rewrote _parse_glb_as_avatar to lift the skin-bound Skeleton3D that generate_scene() already produces, instead of rebuilding it. Attachments now mount via BoneAttachment3D to the catalog's mount_bone and are actually added to the avatar tree (they were parsed then dropped). Fixed the has_bone() crash, the null ARRAY_BONES cast in _retarget_skin, and the Array[String] type mismatch on the accessory list. - smoke_test_avatar_system.gd: replaced the "asserts it fails gracefully because the files don't exist" section with a real assembly check -- Skeleton3D present, full 24-bone rig, canonical bones resolve by name, visible body mesh, equipped items mounted to bones. 14/14 from a cold cache. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://bm01jsuomi50i"
|
||||||
|
path="res://.godot/imported/accessory_belt_01.glb-a2b6be72b4422067d290c84868489d2d.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/accessory_belt_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/accessory_belt_01.glb-a2b6be72b4422067d290c84868489d2d.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dvou487niwnjt"
|
||||||
|
path="res://.godot/imported/accessory_necklace_01.glb-75d713dbb1ff9579d6f0bf8b7ddde1e2.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/accessory_necklace_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/accessory_necklace_01.glb-75d713dbb1ff9579d6f0bf8b7ddde1e2.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dknbtq33pssf4"
|
||||||
|
path="res://.godot/imported/back_backpack_01.glb-d0ccc39aed8e86403d649c8d07008591.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/back_backpack_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/back_backpack_01.glb-d0ccc39aed8e86403d649c8d07008591.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://brfp6g6pcbbw7"
|
||||||
|
path="res://.godot/imported/base_human_male_01.glb-c917b85c8f8ab5d1974812a6891f052f.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/base_human_male_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/base_human_male_01.glb-c917b85c8f8ab5d1974812a6891f052f.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://bva2etlpa8act"
|
||||||
|
path="res://.godot/imported/chest_bandit_01.glb-90867999feb79e02c469ef45d970f0e1.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/chest_bandit_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/chest_bandit_01.glb-90867999feb79e02c469ef45d970f0e1.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://ctwcxf552y325"
|
||||||
|
path="res://.godot/imported/face_glasses_01.glb-fe06ebb12e6caf265dce06de37a74fc5.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/face_glasses_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/face_glasses_01.glb-fe06ebb12e6caf265dce06de37a74fc5.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://j4dc6l3l0j6f"
|
||||||
|
path="res://.godot/imported/hair_mohawk_01.glb-f2a6883730b5d02facb56d1dbb0e371e.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/hair_mohawk_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/hair_mohawk_01.glb-f2a6883730b5d02facb56d1dbb0e371e.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dp4vqp2trrank"
|
||||||
|
path="res://.godot/imported/hair_ponytail_01.glb-cdf5913fc63278b1dd362cb640c2ecb4.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/hair_ponytail_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/hair_ponytail_01.glb-cdf5913fc63278b1dd362cb640c2ecb4.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://bkp35fnns36xb"
|
||||||
|
path="res://.godot/imported/head_cyber_01.glb-8081a99d6b31c121d208dd2f9629df5c.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/head_cyber_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/head_cyber_01.glb-8081a99d6b31c121d208dd2f9629df5c.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://bgfjpdu8kg0aj"
|
||||||
|
path="res://.godot/imported/left_foot_boot_01.glb-cab36c9e44befe0a3ff50ee651f2ff67.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/left_foot_boot_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/left_foot_boot_01.glb-cab36c9e44befe0a3ff50ee651f2ff67.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dtq7tgio1iymd"
|
||||||
|
path="res://.godot/imported/left_hand_glove_01.glb-4b237bea3299c6604547afa353a7b602.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/left_hand_glove_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/left_hand_glove_01.glb-4b237bea3299c6604547afa353a7b602.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://yo204q1m6hs3"
|
||||||
|
path="res://.godot/imported/pants_cargo_01.glb-00f3e3b72fd692344cbb8bfd2b8449f9.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/pants_cargo_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/pants_cargo_01.glb-00f3e3b72fd692344cbb8bfd2b8449f9.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://30ij0eapm6ti"
|
||||||
|
path="res://.godot/imported/right_foot_boot_01.glb-ffff27e561e5632596b7ba89fee5b5ea.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/right_foot_boot_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/right_foot_boot_01.glb-ffff27e561e5632596b7ba89fee5b5ea.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
Binary file not shown.
@@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://bcrtxtp47dv8h"
|
||||||
|
path="res://.godot/imported/right_hand_glove_01.glb-a59875b630ac413350c3bdf6acc84bdf.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/avatars/right_hand_glove_01.glb"
|
||||||
|
dest_files=["res://.godot/imported/right_hand_glove_01.glb-a59875b630ac413350c3bdf6acc84bdf.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
+114
-126
@@ -175,7 +175,7 @@ func load_attachment(slot_id: String, asset_id: String) -> Node3D:
|
|||||||
|
|
||||||
# Check slot exclusivity for accessory slot.
|
# Check slot exclusivity for accessory slot.
|
||||||
if slot_id == "accessory":
|
if slot_id == "accessory":
|
||||||
var current_accessories: Array[String] = _attachments.get("accessory", [])
|
var current_accessories: Array = _attachments.get("accessory", [])
|
||||||
if current_accessories.size() >= MAX_ACCESSORY_COUNT:
|
if current_accessories.size() >= MAX_ACCESSORY_COUNT:
|
||||||
load_error.emit("Accessory slot full (%d/%d)" % [
|
load_error.emit("Accessory slot full (%d/%d)" % [
|
||||||
MAX_ACCESSORY_COUNT, MAX_ACCESSORY_COUNT])
|
MAX_ACCESSORY_COUNT, MAX_ACCESSORY_COUNT])
|
||||||
@@ -192,19 +192,37 @@ func load_attachment(slot_id: String, asset_id: String) -> Node3D:
|
|||||||
load_error.emit("Failed to parse attachment GLB: %s" % path)
|
load_error.emit("Failed to parse attachment GLB: %s" % path)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
# Retarget skin to the canonical skeleton.
|
# If the attachment carries its own skin, retarget it to the canonical
|
||||||
|
# skeleton; static props (the common case) are unaffected.
|
||||||
_retarget_skin(attachment_node)
|
_retarget_skin(attachment_node)
|
||||||
|
|
||||||
# Parent static accessories to their mount bone.
|
# Mount to the appropriate bone via a BoneAttachment3D so it follows the
|
||||||
_parent_attachment(attachment_node, slot_id, asset_meta)
|
# skeleton. Falls back to a plain skeleton child if the mount bone is
|
||||||
|
# missing (a bad catalog entry) rather than dropping the attachment.
|
||||||
|
var mount_bone: String = asset_meta.get("mount_bone", "")
|
||||||
|
if mount_bone.is_empty():
|
||||||
|
mount_bone = _slot_to_mount_bone(slot_id)
|
||||||
|
var mount_node: Node3D
|
||||||
|
if not mount_bone.is_empty() and skeleton.find_bone(mount_bone) != -1:
|
||||||
|
var ba := BoneAttachment3D.new()
|
||||||
|
ba.name = "Mount_%s" % slot_id if slot_id != "accessory" else "Mount_accessory_%s" % asset_id
|
||||||
|
ba.bone_name = mount_bone
|
||||||
|
skeleton.add_child(ba)
|
||||||
|
ba.add_child(attachment_node)
|
||||||
|
mount_node = ba
|
||||||
|
else:
|
||||||
|
skeleton.add_child(attachment_node)
|
||||||
|
mount_node = attachment_node
|
||||||
|
|
||||||
# Store reference.
|
# Store the top-level mount node so unload can free the whole unit.
|
||||||
if slot_id == "accessory":
|
if slot_id == "accessory":
|
||||||
var list: Array[String] = _attachments.get("accessory", [])
|
var list: Array = _attachments.get("accessory", [])
|
||||||
list.append(asset_id)
|
list.append(mount_node)
|
||||||
_attachments["accessory"] = list
|
_attachments["accessory"] = list
|
||||||
else:
|
else:
|
||||||
_attachments[slot_id] = attachment_node
|
if _attachments.has(slot_id) and is_instance_valid(_attachments[slot_id]):
|
||||||
|
_attachments[slot_id].queue_free()
|
||||||
|
_attachments[slot_id] = mount_node
|
||||||
|
|
||||||
attachment_loaded.emit(slot_id, attachment_node)
|
attachment_loaded.emit(slot_id, attachment_node)
|
||||||
return attachment_node
|
return attachment_node
|
||||||
@@ -213,15 +231,13 @@ func load_attachment(slot_id: String, asset_id: String) -> Node3D:
|
|||||||
## Remove an attachment from the given slot.
|
## Remove an attachment from the given slot.
|
||||||
func unload_attachment(slot_id: String) -> void:
|
func unload_attachment(slot_id: String) -> void:
|
||||||
if slot_id == "accessory":
|
if slot_id == "accessory":
|
||||||
var list: Array[String] = _attachments.get("accessory", [])
|
for node in _attachments.get("accessory", []):
|
||||||
for asset_id in list:
|
if is_instance_valid(node):
|
||||||
var node = _attachments.get(asset_id)
|
|
||||||
if node and node is Node:
|
|
||||||
node.queue_free()
|
node.queue_free()
|
||||||
_attachments.erase("accessory")
|
_attachments.erase("accessory")
|
||||||
else:
|
else:
|
||||||
var node = _attachments.get(slot_id)
|
var node = _attachments.get(slot_id)
|
||||||
if node and node is Node:
|
if is_instance_valid(node):
|
||||||
node.queue_free()
|
node.queue_free()
|
||||||
_attachments.erase(slot_id)
|
_attachments.erase(slot_id)
|
||||||
|
|
||||||
@@ -280,58 +296,73 @@ func reset() -> void:
|
|||||||
# Internal: GLB Parsing
|
# Internal: GLB Parsing
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
## Parse a GLB byte array into an avatar Node3D structure.
|
## Parse a GLB byte array into an avatar Node3D structure:
|
||||||
## Creates: Avatar (Node3D) -> Skeleton3D + BodyMeshes + AttachmentPoints
|
## Avatar (Node3D)
|
||||||
|
## Skeleton3D -- the canonical rig, with skinned body meshes as children
|
||||||
|
## BodyMeshes (Node3D) -- references to the skinned body meshes, for slot hiding
|
||||||
|
## Attachments (Node3D)-- equipped attachment nodes (added by load_attachment)
|
||||||
|
##
|
||||||
|
## Rather than hand-copying the GLB skeleton bone-by-bone (which loses the skin
|
||||||
|
## binding), this uses GLTFDocument.generate_scene() -- which already produces a
|
||||||
|
## correctly skin-bound Skeleton3D + MeshInstance3D tree -- and lifts that
|
||||||
|
## skeleton into our container structure intact.
|
||||||
func _parse_glb_as_avatar(glb_data: PackedByteArray, path: String) -> Node3D:
|
func _parse_glb_as_avatar(glb_data: PackedByteArray, path: String) -> Node3D:
|
||||||
var avatar := Node3D.new()
|
|
||||||
avatar.name = "Avatar"
|
|
||||||
|
|
||||||
# Create the skeleton container.
|
|
||||||
var skeleton_node := Skeleton3D.new()
|
|
||||||
skeleton_node.name = "Skeleton3D"
|
|
||||||
skeleton_node.position = Vector3(0, 0, 0)
|
|
||||||
avatar.add_child(skeleton_node)
|
|
||||||
|
|
||||||
# Create attachment points container.
|
|
||||||
var attach_points := Node3D.new()
|
|
||||||
attach_points.name = "AttachmentPoints"
|
|
||||||
avatar.add_child(attach_points)
|
|
||||||
|
|
||||||
# Create body meshes container.
|
|
||||||
var body_meshes := Node3D.new()
|
|
||||||
body_meshes.name = "BodyMeshes"
|
|
||||||
avatar.add_child(body_meshes)
|
|
||||||
|
|
||||||
# Create attachments container.
|
|
||||||
var attachments_container := Node3D.new()
|
|
||||||
attachments_container.name = "Attachments"
|
|
||||||
avatar.add_child(attachments_container)
|
|
||||||
|
|
||||||
# Parse the GLB and extract skeleton + meshes.
|
|
||||||
var gltf_document := GLTFDocument.new()
|
var gltf_document := GLTFDocument.new()
|
||||||
var gltf_state := GLTFState.new()
|
var gltf_state := GLTFState.new()
|
||||||
|
|
||||||
var parse_result := gltf_document.append_from_buffer(glb_data, "", gltf_state)
|
var parse_result := gltf_document.append_from_buffer(glb_data, "", gltf_state)
|
||||||
if parse_result != OK:
|
if parse_result != OK:
|
||||||
push_error("GLB parse error: %s" % error_string(parse_result))
|
push_error("GLB parse error: %s" % error_string(parse_result))
|
||||||
avatar.queue_free()
|
|
||||||
return null
|
return null
|
||||||
|
|
||||||
# Generate the actual scene tree from the parsed GLTF state.
|
|
||||||
var scene_root := gltf_document.generate_scene(gltf_state)
|
var scene_root := gltf_document.generate_scene(gltf_state)
|
||||||
if scene_root == null:
|
if not (scene_root is Node3D):
|
||||||
push_error("Failed to generate scene from GLB: %s" % path)
|
push_error("Failed to generate scene from GLB: %s" % path)
|
||||||
avatar.queue_free()
|
if scene_root:
|
||||||
|
scene_root.queue_free()
|
||||||
return null
|
return null
|
||||||
|
|
||||||
# Merge GLB nodes into our avatar structure (recurses into children).
|
var src_skel := _find_first_skeleton(scene_root)
|
||||||
if scene_root is Node3D:
|
if src_skel == null:
|
||||||
_merge_node_into_avatar(scene_root, avatar, body_meshes,
|
push_error("No Skeleton3D in generated scene for base body: %s" % path)
|
||||||
attach_points, attachments_container)
|
scene_root.queue_free()
|
||||||
|
return null
|
||||||
|
|
||||||
|
var avatar := Node3D.new()
|
||||||
|
avatar.name = "Avatar"
|
||||||
|
|
||||||
|
# Reparent the real (skin-bound) skeleton into our structure, renamed so
|
||||||
|
# downstream get_node("Skeleton3D") lookups work regardless of the GLB's
|
||||||
|
# own naming.
|
||||||
|
src_skel.get_parent().remove_child(src_skel)
|
||||||
|
src_skel.name = "Skeleton3D"
|
||||||
|
src_skel.transform = Transform3D.IDENTITY
|
||||||
|
_clear_owner_recursive(src_skel)
|
||||||
|
avatar.add_child(src_skel)
|
||||||
|
scene_root.queue_free()
|
||||||
|
|
||||||
|
var body_meshes := Node3D.new()
|
||||||
|
body_meshes.name = "BodyMeshes"
|
||||||
|
avatar.add_child(body_meshes)
|
||||||
|
|
||||||
|
var attachments_container := Node3D.new()
|
||||||
|
attachments_container.name = "Attachments"
|
||||||
|
avatar.add_child(attachments_container)
|
||||||
|
|
||||||
return avatar
|
return avatar
|
||||||
|
|
||||||
|
|
||||||
|
## Depth-first search for the first Skeleton3D under [param node].
|
||||||
|
func _find_first_skeleton(node: Node) -> Skeleton3D:
|
||||||
|
if node is Skeleton3D:
|
||||||
|
return node as Skeleton3D
|
||||||
|
for child in node.get_children():
|
||||||
|
var found := _find_first_skeleton(child)
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
## Parse a GLB byte array into an attachment Node3D.
|
## Parse a GLB byte array into an attachment Node3D.
|
||||||
func _parse_glb_as_attachment(glb_data: PackedByteArray, path: String,
|
func _parse_glb_as_attachment(glb_data: PackedByteArray, path: String,
|
||||||
slot_id: String) -> Node3D:
|
slot_id: String) -> Node3D:
|
||||||
@@ -345,97 +376,50 @@ func _parse_glb_as_attachment(glb_data: PackedByteArray, path: String,
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
var scene_root := gltf_document.generate_scene(gltf_state)
|
var scene_root := gltf_document.generate_scene(gltf_state)
|
||||||
if scene_root == null:
|
if not (scene_root is Node3D):
|
||||||
push_error("Failed to generate scene from attachment GLB: %s" % path)
|
push_error("Failed to generate scene from attachment GLB: %s" % path)
|
||||||
|
if scene_root:
|
||||||
|
scene_root.queue_free()
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
# Wrap the generated attachment geometry in a single named Node3D so the
|
||||||
|
# caller can mount it to a bone as one unit.
|
||||||
var attachment := Node3D.new()
|
var attachment := Node3D.new()
|
||||||
attachment.name = "%s_%s" % [slot_id, path.get_file().get_basename()]
|
attachment.name = "%s_%s" % [slot_id, path.get_file().get_basename()]
|
||||||
|
for child in scene_root.get_children().duplicate():
|
||||||
if scene_root is Node3D:
|
scene_root.remove_child(child)
|
||||||
attachment.add_child(scene_root.duplicate())
|
_clear_owner_recursive(child)
|
||||||
|
attachment.add_child(child)
|
||||||
|
scene_root.queue_free()
|
||||||
return attachment
|
return attachment
|
||||||
|
|
||||||
|
|
||||||
## Merge a GLB node tree into the avatar's container structure.
|
## generate_scene() stamps every node with the GLB root as `owner`; that
|
||||||
func _merge_node_into_avatar(node: Node3D, avatar: Node3D,
|
## becomes inconsistent (and noisy) once we reparent the node elsewhere.
|
||||||
body_meshes: Node3D, attach_points: Node3D,
|
func _clear_owner_recursive(node: Node) -> void:
|
||||||
attachments_container: Node3D) -> void:
|
node.owner = null
|
||||||
|
|
||||||
var node_name := node.name
|
|
||||||
|
|
||||||
# Identify skeleton nodes.
|
|
||||||
if node is Skeleton3D:
|
|
||||||
var src_skel := node as Skeleton3D
|
|
||||||
var dst_skel := avatar.get_node_or_null("Skeleton3D") as Skeleton3D
|
|
||||||
if dst_skel:
|
|
||||||
# Copy bones from source skeleton to destination.
|
|
||||||
for i in src_skel.get_bone_count():
|
|
||||||
var bone_name := src_skel.get_bone_name(i)
|
|
||||||
var bone_pose := src_skel.get_bone_pose(i)
|
|
||||||
var parent_idx := src_skel.get_bone_parent(i)
|
|
||||||
if dst_skel.has_bone(bone_name):
|
|
||||||
# Bone already exists (canonical).
|
|
||||||
dst_skel.set_bone_pose(i, bone_pose)
|
|
||||||
if parent_idx >= 0:
|
|
||||||
var parent_name := src_skel.get_bone_name(parent_idx)
|
|
||||||
var dst_parent_idx := dst_skel.find_bone(parent_name)
|
|
||||||
if dst_parent_idx >= 0:
|
|
||||||
dst_skel.set_bone_parent(i, dst_parent_idx)
|
|
||||||
else:
|
|
||||||
# Add new bone.
|
|
||||||
var new_idx := dst_skel.add_bone(bone_name)
|
|
||||||
dst_skel.set_bone_pose(new_idx, bone_pose)
|
|
||||||
if parent_idx >= 0:
|
|
||||||
var parent_name := src_skel.get_bone_name(parent_idx)
|
|
||||||
var dst_parent_idx := dst_skel.find_bone(parent_name)
|
|
||||||
if dst_parent_idx >= 0:
|
|
||||||
dst_skel.set_bone_parent(new_idx, dst_parent_idx)
|
|
||||||
# Skinned meshes are commonly children of the skeleton node in a GLB
|
|
||||||
# export, not siblings — fall through to the recursion below instead
|
|
||||||
# of returning, so they still get merged in.
|
|
||||||
for child in src_skel.get_children():
|
|
||||||
if child is Node3D:
|
|
||||||
_merge_node_into_avatar(child, avatar, body_meshes,
|
|
||||||
attach_points, attachments_container)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Identify attachment point nodes (empty nodes named attach_*).
|
|
||||||
if node_name.begins_with("attach_"):
|
|
||||||
var cloned := node.duplicate()
|
|
||||||
cloned.name = node_name
|
|
||||||
attach_points.add_child(cloned)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Identify mesh nodes.
|
|
||||||
if node is MeshInstance3D:
|
|
||||||
var mesh_inst := node as MeshInstance3D
|
|
||||||
var cloned := mesh_inst.duplicate() as MeshInstance3D
|
|
||||||
# Determine which container to place it in.
|
|
||||||
if node_name == "Body" or node_name.ends_with("_Mesh"):
|
|
||||||
body_meshes.add_child(cloned)
|
|
||||||
else:
|
|
||||||
attachments_container.add_child(cloned)
|
|
||||||
return
|
|
||||||
|
|
||||||
# For other nodes, recurse into children.
|
|
||||||
for child in node.get_children():
|
for child in node.get_children():
|
||||||
if child is Node3D:
|
_clear_owner_recursive(child)
|
||||||
_merge_node_into_avatar(child, avatar, body_meshes,
|
|
||||||
attach_points, attachments_container)
|
|
||||||
|
|
||||||
|
|
||||||
## Extract base body meshes from the avatar and store them by slot.
|
## Index the base body's skinned meshes by slot so equipping a body-part
|
||||||
|
## attachment can hide the corresponding base mesh. generate_scene() parents
|
||||||
|
## skinned meshes under the Skeleton3D, so scan there.
|
||||||
func _extract_base_meshes(avatar: Node3D) -> void:
|
func _extract_base_meshes(avatar: Node3D) -> void:
|
||||||
var body_meshes := avatar.get_node_or_null("BodyMeshes") as Node3D
|
var skel := avatar.get_node_or_null("Skeleton3D") as Skeleton3D
|
||||||
if body_meshes == null:
|
if skel == null:
|
||||||
return
|
return
|
||||||
|
|
||||||
for child in body_meshes.get_children():
|
for child in skel.get_children():
|
||||||
if child is MeshInstance3D:
|
if child is MeshInstance3D:
|
||||||
var mesh_name := child.name
|
# The single "Body" placeholder mesh is registered only under
|
||||||
var slot_id := _mesh_name_to_slot(mesh_name)
|
# "torso" -- mapping it to every body-part slot would make it
|
||||||
|
# vanish the moment any one body-part item is equipped. A real
|
||||||
|
# multi-mesh body registers each mesh under its own slot.
|
||||||
|
if child.name == "Body":
|
||||||
|
_base_meshes["torso"] = child
|
||||||
|
else:
|
||||||
|
var slot_id := _mesh_name_to_slot(child.name)
|
||||||
if slot_id != "":
|
if slot_id != "":
|
||||||
_base_meshes[slot_id] = child
|
_base_meshes[slot_id] = child
|
||||||
|
|
||||||
@@ -484,8 +468,12 @@ func _retarget_skin(attachment_node: Node3D) -> void:
|
|||||||
|
|
||||||
for surface_idx in surface_count:
|
for surface_idx in surface_count:
|
||||||
var arrays := arr_mesh.surface_get_arrays(surface_idx)
|
var arrays := arr_mesh.surface_get_arrays(surface_idx)
|
||||||
var joints_array := arrays[Mesh.ARRAY_BONES] as PackedInt32Array
|
# Static props (the common attachment) carry no bone/weight
|
||||||
var weights_array := arrays[Mesh.ARRAY_WEIGHTS] as PackedFloat32Array
|
# arrays at all -- nothing to retarget.
|
||||||
|
if arrays[Mesh.ARRAY_BONES] == null or arrays[Mesh.ARRAY_WEIGHTS] == null:
|
||||||
|
continue
|
||||||
|
var joints_array: PackedInt32Array = arrays[Mesh.ARRAY_BONES]
|
||||||
|
var weights_array: PackedFloat32Array = arrays[Mesh.ARRAY_WEIGHTS]
|
||||||
|
|
||||||
if joints_array.is_empty():
|
if joints_array.is_empty():
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
## Smoke test for the avatar catalog/slot/loadout/system layers.
|
## Smoke test for the avatar catalog/slot/loadout/system layers AND the full
|
||||||
|
## assembly path against the placeholder GLB asset pack
|
||||||
|
## (assets/avatars/*.glb, generated by scripts/tools/gen_placeholder_avatars.gd).
|
||||||
##
|
##
|
||||||
## Real .glb model assets don't exist yet (assets/avatars/catalog.json
|
## Covers: catalog parsing, slot registry, loadout equip/validate/round-trip,
|
||||||
## references them, the files themselves are a tracked user to-do -- see
|
## the AvatarSystem autoload wiring, and load_avatar() actually producing a
|
||||||
## the "AGP MVP status" project memory). This test covers everything that
|
## Skeleton3D with the canonical rig, a visible body mesh, and equipped
|
||||||
## doesn't require them (catalog parsing, slot registry, loadout equip/
|
## attachments mounted to the right bones.
|
||||||
## validate, the AvatarSystem orchestrator's autoload wiring) and confirms
|
|
||||||
## the one thing that DOES touch missing files -- AvatarLoader.load_base_body()
|
|
||||||
## -- fails gracefully (emits load_error, returns null) instead of crashing.
|
|
||||||
## Visual rendering itself can't be verified until real assets exist.
|
|
||||||
##
|
##
|
||||||
## Run with:
|
## Run with:
|
||||||
## godot --headless --path . --script res://scripts/smoke_test_avatar_system.gd
|
## godot --headless --path . --script res://scripts/smoke_test_avatar_system.gd
|
||||||
@@ -55,8 +53,9 @@ func _run() -> void:
|
|||||||
|
|
||||||
var hair_options: Array[String] = system.get_assets_for_slot("hair")
|
var hair_options: Array[String] = system.get_assets_for_slot("hair")
|
||||||
if hair_options.size() > 0:
|
if hair_options.size() > 0:
|
||||||
var equip_ok: bool = loadout.add_accessory(hair_options[0])
|
loadout.set_equipment("hair", hair_options[0])
|
||||||
_check("adding a real catalog accessory succeeds", equip_ok)
|
_check("equipping a real catalog hair item succeeds",
|
||||||
|
loadout.slots.get("hair") == hair_options[0])
|
||||||
|
|
||||||
# 4. Loadout serializes and survives a round trip.
|
# 4. Loadout serializes and survives a round trip.
|
||||||
var json_str: String = loadout.to_json()
|
var json_str: String = loadout.to_json()
|
||||||
@@ -64,25 +63,45 @@ func _run() -> void:
|
|||||||
var restored: AvatarLoadout = system.load_loadout_from_json(json_str)
|
var restored: AvatarLoadout = system.load_loadout_from_json(json_str)
|
||||||
_check("loadout round-trips through JSON", restored != null and restored.is_valid())
|
_check("loadout round-trips through JSON", restored != null and restored.is_valid())
|
||||||
|
|
||||||
# 5. The part that DOES need real files: confirm it fails gracefully
|
# 5. Full assembly against the placeholder GLB pack.
|
||||||
# rather than crashing, since assets/avatars/*.glb genuinely don't
|
|
||||||
# exist yet. A crash here would be a real bug; a clean load_error
|
|
||||||
# signal + null return is the correct, already-designed-for behavior.
|
|
||||||
if base_bodies.size() > 0:
|
if base_bodies.size() > 0:
|
||||||
# A plain bool captured by a lambda is captured BY VALUE in
|
var errors := [] as Array[String]
|
||||||
# GDScript -- mutating it inside the closure doesn't propagate to
|
var error_handler := func(msg): errors.append(msg)
|
||||||
# this outer scope. A single-element Array is the standard
|
|
||||||
# workaround (confirmed the hard way earlier in this same session,
|
|
||||||
# see scripts/smoke_test_network_client.gd's own comment on this).
|
|
||||||
var error_state := [false]
|
|
||||||
var error_handler := func(_msg): error_state[0] = true
|
|
||||||
system.loader.load_error.connect(error_handler)
|
system.loader.load_error.connect(error_handler)
|
||||||
var result: Node3D = system.loader.load_base_body(base_bodies[0])
|
|
||||||
|
var chest_options: Array[String] = system.get_assets_for_slot("chest")
|
||||||
|
if chest_options.size() > 0:
|
||||||
|
loadout.set_equipment("chest", chest_options[0])
|
||||||
|
var acc_options: Array[String] = system.get_assets_for_slot("accessory")
|
||||||
|
if acc_options.size() > 0:
|
||||||
|
loadout.add_accessory(acc_options[0])
|
||||||
|
|
||||||
|
var avatar: Node3D = system.load_avatar(loadout)
|
||||||
system.loader.load_error.disconnect(error_handler)
|
system.loader.load_error.disconnect(error_handler)
|
||||||
_check("load_base_body against a missing .glb returns null (not a crash)", result == null)
|
|
||||||
_check("load_base_body against a missing .glb emits load_error (not silent)", error_state[0])
|
_check("load_avatar returns a node (no load_error: %s)" % str(errors),
|
||||||
|
avatar != null and errors.is_empty())
|
||||||
|
if avatar:
|
||||||
|
var skel := avatar.get_node_or_null("Skeleton3D") as Skeleton3D
|
||||||
|
_check("assembled avatar has a Skeleton3D", skel != null)
|
||||||
|
if skel:
|
||||||
|
_check("skeleton carries the full canonical rig (24 bones)",
|
||||||
|
skel.get_bone_count() == 24)
|
||||||
|
_check("canonical bones resolve by name (Head, LeftHand, RightFoot)",
|
||||||
|
skel.find_bone("Head") != -1
|
||||||
|
and skel.find_bone("LeftHand") != -1
|
||||||
|
and skel.find_bone("RightFoot") != -1)
|
||||||
|
var body_meshes := 0
|
||||||
|
var bone_mounts := 0
|
||||||
|
for c in skel.get_children():
|
||||||
|
if c is MeshInstance3D and c.visible:
|
||||||
|
body_meshes += 1
|
||||||
|
if c is BoneAttachment3D:
|
||||||
|
bone_mounts += 1
|
||||||
|
_check("a visible body mesh is present", body_meshes >= 1)
|
||||||
|
_check("equipped hair + chest are mounted to bones", bone_mounts >= 2)
|
||||||
else:
|
else:
|
||||||
print(" SKIP: no base_body entries in catalog to test the failure path against")
|
print(" SKIP: no base_body entries in catalog")
|
||||||
|
|
||||||
_finish()
|
_finish()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Generates the placeholder avatar asset pack (GLB files) referenced by
|
||||||
|
## assets/avatars/catalog.json.
|
||||||
|
##
|
||||||
|
## These are deliberately crude primitive stand-ins so the avatar loader /
|
||||||
|
## customization / in-world rendering can be built and tested now; real art
|
||||||
|
## swaps in later as a pure content change (catalog paths are unchanged).
|
||||||
|
##
|
||||||
|
## Run: godot --headless --path . --script res://scripts/tools/gen_placeholder_avatars.gd
|
||||||
|
## Output: res://assets/avatars/*.glb (13 attachments + 1 rigged base body)
|
||||||
|
|
||||||
|
const OUT_DIR := "res://assets/avatars/"
|
||||||
|
|
||||||
|
# Canonical rig — must match avatar_loader.gd CANONICAL_BONES. Each entry:
|
||||||
|
# name -> [parent_name, global_rest_position] (T-pose, Y-up, metres)
|
||||||
|
const BONES := {
|
||||||
|
"Root": ["", Vector3(0.00, 0.00, 0.00)],
|
||||||
|
"Hips": ["Root", Vector3(0.00, 0.95, 0.00)],
|
||||||
|
"Spine": ["Hips", Vector3(0.00, 1.10, 0.00)],
|
||||||
|
"Chest": ["Spine", Vector3(0.00, 1.30, 0.00)],
|
||||||
|
"Neck": ["Chest", Vector3(0.00, 1.48, 0.00)],
|
||||||
|
"Head": ["Neck", Vector3(0.00, 1.60, 0.00)],
|
||||||
|
"HeadTop": ["Head", Vector3(0.00, 1.78, 0.00)],
|
||||||
|
"Jaw": ["Head", Vector3(0.00, 1.56, 0.06)],
|
||||||
|
"LeftEye": ["Head", Vector3(0.04, 1.64, 0.09)],
|
||||||
|
"RightEye": ["Head", Vector3(-0.04, 1.64, 0.09)],
|
||||||
|
"LeftShoulder": ["Chest", Vector3(0.06, 1.42, 0.00)],
|
||||||
|
"LeftUpperArm": ["LeftShoulder", Vector3(0.18, 1.42, 0.00)],
|
||||||
|
"LeftLowerArm": ["LeftUpperArm", Vector3(0.44, 1.42, 0.00)],
|
||||||
|
"LeftHand": ["LeftLowerArm", Vector3(0.68, 1.42, 0.00)],
|
||||||
|
"RightShoulder": ["Chest", Vector3(-0.06, 1.42, 0.00)],
|
||||||
|
"RightUpperArm": ["RightShoulder", Vector3(-0.18, 1.42, 0.00)],
|
||||||
|
"RightLowerArm": ["RightUpperArm", Vector3(-0.44, 1.42, 0.00)],
|
||||||
|
"RightHand": ["RightLowerArm", Vector3(-0.68, 1.42, 0.00)],
|
||||||
|
"LeftUpperLeg": ["Hips", Vector3(0.11, 0.90, 0.00)],
|
||||||
|
"LeftLowerLeg": ["LeftUpperLeg", Vector3(0.11, 0.50, 0.00)],
|
||||||
|
"LeftFoot": ["LeftLowerLeg", Vector3(0.11, 0.08, 0.04)],
|
||||||
|
"RightUpperLeg": ["Hips", Vector3(-0.11, 0.90, 0.00)],
|
||||||
|
"RightLowerLeg": ["RightUpperLeg", Vector3(-0.11, 0.50, 0.00)],
|
||||||
|
"RightFoot": ["RightLowerLeg", Vector3(-0.11, 0.08, 0.04)],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Order matters: parents before children.
|
||||||
|
const BONE_ORDER := [
|
||||||
|
"Root", "Hips", "Spine", "Chest", "Neck", "Head", "HeadTop", "Jaw",
|
||||||
|
"LeftEye", "RightEye",
|
||||||
|
"LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand",
|
||||||
|
"RightShoulder", "RightUpperArm", "RightLowerArm", "RightHand",
|
||||||
|
"LeftUpperLeg", "LeftLowerLeg", "LeftFoot",
|
||||||
|
"RightUpperLeg", "RightLowerLeg", "RightFoot",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Body segments: [boneA, boneB, radius] -> a capsule-ish box between the two,
|
||||||
|
# fully weighted to boneA. boneB "" means a small blob at boneA.
|
||||||
|
const SEGMENTS := [
|
||||||
|
["Hips", "Spine", 0.14],
|
||||||
|
["Spine", "Chest", 0.15],
|
||||||
|
["Chest", "Neck", 0.13],
|
||||||
|
["Neck", "Head", 0.05],
|
||||||
|
["Head", "", 0.11],
|
||||||
|
["LeftUpperArm", "LeftLowerArm", 0.055],
|
||||||
|
["LeftLowerArm", "LeftHand", 0.045],
|
||||||
|
["LeftHand", "", 0.05],
|
||||||
|
["RightUpperArm", "RightLowerArm", 0.055],
|
||||||
|
["RightLowerArm", "RightHand", 0.045],
|
||||||
|
["RightHand", "", 0.05],
|
||||||
|
["LeftUpperLeg", "LeftLowerLeg", 0.08],
|
||||||
|
["LeftLowerLeg", "LeftFoot", 0.06],
|
||||||
|
["LeftFoot", "", 0.06],
|
||||||
|
["RightUpperLeg", "RightLowerLeg", 0.08],
|
||||||
|
["RightLowerLeg", "RightFoot", 0.06],
|
||||||
|
["RightFoot", "", 0.06],
|
||||||
|
]
|
||||||
|
|
||||||
|
# Attachments: id -> [Mesh, local_offset_from_mount_bone, colour]
|
||||||
|
# Mesh is built in _attachment_mesh().
|
||||||
|
const ATTACHMENTS := {
|
||||||
|
"head_cyber_01": ["head", Vector3(0, 0.02, 0.0), Color(0.20, 0.85, 0.95)],
|
||||||
|
"hair_mohawk_01": ["mohawk", Vector3(0, 0.02, 0.0), Color(0.90, 0.20, 0.35)],
|
||||||
|
"hair_ponytail_01": ["ponytail", Vector3(0, 0.0, -0.08), Color(0.45, 0.30, 0.15)],
|
||||||
|
"face_glasses_01": ["glasses", Vector3(0, 0.06, 0.10), Color(0.10, 0.10, 0.12)],
|
||||||
|
"chest_bandit_01": ["torso", Vector3(0, 0.0, 0.0), Color(0.55, 0.20, 0.20)],
|
||||||
|
"back_backpack_01": ["box", Vector3(0, 0.0, -0.16), Color(0.30, 0.35, 0.25)],
|
||||||
|
"pants_cargo_01": ["hips", Vector3(0, -0.18, 0.0), Color(0.30, 0.35, 0.45)],
|
||||||
|
"left_hand_glove_01": ["blob", Vector3(0, 0.0, 0.0), Color(0.15, 0.15, 0.18)],
|
||||||
|
"right_hand_glove_01": ["blob", Vector3(0, 0.0, 0.0), Color(0.15, 0.15, 0.18)],
|
||||||
|
"left_foot_boot_01": ["boot", Vector3(0, -0.02, 0.03), Color(0.12, 0.10, 0.09)],
|
||||||
|
"right_foot_boot_01": ["boot", Vector3(0, -0.02, 0.03), Color(0.12, 0.10, 0.09)],
|
||||||
|
"accessory_belt_01": ["belt", Vector3(0, 0.98, 0.0), Color(0.20, 0.18, 0.12)],
|
||||||
|
"accessory_necklace_01": ["belt", Vector3(0, 1.34, 0.02), Color(0.85, 0.75, 0.20)],
|
||||||
|
}
|
||||||
|
|
||||||
|
var _fail := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var abs_dir := ProjectSettings.globalize_path(OUT_DIR)
|
||||||
|
DirAccess.make_dir_recursive_absolute(abs_dir)
|
||||||
|
|
||||||
|
_gen_base_body()
|
||||||
|
for id in ATTACHMENTS:
|
||||||
|
var spec: Array = ATTACHMENTS[id]
|
||||||
|
_gen_attachment(id, spec[0], spec[1], spec[2])
|
||||||
|
|
||||||
|
print("\n%s" % ("DONE — all placeholder avatar GLBs written" if _fail == 0
|
||||||
|
else "FAILED (%d errors)" % _fail))
|
||||||
|
quit(_fail)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Base body — rigged, skinned
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func _global_rest(bone: String) -> Transform3D:
|
||||||
|
return Transform3D(Basis.IDENTITY, BONES[bone][1])
|
||||||
|
|
||||||
|
|
||||||
|
func _gen_base_body() -> void:
|
||||||
|
var root := Node3D.new()
|
||||||
|
root.name = "AvatarRoot"
|
||||||
|
|
||||||
|
var skel := Skeleton3D.new()
|
||||||
|
skel.name = "Skeleton3D"
|
||||||
|
root.add_child(skel)
|
||||||
|
|
||||||
|
var idx := {}
|
||||||
|
for bone in BONE_ORDER:
|
||||||
|
var i := skel.add_bone(bone)
|
||||||
|
idx[bone] = i
|
||||||
|
for bone in BONE_ORDER:
|
||||||
|
var parent: String = BONES[bone][0]
|
||||||
|
var g := _global_rest(bone)
|
||||||
|
if parent == "":
|
||||||
|
skel.set_bone_rest(idx[bone], g)
|
||||||
|
else:
|
||||||
|
skel.set_bone_parent(idx[bone], idx[parent])
|
||||||
|
skel.set_bone_rest(idx[bone], _global_rest(parent).affine_inverse() * g)
|
||||||
|
for bone in BONE_ORDER:
|
||||||
|
skel.set_bone_pose_position(idx[bone], skel.get_bone_rest(idx[bone]).origin)
|
||||||
|
|
||||||
|
# Skin: one bind per bone, bind pose = inverse global rest.
|
||||||
|
var skin := Skin.new()
|
||||||
|
for bone in BONE_ORDER:
|
||||||
|
skin.add_named_bind(bone, _global_rest(bone).affine_inverse())
|
||||||
|
|
||||||
|
# Build one ArrayMesh surface from all segments, CPU-skinned to bind indices.
|
||||||
|
var verts := PackedVector3Array()
|
||||||
|
var normals := PackedVector3Array()
|
||||||
|
var bones := PackedInt32Array()
|
||||||
|
var weights := PackedFloat32Array()
|
||||||
|
|
||||||
|
var bind_index := {}
|
||||||
|
for b in range(BONE_ORDER.size()):
|
||||||
|
bind_index[BONE_ORDER[b]] = b
|
||||||
|
|
||||||
|
for seg in SEGMENTS:
|
||||||
|
var a: String = seg[0]
|
||||||
|
var b: String = seg[1]
|
||||||
|
var r: float = seg[2]
|
||||||
|
var pa: Vector3 = BONES[a][1]
|
||||||
|
var pb: Vector3 = pa + Vector3(0, 0.12, 0) if b == "" else BONES[b][1]
|
||||||
|
_append_box_between(verts, normals, bones, weights, pa, pb, r, bind_index[a])
|
||||||
|
|
||||||
|
var arr := []
|
||||||
|
arr.resize(Mesh.ARRAY_MAX)
|
||||||
|
arr[Mesh.ARRAY_VERTEX] = verts
|
||||||
|
arr[Mesh.ARRAY_NORMAL] = normals
|
||||||
|
arr[Mesh.ARRAY_BONES] = bones
|
||||||
|
arr[Mesh.ARRAY_WEIGHTS] = weights
|
||||||
|
|
||||||
|
var mesh := ArrayMesh.new()
|
||||||
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arr)
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = Color(0.78, 0.66, 0.55)
|
||||||
|
mesh.surface_set_material(0, mat)
|
||||||
|
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
mi.name = "Body"
|
||||||
|
mi.mesh = mesh
|
||||||
|
mi.skin = skin
|
||||||
|
skel.add_child(mi)
|
||||||
|
mi.skeleton = mi.get_path_to(skel)
|
||||||
|
|
||||||
|
_write_glb(root, "base_human_male_01")
|
||||||
|
root.free()
|
||||||
|
|
||||||
|
|
||||||
|
# Append an axis-aligned-ish box spanning pa..pb, all verts weighted 1.0 to bone_idx.
|
||||||
|
func _append_box_between(verts: PackedVector3Array, normals: PackedVector3Array,
|
||||||
|
bones: PackedInt32Array, weights: PackedFloat32Array,
|
||||||
|
pa: Vector3, pb: Vector3, r: float, bone_idx: int) -> void:
|
||||||
|
var axis := pb - pa
|
||||||
|
var len := axis.length()
|
||||||
|
if len < 0.001:
|
||||||
|
axis = Vector3(0, 0.1, 0)
|
||||||
|
len = 0.1
|
||||||
|
var dir := axis / len
|
||||||
|
var up := Vector3.UP if absf(dir.dot(Vector3.UP)) < 0.95 else Vector3.FORWARD
|
||||||
|
var right := dir.cross(up).normalized()
|
||||||
|
up = right.cross(dir).normalized()
|
||||||
|
|
||||||
|
var corners: Array[Vector3] = [
|
||||||
|
-right * r - up * r, right * r - up * r,
|
||||||
|
right * r + up * r, -right * r + up * r,
|
||||||
|
]
|
||||||
|
# 8 corners: bottom ring at pa, top ring at pb
|
||||||
|
var pts: Array[Vector3] = []
|
||||||
|
for c in corners:
|
||||||
|
pts.append(pa + c)
|
||||||
|
for c in corners:
|
||||||
|
pts.append(pb + c)
|
||||||
|
|
||||||
|
var faces := [
|
||||||
|
[0, 1, 2, 3], [7, 6, 5, 4], # caps
|
||||||
|
[0, 4, 5, 1], [1, 5, 6, 2], # sides
|
||||||
|
[2, 6, 7, 3], [3, 7, 4, 0],
|
||||||
|
]
|
||||||
|
for f in faces:
|
||||||
|
var q: Array[Vector3] = [pts[f[0]], pts[f[1]], pts[f[2]], pts[f[3]]]
|
||||||
|
var n: Vector3 = (q[1] - q[0]).cross(q[2] - q[0]).normalized()
|
||||||
|
for tri in [[0, 1, 2], [0, 2, 3]]:
|
||||||
|
for vi in tri:
|
||||||
|
verts.append(q[vi])
|
||||||
|
normals.append(n)
|
||||||
|
bones.append_array([bone_idx, 0, 0, 0])
|
||||||
|
weights.append_array([1.0, 0.0, 0.0, 0.0])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Attachments — plain static meshes
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func _gen_attachment(id: String, kind: String, offset: Vector3, col: Color) -> void:
|
||||||
|
var root := Node3D.new()
|
||||||
|
root.name = "AttachmentRoot"
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
mi.name = id
|
||||||
|
mi.mesh = _attachment_mesh(kind)
|
||||||
|
mi.position = offset
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = col
|
||||||
|
mi.material_override = mat
|
||||||
|
root.add_child(mi)
|
||||||
|
_write_glb(root, id)
|
||||||
|
root.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _attachment_mesh(kind: String) -> Mesh:
|
||||||
|
match kind:
|
||||||
|
"head":
|
||||||
|
var m := SphereMesh.new(); m.radius = 0.13; m.height = 0.26; return m
|
||||||
|
"mohawk":
|
||||||
|
var m := PrismMesh.new(); m.size = Vector3(0.06, 0.16, 0.30); return m
|
||||||
|
"ponytail":
|
||||||
|
var m := CapsuleMesh.new(); m.radius = 0.04; m.height = 0.28; return m
|
||||||
|
"glasses":
|
||||||
|
var m := BoxMesh.new(); m.size = Vector3(0.20, 0.05, 0.02); return m
|
||||||
|
"torso":
|
||||||
|
var m := BoxMesh.new(); m.size = Vector3(0.34, 0.44, 0.22); return m
|
||||||
|
"box":
|
||||||
|
var m := BoxMesh.new(); m.size = Vector3(0.26, 0.32, 0.14); return m
|
||||||
|
"hips":
|
||||||
|
var m := BoxMesh.new(); m.size = Vector3(0.32, 0.30, 0.20); return m
|
||||||
|
"blob":
|
||||||
|
var m := SphereMesh.new(); m.radius = 0.06; m.height = 0.12; return m
|
||||||
|
"boot":
|
||||||
|
var m := BoxMesh.new(); m.size = Vector3(0.10, 0.10, 0.20); return m
|
||||||
|
"belt":
|
||||||
|
var m := TorusMesh.new(); m.inner_radius = 0.12; m.outer_radius = 0.17; return m
|
||||||
|
_:
|
||||||
|
return BoxMesh.new()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# GLB export
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func _write_glb(scene_root: Node, basename: String) -> void:
|
||||||
|
# append_from_scene needs the node in a tree.
|
||||||
|
get_root().add_child(scene_root)
|
||||||
|
var doc := GLTFDocument.new()
|
||||||
|
var state := GLTFState.new()
|
||||||
|
var err := doc.append_from_scene(scene_root, state)
|
||||||
|
if err != OK:
|
||||||
|
push_error("append_from_scene failed for %s: %s" % [basename, error_string(err)])
|
||||||
|
_fail += 1
|
||||||
|
get_root().remove_child(scene_root)
|
||||||
|
return
|
||||||
|
var out_path := ProjectSettings.globalize_path("%s%s.glb" % [OUT_DIR, basename])
|
||||||
|
err = doc.write_to_filesystem(state, out_path)
|
||||||
|
get_root().remove_child(scene_root)
|
||||||
|
if err != OK:
|
||||||
|
push_error("write_to_filesystem failed for %s: %s" % [basename, error_string(err)])
|
||||||
|
_fail += 1
|
||||||
|
return
|
||||||
|
print(" wrote %s.glb (%d bytes)" % [basename, FileAccess.get_file_as_bytes(out_path).size()])
|
||||||
Reference in New Issue
Block a user