extends SceneTree ## Dev tool: how much room does the collision solver actually HAVE? ## ## godot --headless --path . -s res://debug/cloth_allow_check.gd -- [skin_glb] ## ## SpringBones caps each cloth point's collider radius to just inside where that ## point rests, so the authored rest pose is a valid state and the idle does not ## buzz (see SpringBones._rest_clearances). That cap is also the ceiling on what ## the collision can ever do: a hull point resting 60 mm from a thigh's axis gets ## an allowance of 54 mm, so a 110 mm thigh can put 56 mm of itself inside that ## piece of cloth before a single constraint fires. ## ## This prints, per cloth bone, the gap between the limb's REAL radius and the ## allowance the solver is given — which is the clipping the solver is blind to ## by construction, before any tuning is considered. func _initialize() -> void: var args := OS.get_cmdline_user_args() var path: String = args[0] if args.size() > 0 \ else "res://assets/characters/skins/taila.glb" var scene := GLBLoader.load(path) if scene == null: print("could not load ", path) quit() return root.add_child(scene) var skel: Skeleton3D = _find(scene, "Skeleton3D") as Skeleton3D var side := path.get_basename() + ".rig.json" var info = JSON.parse_string(FileAccess.get_file_as_string(side)) if skel == null or typeof(info) != TYPE_DICTIONARY: print("no skeleton or sidecar") quit() return var cols: Array = [] for c in info.get("colliders", []): var a := skel.find_bone(String(c.get("bone", ""))) var b := skel.find_bone(String(c.get("child", ""))) if a < 0 or b < 0: continue var tail := float(c.get("radius_tail", c.get("radius", 0.1))) cols.append({ "name": String(c.get("bone", "")), "a": a, "b": b, "from": float(c.get("from", 0.0)), "lid": bool(c.get("lid", false)), "rh": float(c.get("radius_head", tail)), "rt": tail, }) print("\n=== how much of each limb the solver is blind to, per cloth bone ===") print(" BLIND = limb radius here - the allowance the rest-clearance cap gives\n") var rows: Array = [] for ch in info.get("chains", []): if String(ch.get("class", "")) not in SpringBones.DRAPE_CLASSES: continue var names: Array = ch.get("bones", []) var tips: Array = ch.get("tips", []) var hulls: Array = ch.get("hulls", []) for i in names.size(): var bi := skel.find_bone(String(names[i])) if bi < 0 or i >= tips.size(): continue var t: Array = tips[i] if t.size() != 3: continue var rest := skel.get_bone_global_rest(bi) var hull := PackedVector3Array() if i < hulls.size(): for h in hulls[i]: if h.size() == 3: hull.append(Vector3(h[0], h[1], h[2])) var pts := SpringBones._sample_points(rest, rest.origin, rest * Vector3(t[0], t[1], t[2]), hull) var worst := 0.0 var who := "" for col in cols: if col["lid"]: continue var a: Vector3 = skel.get_bone_global_rest(col["a"]).origin var b: Vector3 = skel.get_bone_global_rest(col["b"]).origin a = a.lerp(b, float(col["from"])) var ab := b - a var d2 := ab.length_squared() for p: Vector3 in pts: var u: float = 0.0 if d2 < 1e-9 \ else clampf((p - a).dot(ab) / d2, 0.0, 1.0) var d: float = p.distance_to(a + ab * u) var r: float = lerpf(float(col["rh"]), float(col["rt"]), u) # Exactly SpringBones._rest_clearances. var allow: float = maxf(r, d * 0.9) if d >= r else d * 0.9 if r - allow > worst: worst = r - allow who = String(col["name"]) if worst > 0.001: rows.append([worst, skel.get_bone_name(bi), who]) rows.sort_custom(func(x, y): return x[0] > y[0]) for r in rows.slice(0, 24): print(" %-26s BLIND %5.1f mm against %s" % [r[1], r[0] * 1000.0, r[2]]) print(" ... %d cloth bones have a blind band at all\n" % rows.size()) quit() func _find(node: Node, cls: String) -> Node: if node.is_class(cls): return node for c in node.get_children(): var f := _find(c, cls) if f: return f return null