fix: hang the skirt from the hips so it stops collapsing at a stride
It was the SKIRT, not the legs. Every leg check I ran hid the skirt to see the thighs, so I was never looking at what is actually on screen. Rendering the full model side-on shows it immediately: at rest the skirt is a flared shape with real volume, and the moment the legs split for a run or a jump it collapses flat against the body. The lower-body silhouette loses its shape, which reads as the legs squashing. Cause: the skirt is weighted to BOTH thighs. At a split stride the two thighs pull it in opposite directions and linear-blend skinning averages them, so the skirt is dragged inward and flattened. The earlier repair deliberately left it that way, on the reasoning that a skirt should be free to drape across both legs — correct in principle, wrong at these stride angles. Draping cloth now hangs from the hips: vertices that clear both leg bone chains keep at most 20% leg influence and the rest goes to DEF-hips, so the skirt holds its shape and swings as a unit. Limb vertices are still snapped to one leg, so the two are now modelled differently on purpose. Bounded ABOVE THE KNEE. The first attempt used distance from the bone chain alone, and chunky boots sit further from the ankle than the limb radius, so they read as "drape", lost their leg weight and trailed off the hips — the legs came out far worse than before. A skirt hem is above the knee; boots and stockings are not. Legs unchanged by this: knee cross-section stays 0.99, and the knee helper work from the previous commit is intact. Taila snaps 4024 vertices, Miku 670. 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
050011666c
commit
20b70e5eda
@@ -41,7 +41,17 @@ class_name SkinLegRepair
|
||||
## 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
|
||||
## 4. Hang DRAPING CLOTH from the hips instead of the legs. The skirt is
|
||||
## weighted to both thighs so it can follow them, but at a running or
|
||||
## jumping stride the two thighs pull it in opposite directions and it
|
||||
## collapses flat against the body — its flared volume vanishes and the
|
||||
## whole lower body reads as squashed from the side. Cloth that is clear of
|
||||
## both leg bone chains keeps at most MAX_DRAPE_LEG of leg influence, and
|
||||
## the rest goes to the hips, so the skirt holds its shape and swings as a
|
||||
## unit. This is the "model the skirt differently from the legs" case, and
|
||||
## it is separate from the limb work above: LIMB vertices are snapped to
|
||||
## one leg, DRAPE vertices are taken off the legs almost entirely.
|
||||
## 5. 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.
|
||||
##
|
||||
@@ -53,6 +63,9 @@ 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
|
||||
## The most LEG influence a draping cloth vertex may keep. A little lets the
|
||||
## skirt react to the legs; more than this and a split stride tears it flat.
|
||||
const MAX_DRAPE_LEG := 0.20
|
||||
## Ignore influences below this — they are rounding, not real weighting.
|
||||
const EPSILON := 0.005
|
||||
|
||||
@@ -126,12 +139,13 @@ static func _repair_mesh(mi: MeshInstance3D, skeleton: Skeleton3D, knee: float,
|
||||
limb_radius: float) -> Array:
|
||||
var side := _side_map(mi.skin, skeleton)
|
||||
var torso_bone := _torso_map(mi.skin, skeleton)
|
||||
var hips_slot := _hips_bind(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)
|
||||
var r := _repair_surface(arrays, side, torso_bone, hips_slot, knee, chain_l, chain_r, limb_radius)
|
||||
snapped += r[0]
|
||||
removed += r[1]
|
||||
surfaces.append({
|
||||
@@ -153,6 +167,18 @@ static func _repair_mesh(mi: MeshInstance3D, skeleton: Skeleton3D, knee: float,
|
||||
return [snapped, removed]
|
||||
|
||||
|
||||
## SKIN BIND index of the hips, which is what draping cloth should hang from.
|
||||
static func _hips_bind(skin: Skin, skeleton: Skeleton3D) -> int:
|
||||
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 ""
|
||||
if n.findn("hips") != -1 or n.findn("pelvis") != -1:
|
||||
return b
|
||||
return -1
|
||||
|
||||
|
||||
## 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 = []
|
||||
@@ -197,7 +223,7 @@ static func _side_map(skin: Skin, skeleton: Skeleton3D) -> PackedInt32Array:
|
||||
|
||||
|
||||
static func _repair_surface(arrays: Array, side: PackedInt32Array,
|
||||
torso_bone: Array, knee: float,
|
||||
torso_bone: Array, hips_slot: int, knee: float,
|
||||
chain_l: PackedVector3Array, chain_r: PackedVector3Array,
|
||||
limb_radius: float) -> Array:
|
||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
@@ -290,7 +316,63 @@ 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 — draping cloth hangs from the hips. A skirt pulled by both thighs
|
||||
# at a split stride collapses flat; taking it off the legs keeps its volume.
|
||||
if hips_slot >= 0:
|
||||
for v in verts.size():
|
||||
var leg_total := 0.0
|
||||
for k in per:
|
||||
if side[bones[v * per + k]] != 0:
|
||||
leg_total += weights[v * per + k]
|
||||
if leg_total <= MAX_DRAPE_LEG:
|
||||
continue
|
||||
# ABOVE THE KNEE ONLY. Distance from the bone chain alone is not a
|
||||
# safe test down here: chunky footwear sits further from the ankle
|
||||
# than the limb radius, so a boot reads as "drape" and taking its
|
||||
# leg weight away makes it trail off the hips. A skirt hem is above
|
||||
# the knee; boots and stockings are not.
|
||||
if verts[v].y <= knee:
|
||||
continue
|
||||
# Limb vertices were already handled above; this is only for cloth
|
||||
# that hangs clear of BOTH leg bone chains.
|
||||
if minf(_dist_to_chain(verts[v], chain_l),
|
||||
_dist_to_chain(verts[v], chain_r)) <= limb_radius:
|
||||
continue
|
||||
var k_hips := -1
|
||||
for k in per:
|
||||
if bones[v * per + k] == hips_slot:
|
||||
k_hips = k
|
||||
break
|
||||
if k_hips < 0:
|
||||
# No hips influence yet: take over the weakest leg slot.
|
||||
var weakest := -1
|
||||
var weakest_w := 2.0
|
||||
for k in per:
|
||||
if side[bones[v * per + k]] != 0 and weights[v * per + k] < weakest_w:
|
||||
weakest_w = weights[v * per + k]
|
||||
weakest = k
|
||||
if weakest < 0:
|
||||
continue
|
||||
bones[v * per + weakest] = hips_slot
|
||||
weights[v * per + weakest] = 0.0
|
||||
k_hips = weakest
|
||||
leg_total -= weakest_w
|
||||
if leg_total <= MAX_DRAPE_LEG:
|
||||
continue
|
||||
var keep: float = MAX_DRAPE_LEG / leg_total
|
||||
for k in per:
|
||||
if side[bones[v * per + k]] != 0:
|
||||
weights[v * per + k] *= keep
|
||||
weights[v * per + k_hips] += leg_total - MAX_DRAPE_LEG
|
||||
var total2 := 0.0
|
||||
for k in per:
|
||||
total2 += weights[v * per + k]
|
||||
if total2 > 0.0:
|
||||
for k in per:
|
||||
weights[v * per + k] /= total2
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user