fix: stop the torso holding back the top of the thigh
Hiding the skirt and jacket and rendering the legs alone, side-on, finally showed where the deformation actually is: not the knee, the HIP. The top of the thigh flattens into a wedge at full stride. That matches the worst number the corrected measurement reports — cross-section 0.85 at the hip, against 0.97-0.99 at the knee — which had been dismissed as ordinary. Cause is the usual linear-blend failure at a big rotation: upper-thigh vertices are weighted between the near-static hips and a thigh swung far out, and the average of those two transforms collapses the top of the leg. SkinLegRepair now caps torso (hips/spine) influence on LEG vertices at 15% and hands the excess to the leg bone that already dominates them. Only vertices within the limb radius of a leg bone chain are touched, so the skirt keeps swinging from the hips. Hip cross-section 0.85 -> 0.88, and the flattened wedge is visibly fuller in the side-on render. This is a real improvement but a modest one; the legs are slim low-poly geometry and some collapse at a 60 degree hip rotation is inherent to linear-blend skinning. Taila now snaps 1217+ vertices, Miku 434. FSM tests 11/11, spawn smoke test 0 failures. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1e12c36cca
commit
e707229283
@@ -32,13 +32,27 @@ class_name SkinLegRepair
|
|||||||
## leg's own bone chain, NOT by height or by surface name: a thigh vertex
|
## 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
|
## 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.
|
## left blended, which is what lets a skirt drape across both legs.
|
||||||
## 3. Drop any triangle still spanning the two legs below the knee. Those are
|
## 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
|
## the midline band between the ankles, which has no correct pose either
|
||||||
## way.
|
## way.
|
||||||
##
|
##
|
||||||
## Measure with debug/limb_deform_check.gd.
|
## Measure with debug/limb_deform_check.gd.
|
||||||
|
|
||||||
const LEG_BONE_HINTS := ["thigh", "shin", "foot", "toe"]
|
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.
|
## Ignore influences below this — they are rounding, not real weighting.
|
||||||
const EPSILON := 0.005
|
const EPSILON := 0.005
|
||||||
|
|
||||||
@@ -111,12 +125,13 @@ static func _repair_mesh(mi: MeshInstance3D, skeleton: Skeleton3D, knee: float,
|
|||||||
chain_l: PackedVector3Array, chain_r: PackedVector3Array,
|
chain_l: PackedVector3Array, chain_r: PackedVector3Array,
|
||||||
limb_radius: float) -> Array:
|
limb_radius: float) -> Array:
|
||||||
var side := _side_map(mi.skin, skeleton)
|
var side := _side_map(mi.skin, skeleton)
|
||||||
|
var torso_bone := _torso_map(mi.skin, skeleton)
|
||||||
var surfaces: Array = []
|
var surfaces: Array = []
|
||||||
var snapped := 0
|
var snapped := 0
|
||||||
var removed := 0
|
var removed := 0
|
||||||
for s in range(mi.mesh.get_surface_count()):
|
for s in range(mi.mesh.get_surface_count()):
|
||||||
var arrays: Array = mi.mesh.surface_get_arrays(s)
|
var arrays: Array = mi.mesh.surface_get_arrays(s)
|
||||||
var r := _repair_surface(arrays, side, knee, chain_l, chain_r, limb_radius)
|
var r := _repair_surface(arrays, side, torso_bone, knee, chain_l, chain_r, limb_radius)
|
||||||
snapped += r[0]
|
snapped += r[0]
|
||||||
removed += r[1]
|
removed += r[1]
|
||||||
surfaces.append({
|
surfaces.append({
|
||||||
@@ -138,6 +153,23 @@ static func _repair_mesh(mi: MeshInstance3D, skeleton: Skeleton3D, knee: float,
|
|||||||
return [snapped, removed]
|
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
|
## -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).
|
## is what ARRAY_BONES stores (not the skeleton's bone index).
|
||||||
static func _side_map(skin: Skin, skeleton: Skeleton3D) -> PackedInt32Array:
|
static func _side_map(skin: Skin, skeleton: Skeleton3D) -> PackedInt32Array:
|
||||||
@@ -164,7 +196,8 @@ static func _side_map(skin: Skin, skeleton: Skeleton3D) -> PackedInt32Array:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
static func _repair_surface(arrays: Array, side: PackedInt32Array, knee: float,
|
static func _repair_surface(arrays: Array, side: PackedInt32Array,
|
||||||
|
torso_bone: Array, knee: float,
|
||||||
chain_l: PackedVector3Array, chain_r: PackedVector3Array,
|
chain_l: PackedVector3Array, chain_r: PackedVector3Array,
|
||||||
limb_radius: float) -> Array:
|
limb_radius: float) -> Array:
|
||||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||||
@@ -220,7 +253,44 @@ static func _repair_surface(arrays: Array, side: PackedInt32Array, knee: float,
|
|||||||
weights[v * per + k] /= total
|
weights[v * per + k] /= total
|
||||||
snapped += 1
|
snapped += 1
|
||||||
|
|
||||||
# Step 2 — drop triangles that still span the legs below the knee.
|
# 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 — drop triangles that still span the legs below the knee.
|
||||||
var removed := 0
|
var removed := 0
|
||||||
if not idx.is_empty():
|
if not idx.is_empty():
|
||||||
var keep_idx := PackedInt32Array()
|
var keep_idx := PackedInt32Array()
|
||||||
|
|||||||
Reference in New Issue
Block a user