From ddcc0866b82374e0fda4f364f7c388fc1fc4c7d6 Mon Sep 17 00:00:00 2001 From: daddy Date: Sun, 20 Sep 2026 21:05:55 +0000 Subject: [PATCH] fix: Articulate pants with leg bones and fix walking animation clipping --- assets/avatars/pants_cargo_01.glb | Bin 5116 -> 5244 bytes scripts/avatar/avatar_loader.gd | 65 ++++++++++++++++++----- scripts/avatar/limb_animator.gd | 2 +- scripts/tools/gen_placeholder_avatars.gd | 31 +++++++---- 4 files changed, 73 insertions(+), 25 deletions(-) diff --git a/assets/avatars/pants_cargo_01.glb b/assets/avatars/pants_cargo_01.glb index 9a1d2415a4eaa2b455d81e3ceebbb23529263c8f..986462ecbdda733d6e431c1ca6f979b2f13d25ed 100644 GIT binary patch delta 1043 zcmZ{gPfHs?7{)gS+QX&^wu;z74=etotIp2s?CeImB|WqXp@q<%TG60u5-@*A_tr|h z6!avzpQA#_r5AJP2WWfiS@7yN$iaEDdBd2*g<+ogz0dnTGuvJ4E~QHcAKpx8n)Yo< zu19C-_v>Eo6OP4Q%cdq-wMdLSn=p?v&LyKfX2fRuURkNzd- z&zrT0jELuO$}ESvcJv~-FwELJ$sZ4M%yeW%tym3=tdR@D;%>Pb)WSl&9vX+{S7Wza zIoN9kH6#0p4jZ>ODvd9ZsoQfT{)Ej5cZnlkLbiswqe=ZqX1h=+mBWvXM$pU$B_j%T zIwQTNd70QfzvB#&JrF z&3M(iexPZuH{WR&Z?8IXz5Mn3yDxWGpn;)=9nQ!>Un+jkz`%zbJ;Tm|#QVXE5 z;7bkbzCpmzj~?*DP}re{95iyMA(wi22F`tjwCGleJxD` z4ZNdp&>h7O8vV%8gB<6pbNv2H_+5hl2!q0XgD?yO(60uly~x#>7~IdD?S-kA01c-0 I|Ht!*TSVlI0ssI2 delta 915 zcma)%u}i~16vnRxESk0!>Y^Z&LbYH_bC(nS$O1?k`5`1s!Mec#KVHCU;fT^wvr5kdyF^ef$0_Kv;blTmU>KdoyO()Rb%~R&2zawwv>9mw)Bk^_UuA?YK}(3$3iYSXMyfm z%lfcf?Zwt(vB(_X;f~O*y`-=0_I$>Mfe6D$@F?x}y86pzt+a!A+0^IU=V9bYCpCSZ z9peibFd0himZB>6Gg1argp@PAUtZL&CD#03#7tREir9?n_0MTSwmQ4yyY-R0_4^xy z+^SToTqlMG9~tmLzZg7d+OYSs8l zvj7+HYFOw!SMIEM_WeOY+tbde%5`FB@Dsxaoftf5 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()