Files
Papay-Shooter/debug/cloth_allow_check.gd
2026-08-02 02:20:02 -04:00

113 lines
4.0 KiB
GDScript

extends SceneTree
const CLOTH_CLASSES := ["skirt", "cloth", "cape", "coat", "scarf", "ribbon", "sleeve"]
## 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 CLOTH_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