extends SceneTree ## Dev tool: is the skirt STRETCHING around the thigh, or tearing open? ## ## godot --headless --path . -s res://debug/cloth_stretch_check.gd -- [skin_glb] ## ## The collision and drape solve each cloth bone on its own. Neighbouring skirt ## panels therefore get different answers, and the mesh between them has to ## absorb the difference — which linear-blend skinning does by pulling the shared ## edge apart. On screen that reads as the skirt "breaking" open around the thigh ## instead of deforming over it, and no capsule or spring number shows it, ## because every individual bone is behaving. ## ## So measure the MESH: skin every cloth triangle over a movement sweep and ## compare each edge against its own rest length. An edge whose two ends are ## driven by different panels is a SEAM — that is where a tear appears — so those ## are reported separately from edges inside one panel. ## ## Reports, worst over the sweep: ## stretch posed edge length / rest length ## gap how many millimetres that edge grew const SWEEP := [["ground", 9.0], ["ground", 3.0], ["air", 6.0], ["air", -8.0], ["slide", 10.0], ["dash", 14.0]] const FRAMES_PER_STATE := 30 ## An edge has to grow by more than this to count as a tear rather than noise. const REPORT_MM := 8.0 var _frames := 0 var _model: SkinnedPlayerModel = null var _cloth := {} # bone index -> panel family name var _edges: Array = [] # [mesh, surface, ia, ib, rest_len, family_a, family_b] var _skins: Array = [] # [mesh, skin, bone_of, verts, bones, weights, per] var _worst := {} # "famA|famB" -> [stretch, grow_m, bones, rest_m, state] ## The single worst edge seen, kept so _report can dump what actually drives it. var _peak := 0.0 var _peak_edge: Array = [] var _state := "" var _ready := false 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 := Node3D.new() root.add_child(scene) current_scene = scene _model = SkinnedPlayerModel.new() _model.model_path = path scene.add_child(_model) ## Panel a bone belongs to: the chain root's name, with the segments built by ## tools/retarget.py::subdivide_cloth_panels stripped off. Two segments of the ## same panel are meant to bend apart; two different panels are not. static func _family(bone_name: String) -> String: var n := bone_name var cut := n.find(".seg") return n.substr(0, cut) if cut >= 0 else n func _build(skel: Skeleton3D) -> void: var side: String = _model.model_path.get_basename() + ".rig.json" var info = JSON.parse_string(FileAccess.get_file_as_string(side)) if typeof(info) != TYPE_DICTIONARY: print("no sidecar") return for c in info.get("chains", []): if String(c.get("class", "")) == "hair": continue for n in c.get("bones", []): var i := skel.find_bone(String(n)) if i >= 0: _cloth[i] = _family(String(n)) for mi in _model.find_children("*", "MeshInstance3D", true, false): if mi.mesh == null or mi.skin == null: continue var skin: Skin = mi.skin var bone_of := {} for b in skin.get_bind_count(): var bi := skin.get_bind_bone(b) if bi < 0: bi = skel.find_bone(skin.get_bind_name(b)) bone_of[b] = bi for s in range(mi.mesh.get_surface_count()): var arrays: Array = mi.mesh.surface_get_arrays(s) var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX] var bones: PackedInt32Array = arrays[Mesh.ARRAY_BONES] var weights: PackedFloat32Array = arrays[Mesh.ARRAY_WEIGHTS] var idx: PackedInt32Array = arrays[Mesh.ARRAY_INDEX] if bones.is_empty() or verts.is_empty() or idx.is_empty(): continue var per: int = bones.size() / verts.size() var sk := [mi, skin, bone_of, verts, bones, weights, per] # Which panel drives each vertex, and its rest position. var fam := [] var drv := [] var rest := PackedVector3Array() var any := false fam.resize(verts.size()) drv.resize(verts.size()) rest.resize(verts.size()) for v in verts.size(): var bw := 0.0 var cw := 0.0 var f := "" var dn := "" var q := Vector3.ZERO for k in per: var w: float = weights[v * per + k] var bind: int = bones[v * per + k] var bi: int = bone_of[bind] if bi < 0 or w <= 0.0: continue q += (skel.get_bone_global_rest(bi) * skin.get_bind_pose(bind) * verts[v]) * w if _cloth.has(bi): cw += w if w > bw: bw = w f = _cloth[bi] dn = skel.get_bone_name(bi) # The cloth chains must actually OWN this vertex. Body surfaces # carry stray cloth influence — one arm vertex measured 0.54 # forearm, 0.35 skirt — and counting those made the skirt look # like it was tearing by half a metre when the arm was simply # moving during a dash. if cw < 0.75: f = "" dn = "" fam[v] = f drv[v] = dn rest[v] = q if f != "": any = true if not any: continue _skins.append(sk) var seen := {} for t in range(0, idx.size(), 3): for pair in [[idx[t], idx[t + 1]], [idx[t + 1], idx[t + 2]], [idx[t + 2], idx[t]]]: var a: int = mini(pair[0], pair[1]) var b: int = maxi(pair[0], pair[1]) if String(fam[a]) == "" or String(fam[b]) == "": continue var key := "%d_%d_%d" % [_skins.size(), a, b] if seen.has(key): continue seen[key] = true var L := rest[a].distance_to(rest[b]) if L < 0.0005: continue _edges.append([_skins.size() - 1, a, b, L, String(fam[a]), String(fam[b]), "%s -> %s" % [drv[a], drv[b]]]) var seams := 0 for e in _edges: if e[4] != e[5]: seams += 1 print("tracking %d cloth edges, %d of them across a panel seam" % [ _edges.size(), seams]) func _process(_delta: float) -> bool: _frames += 1 if _frames < 8 or not _model.loaded: return false var skel: Skeleton3D = _model.skeleton if skel == null: return true if not _ready: _build(skel) _ready = true if _edges.is_empty(): return true return false var phase: int = clampi((_frames - 9) / FRAMES_PER_STATE, 0, SWEEP.size() - 1) _model.update_state(SWEEP[phase][0], SWEEP[phase][1], false) _model.set_locomotion(0.0, 1.0, 0.0) _state = "%s %.0f" % [SWEEP[phase][0], SWEEP[phase][1]] _measure(skel) if _frames > 9 + FRAMES_PER_STATE * SWEEP.size(): _report() return true return false func _measure(skel: Skeleton3D) -> void: # Skin every cloth vertex once, then walk the edges. var posed: Array = [] for sk in _skins: var skin: Skin = sk[1] var bone_of: Dictionary = sk[2] var verts: PackedVector3Array = sk[3] var bones: PackedInt32Array = sk[4] var weights: PackedFloat32Array = sk[5] var per: int = sk[6] var out := PackedVector3Array() out.resize(verts.size()) for v in verts.size(): var q := Vector3.ZERO for k in per: var w: float = weights[v * per + k] var bind: int = bones[v * per + k] var bi: int = bone_of[bind] if bi < 0 or w <= 0.0: continue q += (skel.get_bone_global_pose(bi) * skin.get_bind_pose(bind) * verts[v]) * w out[v] = q posed.append(out) for e in _edges: var p: PackedVector3Array = posed[e[0]] var L: float = p[e[1]].distance_to(p[e[2]]) var grow: float = L - float(e[3]) if grow <= 0.0: continue var key: String = "%s | %s" % [e[4], e[5]] if e[4] != e[5] else "%s (inside)" % e[4] var cur: Array = _worst.get(key, [0.0, 0.0]) if grow > cur[1]: _worst[key] = [L / float(e[3]), grow, e[6], float(e[3]), _state] if grow > _peak: _peak = grow _peak_edge = [e[0], e[1], e[2], float(e[3]), _state] func _report() -> void: print("\n=== worst cloth EDGE STRETCH over the sweep ===") print(" a growing seam is the skirt tearing open between two panels;") print(" growth inside one panel is the panel itself being stretched.\n") var keys := _worst.keys() keys.sort_custom(func(a, b): return _worst[a][1] > _worst[b][1]) var shown := 0 for k in keys: var w: Array = _worst[k] if w[1] * 1000.0 < REPORT_MM: break print(" %-30s x%6.2f +%6.1f mm rest %5.1f mm %-42s %s" % [ k, w[0], w[1] * 1000.0, w[3] * 1000.0, w[2], w[4]]) shown += 1 if shown >= 24: break if shown == 0: print(" nothing grew by more than %.0f mm" % REPORT_MM) _dissect() print("") ## Everything that drives the two ends of the single worst edge. A rigid bone ## cannot change the distance between two points, so an edge that grew while ## both ends report the same DOMINANT bone is being pulled by something else in ## their influence lists — which is the only way to find out what. func _dissect() -> void: if _peak_edge.is_empty(): return var skel: Skeleton3D = _model.skeleton var sk: Array = _skins[_peak_edge[0]] var skin: Skin = sk[1] var bone_of: Dictionary = sk[2] var bones: PackedInt32Array = sk[4] var weights: PackedFloat32Array = sk[5] var per: int = sk[6] print(" worst single edge: rest %.1f mm, grew %.1f mm, during %s" % [ _peak_edge[3] * 1000.0, _peak * 1000.0, _peak_edge[4]]) for which in [1, 2]: var v: int = _peak_edge[which] var line := " vertex %d:" % v for k in per: var w: float = weights[v * per + k] if w <= 0.0001: continue var bi: int = bone_of[bones[v * per + k]] line += " %s %.2f" % [ skel.get_bone_name(bi) if bi >= 0 else "?", w] print(line)