fix: no limb vertex may carry opposite-leg weight — thigh collapse gone

The thighs going thin was never the knee bend. It was inner-thigh vertices
keeping ~20% of the weight of the OPPOSITE leg, so a split stride tore them
across the body. Printing the weight composition of the worst-collapsing
vertices is what finally showed it: a right-thigh vertex reading
DEF-thigh.R=0.58 DEF-shin.R=0.21 DEF-thigh.L=0.13 DEF-shin.L=0.08.

Every earlier pass had a guard that let these through. The below-knee pass does
not reach them; the above-knee pass demanded one leg chain be 1.25x nearer than
the other, which excludes everything near the centre line — precisely where the
damage was; and the limb-radius gates are measured from the bone AXIS, so a
vertex on the front or back of a thigh clears them easily.

SkinLegRepair now ends with an unconditional pass: any vertex within a generous
radius of either leg chain is forced onto the leg that actually drives it, at
any height. It runs last so none of the staged rules can reintroduce the
problem.

Measured per vertex, across jump, fall, run and dash:

                     before        after
  Body below 0.80      70            0
  Body worst          0.46         0.82
  ClothCAndW below 0.80  107          0
  ClothCAndW worst    0.66         0.85
  ClothB worst        0.81         0.87

No vertex anywhere on the legs now loses more than 18% of its thickness, down
from 54%. The skirt still deforms freely (0.87-0.88) — clothes flow, limbs hold.

Also fixed in SkinJointHelper along the way: it picked the FIRST joint chain
that matched a vertex rather than the one holding most of its weight, which
bound right-thigh vertices with stray left-leg weight to the LEFT knee's helpers
and dragged them across the body. It now picks the dominant joint and requires
the joint to own at least half the vertex.

Taila snaps 2021 vertices, Miku 659. FSM tests 11/11, spawn smoke test 0
failures.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-22 09:03:45 -04:00
co-authored by Claude Opus 4.8
parent d33f9cd812
commit afc954e129
2 changed files with 161 additions and 107 deletions
+48 -7
View File
@@ -255,12 +255,14 @@ static func _repair_surface(arrays: Array, side: PackedInt32Array,
# 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)
var near: float = minf(dl, dr)
var far: float = maxf(dl, dr)
if near > limb_radius or far < near * 1.25:
continue # drapes over both, or hugs neither — leave it alone
keep = -1 if dl < dr else 1
vside[v] = keep
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:
@@ -310,7 +312,46 @@ static func _repair_surface(arrays: Array, side: PackedInt32Array,
weights[v * per + k] /= sum
snapped += 1
# Step 3 — drop triangles that still span the legs below the knee.
# 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()