diff --git a/assets/avatars/accessory_belt_01.glb b/assets/avatars/accessory_belt_01.glb new file mode 100644 index 0000000..90381c5 Binary files /dev/null and b/assets/avatars/accessory_belt_01.glb differ diff --git a/assets/avatars/accessory_belt_01.glb.import b/assets/avatars/accessory_belt_01.glb.import new file mode 100644 index 0000000..7b4f62a --- /dev/null +++ b/assets/avatars/accessory_belt_01.glb.import @@ -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 diff --git a/assets/avatars/accessory_necklace_01.glb b/assets/avatars/accessory_necklace_01.glb new file mode 100644 index 0000000..d92f58c Binary files /dev/null and b/assets/avatars/accessory_necklace_01.glb differ diff --git a/assets/avatars/accessory_necklace_01.glb.import b/assets/avatars/accessory_necklace_01.glb.import new file mode 100644 index 0000000..fceffc9 --- /dev/null +++ b/assets/avatars/accessory_necklace_01.glb.import @@ -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 diff --git a/assets/avatars/back_backpack_01.glb b/assets/avatars/back_backpack_01.glb new file mode 100644 index 0000000..7e79fcd Binary files /dev/null and b/assets/avatars/back_backpack_01.glb differ diff --git a/assets/avatars/back_backpack_01.glb.import b/assets/avatars/back_backpack_01.glb.import new file mode 100644 index 0000000..a0e275d --- /dev/null +++ b/assets/avatars/back_backpack_01.glb.import @@ -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 diff --git a/assets/avatars/base_human_male_01.glb b/assets/avatars/base_human_male_01.glb new file mode 100644 index 0000000..15d54f2 Binary files /dev/null and b/assets/avatars/base_human_male_01.glb differ diff --git a/assets/avatars/base_human_male_01.glb.import b/assets/avatars/base_human_male_01.glb.import new file mode 100644 index 0000000..6fca9a3 --- /dev/null +++ b/assets/avatars/base_human_male_01.glb.import @@ -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 diff --git a/assets/avatars/chest_bandit_01.glb b/assets/avatars/chest_bandit_01.glb new file mode 100644 index 0000000..8bc2b83 Binary files /dev/null and b/assets/avatars/chest_bandit_01.glb differ diff --git a/assets/avatars/chest_bandit_01.glb.import b/assets/avatars/chest_bandit_01.glb.import new file mode 100644 index 0000000..dc420d9 --- /dev/null +++ b/assets/avatars/chest_bandit_01.glb.import @@ -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 diff --git a/assets/avatars/face_glasses_01.glb b/assets/avatars/face_glasses_01.glb new file mode 100644 index 0000000..1c6339e Binary files /dev/null and b/assets/avatars/face_glasses_01.glb differ diff --git a/assets/avatars/face_glasses_01.glb.import b/assets/avatars/face_glasses_01.glb.import new file mode 100644 index 0000000..6ecb2cb --- /dev/null +++ b/assets/avatars/face_glasses_01.glb.import @@ -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 diff --git a/assets/avatars/hair_mohawk_01.glb b/assets/avatars/hair_mohawk_01.glb new file mode 100644 index 0000000..0f1cc97 Binary files /dev/null and b/assets/avatars/hair_mohawk_01.glb differ diff --git a/assets/avatars/hair_mohawk_01.glb.import b/assets/avatars/hair_mohawk_01.glb.import new file mode 100644 index 0000000..1ce9db1 --- /dev/null +++ b/assets/avatars/hair_mohawk_01.glb.import @@ -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 diff --git a/assets/avatars/hair_ponytail_01.glb b/assets/avatars/hair_ponytail_01.glb new file mode 100644 index 0000000..7c1a03f Binary files /dev/null and b/assets/avatars/hair_ponytail_01.glb differ diff --git a/assets/avatars/hair_ponytail_01.glb.import b/assets/avatars/hair_ponytail_01.glb.import new file mode 100644 index 0000000..eef5bb4 --- /dev/null +++ b/assets/avatars/hair_ponytail_01.glb.import @@ -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 diff --git a/assets/avatars/head_cyber_01.glb b/assets/avatars/head_cyber_01.glb new file mode 100644 index 0000000..6bce48d Binary files /dev/null and b/assets/avatars/head_cyber_01.glb differ diff --git a/assets/avatars/head_cyber_01.glb.import b/assets/avatars/head_cyber_01.glb.import new file mode 100644 index 0000000..beee6fb --- /dev/null +++ b/assets/avatars/head_cyber_01.glb.import @@ -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 diff --git a/assets/avatars/left_foot_boot_01.glb b/assets/avatars/left_foot_boot_01.glb new file mode 100644 index 0000000..0fbdbb2 Binary files /dev/null and b/assets/avatars/left_foot_boot_01.glb differ diff --git a/assets/avatars/left_foot_boot_01.glb.import b/assets/avatars/left_foot_boot_01.glb.import new file mode 100644 index 0000000..abad444 --- /dev/null +++ b/assets/avatars/left_foot_boot_01.glb.import @@ -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 diff --git a/assets/avatars/left_hand_glove_01.glb b/assets/avatars/left_hand_glove_01.glb new file mode 100644 index 0000000..fa634a5 Binary files /dev/null and b/assets/avatars/left_hand_glove_01.glb differ diff --git a/assets/avatars/left_hand_glove_01.glb.import b/assets/avatars/left_hand_glove_01.glb.import new file mode 100644 index 0000000..20cde88 --- /dev/null +++ b/assets/avatars/left_hand_glove_01.glb.import @@ -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 diff --git a/assets/avatars/pants_cargo_01.glb b/assets/avatars/pants_cargo_01.glb new file mode 100644 index 0000000..4c24171 Binary files /dev/null and b/assets/avatars/pants_cargo_01.glb differ diff --git a/assets/avatars/pants_cargo_01.glb.import b/assets/avatars/pants_cargo_01.glb.import new file mode 100644 index 0000000..d165cc0 --- /dev/null +++ b/assets/avatars/pants_cargo_01.glb.import @@ -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 diff --git a/assets/avatars/right_foot_boot_01.glb b/assets/avatars/right_foot_boot_01.glb new file mode 100644 index 0000000..1c8e6a4 Binary files /dev/null and b/assets/avatars/right_foot_boot_01.glb differ diff --git a/assets/avatars/right_foot_boot_01.glb.import b/assets/avatars/right_foot_boot_01.glb.import new file mode 100644 index 0000000..72145ca --- /dev/null +++ b/assets/avatars/right_foot_boot_01.glb.import @@ -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 diff --git a/assets/avatars/right_hand_glove_01.glb b/assets/avatars/right_hand_glove_01.glb new file mode 100644 index 0000000..c7ecc04 Binary files /dev/null and b/assets/avatars/right_hand_glove_01.glb differ diff --git a/assets/avatars/right_hand_glove_01.glb.import b/assets/avatars/right_hand_glove_01.glb.import new file mode 100644 index 0000000..362d09c --- /dev/null +++ b/assets/avatars/right_hand_glove_01.glb.import @@ -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 diff --git a/scripts/avatar/avatar_loader.gd b/scripts/avatar/avatar_loader.gd index 629dc22..c8242c8 100644 --- a/scripts/avatar/avatar_loader.gd +++ b/scripts/avatar/avatar_loader.gd @@ -175,7 +175,7 @@ func load_attachment(slot_id: String, asset_id: String) -> Node3D: # Check slot exclusivity for accessory slot. 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: load_error.emit("Accessory slot full (%d/%d)" % [ 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) 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) - # Parent static accessories to their mount bone. - _parent_attachment(attachment_node, slot_id, asset_meta) + # Mount to the appropriate bone via a BoneAttachment3D so it follows the + # 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": - var list: Array[String] = _attachments.get("accessory", []) - list.append(asset_id) + var list: Array = _attachments.get("accessory", []) + list.append(mount_node) _attachments["accessory"] = list 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) return attachment_node @@ -213,15 +231,13 @@ func load_attachment(slot_id: String, asset_id: String) -> Node3D: ## Remove an attachment from the given slot. func unload_attachment(slot_id: String) -> void: if slot_id == "accessory": - var list: Array[String] = _attachments.get("accessory", []) - for asset_id in list: - var node = _attachments.get(asset_id) - if node and node is Node: + for node in _attachments.get("accessory", []): + if is_instance_valid(node): node.queue_free() _attachments.erase("accessory") else: var node = _attachments.get(slot_id) - if node and node is Node: + if is_instance_valid(node): node.queue_free() _attachments.erase(slot_id) @@ -280,58 +296,73 @@ func reset() -> void: # Internal: GLB Parsing # --------------------------------------------------------------------------- -## Parse a GLB byte array into an avatar Node3D structure. -## Creates: Avatar (Node3D) -> Skeleton3D + BodyMeshes + AttachmentPoints +## Parse a GLB byte array into an avatar Node3D structure: +## 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: - 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_state := GLTFState.new() var parse_result := gltf_document.append_from_buffer(glb_data, "", gltf_state) if parse_result != OK: push_error("GLB parse error: %s" % error_string(parse_result)) - avatar.queue_free() return null - # Generate the actual scene tree from the parsed 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) - avatar.queue_free() + if scene_root: + scene_root.queue_free() return null - # Merge GLB nodes into our avatar structure (recurses into children). - if scene_root is Node3D: - _merge_node_into_avatar(scene_root, avatar, body_meshes, - attach_points, attachments_container) + var src_skel := _find_first_skeleton(scene_root) + if src_skel == null: + push_error("No Skeleton3D in generated scene for base body: %s" % path) + 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 +## 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. func _parse_glb_as_attachment(glb_data: PackedByteArray, path: String, slot_id: String) -> Node3D: @@ -345,99 +376,52 @@ func _parse_glb_as_attachment(glb_data: PackedByteArray, path: String, return null 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) + if scene_root: + scene_root.queue_free() 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() attachment.name = "%s_%s" % [slot_id, path.get_file().get_basename()] - - if scene_root is Node3D: - attachment.add_child(scene_root.duplicate()) - + for child in scene_root.get_children().duplicate(): + scene_root.remove_child(child) + _clear_owner_recursive(child) + attachment.add_child(child) + scene_root.queue_free() return attachment -## Merge a GLB node tree into the avatar's container structure. -func _merge_node_into_avatar(node: Node3D, avatar: Node3D, - body_meshes: Node3D, attach_points: Node3D, - attachments_container: Node3D) -> void: - - 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. +## generate_scene() stamps every node with the GLB root as `owner`; that +## becomes inconsistent (and noisy) once we reparent the node elsewhere. +func _clear_owner_recursive(node: Node) -> void: + node.owner = null for child in node.get_children(): - if child is Node3D: - _merge_node_into_avatar(child, avatar, body_meshes, - attach_points, attachments_container) + _clear_owner_recursive(child) -## 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: - var body_meshes := avatar.get_node_or_null("BodyMeshes") as Node3D - if body_meshes == null: + var skel := avatar.get_node_or_null("Skeleton3D") as Skeleton3D + if skel == null: return - for child in body_meshes.get_children(): + for child in skel.get_children(): if child is MeshInstance3D: - var mesh_name := child.name - var slot_id := _mesh_name_to_slot(mesh_name) - if slot_id != "": - _base_meshes[slot_id] = child + # The single "Body" placeholder mesh is registered only under + # "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 != "": + _base_meshes[slot_id] = child ## Map a mesh name to a slot id based on naming conventions (§4.3). @@ -484,8 +468,12 @@ func _retarget_skin(attachment_node: Node3D) -> void: for surface_idx in surface_count: var arrays := arr_mesh.surface_get_arrays(surface_idx) - var joints_array := arrays[Mesh.ARRAY_BONES] as PackedInt32Array - var weights_array := arrays[Mesh.ARRAY_WEIGHTS] as PackedFloat32Array + # Static props (the common attachment) carry no bone/weight + # 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(): continue diff --git a/scripts/smoke_test_avatar_system.gd b/scripts/smoke_test_avatar_system.gd index e7fac75..7b7db06 100644 --- a/scripts/smoke_test_avatar_system.gd +++ b/scripts/smoke_test_avatar_system.gd @@ -1,14 +1,12 @@ 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 -## references them, the files themselves are a tracked user to-do -- see -## the "AGP MVP status" project memory). This test covers everything that -## doesn't require them (catalog parsing, slot registry, loadout equip/ -## 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. +## Covers: catalog parsing, slot registry, loadout equip/validate/round-trip, +## the AvatarSystem autoload wiring, and load_avatar() actually producing a +## Skeleton3D with the canonical rig, a visible body mesh, and equipped +## attachments mounted to the right bones. ## ## Run with: ## 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") if hair_options.size() > 0: - var equip_ok: bool = loadout.add_accessory(hair_options[0]) - _check("adding a real catalog accessory succeeds", equip_ok) + loadout.set_equipment("hair", hair_options[0]) + _check("equipping a real catalog hair item succeeds", + loadout.slots.get("hair") == hair_options[0]) # 4. Loadout serializes and survives a round trip. var json_str: String = loadout.to_json() @@ -64,25 +63,45 @@ func _run() -> void: var restored: AvatarLoadout = system.load_loadout_from_json(json_str) _check("loadout round-trips through JSON", restored != null and restored.is_valid()) - # 5. The part that DOES need real files: confirm it fails gracefully - # 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. + # 5. Full assembly against the placeholder GLB pack. if base_bodies.size() > 0: - # A plain bool captured by a lambda is captured BY VALUE in - # GDScript -- mutating it inside the closure doesn't propagate to - # 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 + var errors := [] as Array[String] + var error_handler := func(msg): errors.append(msg) 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) - _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: - print(" SKIP: no base_body entries in catalog to test the failure path against") + print(" SKIP: no base_body entries in catalog") _finish() diff --git a/scripts/tools/gen_placeholder_avatars.gd b/scripts/tools/gen_placeholder_avatars.gd new file mode 100644 index 0000000..6878faf --- /dev/null +++ b/scripts/tools/gen_placeholder_avatars.gd @@ -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()])