113 lines
4.0 KiB
GDScript
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
|