diff --git a/assets/avatars/pants_cargo_01.glb b/assets/avatars/pants_cargo_01.glb index 9a1d241..986462e 100644 Binary files a/assets/avatars/pants_cargo_01.glb and b/assets/avatars/pants_cargo_01.glb differ diff --git a/scripts/avatar/avatar_loader.gd b/scripts/avatar/avatar_loader.gd index 403ac6f..19a7f12 100644 --- a/scripts/avatar/avatar_loader.gd +++ b/scripts/avatar/avatar_loader.gd @@ -191,23 +191,52 @@ func load_attachment(slot_id: String, asset_id: String) -> Node3D: # skeleton; static props (the common case) are unaffected. _retarget_skin(attachment_node) - # 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) + # Check if this attachment has multi-bone children (e.g. Attach_LeftUpperLeg, Attach_Hips). + var multi_bone_children: Array[Node] = [] + for child in attachment_node.get_children(): + var cname := child.name + if cname.begins_with("Attach_") or cname.begins_with("attach_"): + var target_bone := cname.substr(7) + if skeleton.find_bone(target_bone) != -1: + multi_bone_children.append(child) + 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: + if not multi_bone_children.is_empty(): + var created_bas: Array[BoneAttachment3D] = [] + for child in multi_bone_children: + var target_bone := child.name.substr(7) + var ba := BoneAttachment3D.new() + ba.name = "Mount_%s_%s" % [slot_id, target_bone] + ba.bone_name = target_bone + skeleton.add_child(ba) + attachment_node.remove_child(child) + ba.add_child(child) + created_bas.append(ba) + skeleton.add_child(attachment_node) + attachment_node.set_meta("bone_attachments", created_bas) + attachment_node.tree_exiting.connect(func(): + for ba in created_bas: + if is_instance_valid(ba) and not ba.is_queued_for_deletion(): + ba.queue_free() + ) mount_node = attachment_node + else: + # Single mount bone via a BoneAttachment3D so it follows the skeleton. + # Falls back to a plain skeleton child if the mount bone is missing. + var mount_bone: String = asset_meta.get("mount_bone", "") + if mount_bone.is_empty(): + mount_bone = _slot_to_mount_bone(slot_id) + 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 the top-level mount node so unload can free the whole unit. if slot_id == "accessory": @@ -228,11 +257,19 @@ func unload_attachment(slot_id: String) -> void: if slot_id == "accessory": for node in _attachments.get("accessory", []): if is_instance_valid(node): + if node.has_meta("bone_attachments"): + for ba in node.get_meta("bone_attachments"): + if is_instance_valid(ba): + ba.queue_free() node.queue_free() _attachments.erase("accessory") else: var node = _attachments.get(slot_id) if is_instance_valid(node): + if node.has_meta("bone_attachments"): + for ba in node.get_meta("bone_attachments"): + if is_instance_valid(ba): + ba.queue_free() node.queue_free() _attachments.erase(slot_id) diff --git a/scripts/avatar/limb_animator.gd b/scripts/avatar/limb_animator.gd index 5c95894..1ace7e5 100644 --- a/scripts/avatar/limb_animator.gd +++ b/scripts/avatar/limb_animator.gd @@ -12,7 +12,7 @@ extends Node const STRIDE := 0.42 ## peak hip swing, radians (~24 deg) const ARM_SWING := 0.32 ## peak shoulder swing, radians -const KNEE := 0.5 ## peak knee bend, radians +const KNEE := 0.0 ## peak knee bend, radians (0.0 for rigid placeholder limbs so feet/shoes don't dislocate) const CYCLE := 1.9 ## phase (radians) advanced per metre travelled const BLEND := 12.0 ## ease rate toward the target pose const MOVE_EPS := 0.35 ## m/s below which we treat the avatar as idle diff --git a/scripts/tools/gen_placeholder_avatars.gd b/scripts/tools/gen_placeholder_avatars.gd index f30703f..5f5a28e 100644 --- a/scripts/tools/gen_placeholder_avatars.gd +++ b/scripts/tools/gen_placeholder_avatars.gd @@ -458,27 +458,38 @@ func _gen_pants_cargo() -> void: pocket_mat.albedo_color = Color(0.20, 0.24, 0.32) # Dark pocket trim pocket_mat.roughness = 0.9 - # 1. Waist section (covering hips across both legs) + # 1. Waist mounted to Hips bone (Y=0.76) + var attach_hips := Node3D.new() + attach_hips.name = "Attach_Hips" + root.add_child(attach_hips) + var waist := MeshInstance3D.new() waist.name = "Waist" var waist_mesh := BoxMesh.new() - waist_mesh.size = Vector3(0.86, 0.20, 0.50) + waist_mesh.size = Vector3(0.88, 0.22, 0.52) waist.mesh = waist_mesh - waist.position = Vector3(0.0, -0.06, 0.0) # Relative to Hips bone (Y=0.76) + waist.position = Vector3(0.0, -0.05, 0.0) # Relative to Hips (Y=0.76), covers world Y 0.60 to 0.82 waist.material_override = pants_mat - root.add_child(waist) + attach_hips.add_child(waist) - # 2. Left and Right Pant Legs (legs are 0.40 wide, 0.78 tall, 0.46 deep, centers X = +-0.21) + # 2. Left and Right Pant Legs mounted to LeftUpperLeg and RightUpperLeg for side in [-1.0, 1.0]: var sname := "Right" if side < 0.0 else "Left" + var attach_leg := Node3D.new() + attach_leg.name = "Attach_%sUpperLeg" % sname + root.add_child(attach_leg) + var leg := MeshInstance3D.new() leg.name = "PantLeg_%s" % sname var leg_mesh := BoxMesh.new() - leg_mesh.size = Vector3(0.44, 0.64, 0.50) # Covers down to ankle + # Encases leg from hip joint down to ankle/sole. + # Relative to UpperLeg bone (Y=0.74), center Y=-0.37, height 0.76 covers Y=-0.75 to +0.01 (world Y=-0.01 to 0.75). + # Width 0.44 encloses 0.40 leg, Depth 0.50 encloses 0.46 leg. + leg_mesh.size = Vector3(0.44, 0.76, 0.50) leg.mesh = leg_mesh - leg.position = Vector3(side * 0.21, -0.34, 0.0) + leg.position = Vector3(0.0, -0.37, 0.0) leg.material_override = pants_mat - root.add_child(leg) + attach_leg.add_child(leg) # Cargo pocket on outer side var pocket := MeshInstance3D.new() @@ -486,9 +497,9 @@ func _gen_pants_cargo() -> void: var pocket_mesh := BoxMesh.new() pocket_mesh.size = Vector3(0.08, 0.20, 0.24) pocket.mesh = pocket_mesh - pocket.position = Vector3(side * 0.44, -0.32, 0.0) + pocket.position = Vector3(side * 0.23, -0.32, 0.0) pocket.material_override = pocket_mat - root.add_child(pocket) + attach_leg.add_child(pocket) _write_glb(root, "pants_cargo_01") root.free()