385 lines
15 KiB
GDScript
385 lines
15 KiB
GDScript
extends Object
|
|
class_name SkinLegRepair
|
|
|
|
## Keeps the HUMANOID rigid while letting the CLOTHES flow.
|
|
##
|
|
## That split is the whole design rule here, and it decides what every pass below
|
|
## is allowed to touch:
|
|
##
|
|
## * LIMB vertices — anything hugging a leg's own bone chain: skin, stockings,
|
|
## boots — are cleaned up hard. They belong to one leg, they follow it, and
|
|
## they hold their shape. Deforming here is a bug.
|
|
## * DRAPE vertices — the skirt and anything else hanging clear of both leg
|
|
## chains — are LEFT ALONE, so they keep swinging with the body.
|
|
##
|
|
## DO NOT stiffen the skirt onto the hips. It was tried (to stop it flattening at
|
|
## a wide stride) and reverted: it makes the clothes read as a rigid shell, which
|
|
## is worse than the flattening it fixed. Cloth is supposed to move.
|
|
##
|
|
## Current split, from debug/limb_deform_check.gd: the body holds 0.98 of its
|
|
## cross-section at the knee, while the skirt is free at 0.88 — rigid limbs,
|
|
## moving cloth.
|
|
##
|
|
##
|
|
## Stops below-the-knee geometry being dragged by BOTH legs at once.
|
|
##
|
|
## Taila's boots are skinned with weights that bleed across the centre line:
|
|
## 262 vertices in the boot/cuff surface (plus 208 in the model's outline shell
|
|
## and 176 in the body) carry weight from the left AND right leg, the worst at a
|
|
## dead-even 49/51 split. A vertex pulled equally by both feet sits halfway
|
|
## between them and stays there while the legs separate, stretching every
|
|
## triangle around it. That is the "ankle cuffs are linked" stretching, and the
|
|
## elongated boot that reads as the legs being squashed.
|
|
##
|
|
## Everything else about the rig is fine, which is why this took so long to
|
|
## find. Measured through the full runtime stack during a run: no bone's pose
|
|
## basis deviates from a pure rotation by more than 0.00001, no bone's length
|
|
## drifts from its rest offset by more than 0.0000 m, and no below-knee vertex
|
|
## is influenced by any non-leg bone. The skeleton is correct; the weights are
|
|
## not. Freezing the AnimationTree at the rest pose renders the boots perfectly,
|
|
## which is what proves it is a skinning problem rather than a pose one.
|
|
##
|
|
## Three steps:
|
|
##
|
|
## 1. BELOW THE KNEE (height taken from the skeleton's own rest pose, so this
|
|
## scales to any character): snap each vertex to the leg that already
|
|
## dominates it and renormalise, so nothing is pulled in two directions.
|
|
## 2. ABOVE THE KNEE, only for vertices that are part of a LEG rather than
|
|
## draping cloth: same snap, to the nearer leg. 122 vertices on Taila's
|
|
## thigh mesh carry up to 35% of the opposite leg — that is the squashing
|
|
## that survived the below-knee pass and showed up when running and
|
|
## jumping split the legs. Limb membership is decided by distance to the
|
|
## leg's own bone chain, NOT by height or by surface name: a thigh vertex
|
|
## hugs its bone, while a skirt vertex hangs well clear of both and is
|
|
## left blended, which is what lets a skirt drape across both legs.
|
|
## 3. Cap how much the TORSO owns a leg vertex. The top of the thigh is
|
|
## weighted between the hips and the thigh; at a wide stride, linear-blend
|
|
## skinning averages the near-static hips against a thigh swung 60 degrees
|
|
## out, and the top of the leg flattens into a wedge. That is the hip
|
|
## collapse visible from the side while running and jumping, and it is the
|
|
## worst number the measurement reports (cross-section 0.85 at the hip
|
|
## against 0.97-0.99 at the knee). Leg vertices keep at most MAX_TORSO of
|
|
## hips/spine influence, and the excess goes to the leg bone that already
|
|
## dominates them, so the thigh follows its own bone.
|
|
## 4. Drop any triangle still spanning the two legs below the knee. Those are
|
|
## the midline band between the ankles, which has no correct pose either
|
|
## way.
|
|
##
|
|
## Measure with debug/limb_deform_check.gd.
|
|
|
|
const LEG_BONE_HINTS := ["thigh", "shin", "foot", "toe"]
|
|
## Bones that belong to the torso, not the leg.
|
|
const TORSO_BONE_HINTS := ["hips", "spine", "pelvis"]
|
|
## The most torso influence a leg vertex may keep. Some is wanted — it is what
|
|
## rounds the hip off — but past this the thigh stops following its own bone.
|
|
const MAX_TORSO := 0.15
|
|
## Ignore influences below this — they are rounding, not real weighting.
|
|
const EPSILON := 0.005
|
|
|
|
|
|
## Returns [vertices_snapped, triangles_removed] so callers can log the result.
|
|
static func repair(root: Node, skeleton: Skeleton3D) -> Array:
|
|
var knee := _knee_height(skeleton)
|
|
if is_nan(knee):
|
|
return [0, 0]
|
|
var chain_l := _leg_chain(skeleton, ".L")
|
|
var chain_r := _leg_chain(skeleton, ".R")
|
|
if chain_l.is_empty() or chain_r.is_empty():
|
|
return [0, 0]
|
|
# "Part of a leg" means within roughly half the gap between the two legs of
|
|
# that leg's bone chain — self-scaling to the character's proportions.
|
|
var limb_radius: float = maxf(absf(chain_l[0].x - chain_r[0].x) * 0.5, 0.02)
|
|
|
|
var snapped_total := 0
|
|
var removed_total := 0
|
|
for mi in root.find_children("*", "MeshInstance3D", true, false):
|
|
if mi.mesh == null or mi.skin == null:
|
|
continue
|
|
# Rebuilding a mesh drops blend shapes, so a skin that uses them (a face
|
|
# rig) is left alone rather than silently losing its expressions.
|
|
if mi.mesh.get_blend_shape_count() > 0:
|
|
continue
|
|
var r := _repair_mesh(mi, skeleton, knee, chain_l, chain_r, limb_radius)
|
|
snapped_total += r[0]
|
|
removed_total += r[1]
|
|
return [snapped_total, removed_total]
|
|
|
|
|
|
## Rest-pose joint positions down one leg, used as a polyline to measure how
|
|
## close a vertex sits to that limb.
|
|
static func _leg_chain(skeleton: Skeleton3D, suffix: String) -> PackedVector3Array:
|
|
var out := PackedVector3Array()
|
|
for stem in ["DEF-thigh", "DEF-shin", "DEF-foot", "DEF-toe"]:
|
|
var i := skeleton.find_bone(stem + suffix)
|
|
if i < 0:
|
|
i = skeleton.find_bone(stem.trim_prefix("DEF-") + suffix)
|
|
if i >= 0:
|
|
out.append(skeleton.get_bone_global_rest(i).origin)
|
|
return out
|
|
|
|
|
|
## Distance from a point to a polyline.
|
|
static func _dist_to_chain(p: Vector3, chain: PackedVector3Array) -> float:
|
|
var best := INF
|
|
for i in range(chain.size() - 1):
|
|
var a: Vector3 = chain[i]
|
|
var b: Vector3 = chain[i + 1]
|
|
var ab: Vector3 = b - a
|
|
var len2: float = ab.length_squared()
|
|
var t: float = 0.0 if len2 < 0.000001 else clampf((p - a).dot(ab) / len2, 0.0, 1.0)
|
|
best = minf(best, p.distance_to(a + ab * t))
|
|
return best
|
|
|
|
|
|
static func _knee_height(skeleton: Skeleton3D) -> float:
|
|
if skeleton == null:
|
|
return NAN
|
|
for n in ["DEF-shin.L", "shin.L", "DEF-shin.R", "shin.R"]:
|
|
var i := skeleton.find_bone(n)
|
|
if i >= 0:
|
|
return skeleton.get_bone_global_rest(i).origin.y
|
|
return NAN
|
|
|
|
|
|
static func _repair_mesh(mi: MeshInstance3D, skeleton: Skeleton3D, knee: float,
|
|
chain_l: PackedVector3Array, chain_r: PackedVector3Array,
|
|
limb_radius: float) -> Array:
|
|
var safe_bounds := mi.mesh.get_aabb()
|
|
var side := _side_map(mi.skin, skeleton)
|
|
var torso_bone := _torso_map(mi.skin, skeleton)
|
|
var surfaces: Array = []
|
|
var snapped := 0
|
|
var removed := 0
|
|
for s in range(mi.mesh.get_surface_count()):
|
|
var arrays: Array = mi.mesh.surface_get_arrays(s)
|
|
var r := _repair_surface(arrays, side, torso_bone, knee, chain_l, chain_r, limb_radius)
|
|
snapped += r[0]
|
|
removed += r[1]
|
|
surfaces.append({
|
|
"arrays": arrays,
|
|
"material": mi.mesh.surface_get_material(s),
|
|
"name": mi.mesh.surface_get_name(s),
|
|
})
|
|
if snapped == 0 and removed == 0:
|
|
return [0, 0]
|
|
|
|
var rebuilt := ArrayMesh.new()
|
|
for i in surfaces.size():
|
|
var entry: Dictionary = surfaces[i]
|
|
rebuilt.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, entry["arrays"])
|
|
rebuilt.surface_set_material(i, entry["material"])
|
|
if entry["name"] != "":
|
|
rebuilt.surface_set_name(i, entry["name"])
|
|
# ArrayMesh rebuilds per-bind bone bounds from ARRAY_BONES. Some runtime
|
|
# GLBs have a sparse Skin bind table, so that derived array can be longer
|
|
# than the renderer skeleton and mesh_get_aabb rejects it. A padded bind-pose
|
|
# bound is conservative for humanoid animation and avoids that invalid path.
|
|
var padding := Vector3.ONE * maxf(safe_bounds.size.length() * 0.12, 0.25)
|
|
safe_bounds.position -= padding
|
|
safe_bounds.size += padding * 2.0
|
|
rebuilt.custom_aabb = safe_bounds
|
|
mi.mesh = rebuilt
|
|
return [snapped, removed]
|
|
|
|
|
|
## Is each bind a torso bone? Keyed by SKIN BIND index, like _side_map.
|
|
static func _torso_map(skin: Skin, skeleton: Skeleton3D) -> Array:
|
|
var out: Array = []
|
|
out.resize(skin.get_bind_count())
|
|
for b in skin.get_bind_count():
|
|
var n := skin.get_bind_name(b)
|
|
if n == "":
|
|
var bone := skin.get_bind_bone(b)
|
|
n = skeleton.get_bone_name(bone) if bone >= 0 else ""
|
|
out[b] = false
|
|
for hint in TORSO_BONE_HINTS:
|
|
if n.findn(hint) != -1:
|
|
out[b] = true
|
|
break
|
|
return out
|
|
|
|
|
|
## -1 left leg, +1 right leg, 0 anything else — keyed by SKIN BIND index, which
|
|
## is what ARRAY_BONES stores (not the skeleton's bone index).
|
|
static func _side_map(skin: Skin, skeleton: Skeleton3D) -> PackedInt32Array:
|
|
var out := PackedInt32Array()
|
|
out.resize(skin.get_bind_count())
|
|
for b in skin.get_bind_count():
|
|
var n := skin.get_bind_name(b)
|
|
if n == "":
|
|
var bone := skin.get_bind_bone(b)
|
|
n = skeleton.get_bone_name(bone) if bone >= 0 else ""
|
|
var is_leg := false
|
|
for hint in LEG_BONE_HINTS:
|
|
if n.findn(hint) != -1:
|
|
is_leg = true
|
|
break
|
|
if not is_leg:
|
|
out[b] = 0
|
|
elif n.ends_with(".L"):
|
|
out[b] = -1
|
|
elif n.ends_with(".R"):
|
|
out[b] = 1
|
|
else:
|
|
out[b] = 0
|
|
return out
|
|
|
|
|
|
static func _repair_surface(arrays: Array, side: PackedInt32Array,
|
|
torso_bone: Array, knee: float,
|
|
chain_l: PackedVector3Array, chain_r: PackedVector3Array,
|
|
limb_radius: float) -> Array:
|
|
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():
|
|
return [0, 0]
|
|
var per: int = bones.size() / verts.size()
|
|
|
|
# Step 1 — one leg per vertex.
|
|
var vside := PackedInt32Array()
|
|
vside.resize(verts.size())
|
|
var snapped := 0
|
|
for v in verts.size():
|
|
var wl := 0.0
|
|
var wr := 0.0
|
|
for k in per:
|
|
var w: float = weights[v * per + k]
|
|
if w <= EPSILON:
|
|
continue
|
|
match side[bones[v * per + k]]:
|
|
-1: wl += w
|
|
1: wr += w
|
|
if wl <= 0.0 and wr <= 0.0:
|
|
vside[v] = 0
|
|
continue
|
|
var keep: int = -1 if wl >= wr else 1
|
|
vside[v] = keep
|
|
if minf(wl, wr) <= EPSILON:
|
|
continue # already single-legged
|
|
if verts[v].y > knee:
|
|
# Above the knee, only repair vertices that belong to a LEG. Cloth
|
|
# that drapes across both legs sits clear of either bone chain and
|
|
# is left blended so it can keep draping.
|
|
var dl: float = _dist_to_chain(verts[v], chain_l)
|
|
var dr: float = _dist_to_chain(verts[v], chain_r)
|
|
if minf(dl, dr) > limb_radius:
|
|
continue # hugs neither chain — cloth, leave it alone
|
|
# Side comes from WEIGHT, not from which chain is nearer. An earlier
|
|
# version also demanded one chain be 1.25x closer than the other,
|
|
# which skipped everything near the centre line — and that is exactly
|
|
# where the damage was: inner-thigh vertices kept 21% of the OPPOSITE
|
|
# leg and were torn apart when the legs split (measured 0.46). A limb
|
|
# vertex belongs to whichever leg actually drives it.
|
|
# Drop the losing leg's influence and renormalise what remains.
|
|
var total := 0.0
|
|
for k in per:
|
|
var b: int = bones[v * per + k]
|
|
if side[b] != 0 and side[b] != keep:
|
|
weights[v * per + k] = 0.0
|
|
total += weights[v * per + k]
|
|
if total > 0.0:
|
|
for k in per:
|
|
weights[v * per + k] /= total
|
|
snapped += 1
|
|
|
|
# Step 2 — stop the torso holding the top of the leg back. Only vertices that
|
|
# actually belong to a limb are touched, so the skirt keeps swinging from
|
|
# the hips as it should.
|
|
for v in verts.size():
|
|
if vside[v] == 0:
|
|
continue
|
|
var chain: PackedVector3Array = chain_l if vside[v] == -1 else chain_r
|
|
if _dist_to_chain(verts[v], chain) > limb_radius:
|
|
continue
|
|
var torso := 0.0
|
|
var dom_k := -1
|
|
var dom_w := 0.0
|
|
for k in per:
|
|
var b: int = bones[v * per + k]
|
|
var w: float = weights[v * per + k]
|
|
if torso_bone[b]:
|
|
torso += w
|
|
elif side[b] != 0 and w > dom_w:
|
|
dom_w = w
|
|
dom_k = k
|
|
if torso <= MAX_TORSO or dom_k < 0:
|
|
continue
|
|
# Scale the torso influence down to the cap and hand the rest to the
|
|
# leg bone this vertex already follows.
|
|
var keep_scale: float = MAX_TORSO / torso
|
|
for k in per:
|
|
if torso_bone[bones[v * per + k]]:
|
|
weights[v * per + k] *= keep_scale
|
|
weights[v * per + dom_k] += torso - MAX_TORSO
|
|
var sum := 0.0
|
|
for k in per:
|
|
sum += weights[v * per + k]
|
|
if sum > 0.0:
|
|
for k in per:
|
|
weights[v * per + k] /= sum
|
|
snapped += 1
|
|
|
|
# Step 3 — belt and braces: NO vertex that sits on a limb may carry any
|
|
# weight from the opposite leg, at any height. The staged rules above each
|
|
# have their own guards and between them they were still letting inner-thigh
|
|
# vertices through with ~20% of the far leg, which tears them apart when the
|
|
# legs split (measured 0.46 — the worst collapse left on the model). This is
|
|
# unconditional and runs last so nothing can reintroduce it.
|
|
for v in verts.size():
|
|
var near_l: float = _dist_to_chain(verts[v], chain_l)
|
|
var near_r: float = _dist_to_chain(verts[v], chain_r)
|
|
# Generous radius: the limb radius is measured from the bone AXIS, so a
|
|
# vertex on the front or back of a thigh clears it easily, and those were
|
|
# exactly the ones slipping through with opposite-leg weight. The skirt
|
|
# hangs far enough out to stay outside even this.
|
|
if minf(near_l, near_r) > limb_radius * 1.6:
|
|
continue # cloth
|
|
var own: int = -1 if near_l < near_r else 1
|
|
# Prefer the leg that actually drives it; fall back to the nearer chain.
|
|
var wl2 := 0.0
|
|
var wr2 := 0.0
|
|
for k in per:
|
|
match side[bones[v * per + k]]:
|
|
-1: wl2 += weights[v * per + k]
|
|
1: wr2 += weights[v * per + k]
|
|
if maxf(wl2, wr2) > 0.0:
|
|
own = -1 if wl2 >= wr2 else 1
|
|
if minf(wl2, wr2) <= 0.0:
|
|
continue # already single-legged
|
|
var tot := 0.0
|
|
for k in per:
|
|
var b2: int = bones[v * per + k]
|
|
if side[b2] != 0 and side[b2] != own:
|
|
weights[v * per + k] = 0.0
|
|
tot += weights[v * per + k]
|
|
if tot > 0.0:
|
|
for k in per:
|
|
weights[v * per + k] /= tot
|
|
vside[v] = own
|
|
snapped += 1
|
|
|
|
# Step 4 — drop triangles that still span the legs below the knee.
|
|
var removed := 0
|
|
if not idx.is_empty():
|
|
var keep_idx := PackedInt32Array()
|
|
for t in range(0, idx.size(), 3):
|
|
var a: int = idx[t]
|
|
var b: int = idx[t + 1]
|
|
var c: int = idx[t + 2]
|
|
var below: bool = (verts[a].y + verts[b].y + verts[c].y) / 3.0 < knee
|
|
var has_l: bool = vside[a] == -1 or vside[b] == -1 or vside[c] == -1
|
|
var has_r: bool = vside[a] == 1 or vside[b] == 1 or vside[c] == 1
|
|
if below and has_l and has_r:
|
|
removed += 1
|
|
continue
|
|
keep_idx.append(a)
|
|
keep_idx.append(b)
|
|
keep_idx.append(c)
|
|
if removed > 0:
|
|
arrays[Mesh.ARRAY_INDEX] = keep_idx
|
|
if snapped > 0:
|
|
arrays[Mesh.ARRAY_WEIGHTS] = weights
|
|
return [snapped, removed]
|