feat(skirt-weights): flap snap + center tie seam + ride fade + top-band gates
Wrap skirt run approval (pairs with ariki-game c03c270d1): - SKIRT_PIN_SNAP: each front vert glued to its OWN thigh (cross-leg blends park cloth between the legs at full split — the two-day pop-through bug). - SKIRT_PIN_SEAM: ramp from own-thigh to 50/50 at the centerline — ties the two front flap ends without a hard weight step (a flat band read as a cut). - SKIRT_RIDE_AZ_FADE: hard weight-zone boundaries animate as sawtooth. - SKIRT_PIN_T_LO / SKIRT_RIDE_T_LO: keep the new belly rope pure pelvis.
This commit is contained in:
@@ -127,6 +127,14 @@ PANTS_SOLID4 = os.environ.get("SKIRT_PANTS_SOLID4") == "1"
|
|||||||
# cloth stays strand-driven, where the thigh cage + groin spheres own the
|
# cloth stays strand-driven, where the thigh cage + groin spheres own the
|
||||||
# interaction (that regime never holed). 0 (default) = pants all the way up.
|
# interaction (that regime never holed). 0 (default) = pants all the way up.
|
||||||
PANTS_FROM = _envf("SKIRT_PANTS_FROM", 0.0)
|
PANTS_FROM = _envf("SKIRT_PANTS_FROM", 0.0)
|
||||||
|
# MIDLINE GATE (2026-08-22, wrap leg-springs architecture): when > 0, the pants
|
||||||
|
# split applies ONLY inside ±<deg> of the forward axis — the front SECTORS
|
||||||
|
# beyond it fall through to the classic strand+follow path, whose strands the
|
||||||
|
# runtime re-roots onto the thighs (SKIRT_LEG_SPRINGS), so that cloth is carried
|
||||||
|
# by leg springs instead of a rigid thigh split. The crotch midline keeps the
|
||||||
|
# pack's pants answer; sides/back (|az| ≥ 80°) keep pants exactly as before.
|
||||||
|
# 0 (default) = off, pants everywhere as before.
|
||||||
|
PANTS_MIDLINE_AZ = _envf("SKIRT_PANTS_MIDLINE_AZ", 0.0)
|
||||||
# FOLLOW TUBE (2026-08-20, Ozan: "the leg is a tube pushing in and out, not just the
|
# FOLLOW TUBE (2026-08-20, Ozan: "the leg is a tube pushing in and out, not just the
|
||||||
# line — govern the front quarter"): proximity follow keys off distance to the thigh
|
# line — govern the front quarter"): proximity follow keys off distance to the thigh
|
||||||
# AXIS, so cloth beside the stitch line follows the line while the leg's VOLUME
|
# AXIS, so cloth beside the stitch line follows the line while the leg's VOLUME
|
||||||
@@ -173,6 +181,56 @@ ANCHOR_SECTOR_OUTER = _envf("SKIRT_ANCHOR_SECTOR_OUTER", 135.0)
|
|||||||
# > 0 blends each vertex across every strand within this many gaps using a
|
# > 0 blends each vertex across every strand within this many gaps using a
|
||||||
# raised-cosine kernel, so folds curve instead of cornering. 0 = classic.
|
# raised-cosine kernel, so folds curve instead of cornering. 0 = classic.
|
||||||
AZ_SPREAD = _envf("SKIRT_AZ_SPREAD", 0.0)
|
AZ_SPREAD = _envf("SKIRT_AZ_SPREAD", 0.0)
|
||||||
|
# WAISTBAND RIDE BAND (2026-08-22, wrap skirt — PokeDiag-measured): the slab
|
||||||
|
# above the strand ring top is pelvis=1.00 by construction ("no skirt bone
|
||||||
|
# reaches it"), and at ~90° hip flexion the advancing thigh's front surface
|
||||||
|
# sweeps straight THROUGH that rigid cloth over each thigh front (worst vert
|
||||||
|
# measured 9.5cm inside the skin, rest az ±45°). Horizontal ease can't help a
|
||||||
|
# vert that never moves. SKIRT_RIDE_FRONT=<share> gives the pelvis-forced
|
||||||
|
# verts in the front sector (|az off forward| in [RIDE_AZ_LO, RIDE_AZ_HI]) a
|
||||||
|
# thigh split instead, so the rising thigh CARRIES that cloth over its front.
|
||||||
|
# The midline and the back stay pelvis-rigid (a swinging center/back waistband
|
||||||
|
# looks broken). 0 (default) = off, approved recipes bit-identical.
|
||||||
|
RIDE_FRONT = _envf("SKIRT_RIDE_FRONT", 0.0)
|
||||||
|
RIDE_AZ_LO = _envf("SKIRT_RIDE_AZ_LO", 30.0)
|
||||||
|
RIDE_AZ_HI = _envf("SKIRT_RIDE_AZ_HI", 65.0)
|
||||||
|
RIDE_AZ_FADE = _envf("SKIRT_RIDE_AZ_FADE", 12.0) # waistband edge fade, degrees
|
||||||
|
# Only apply the ride BELOW this drop fraction (2026-08-23, wrap rope): the
|
||||||
|
# belly rope ring sits at the very top of the garment and must stay pure
|
||||||
|
# pelvis — a thigh-riding rope ring distorts mid-stride. 0 (default) = whole
|
||||||
|
# band rides (backwards compatible).
|
||||||
|
RIDE_T_LO = _envf("SKIRT_RIDE_T_LO", 0.0)
|
||||||
|
# HARD FRONT PIN (2026-08-22, wrap — PokeDiag: hem-tip verts at 84% thigh still
|
||||||
|
# sink 3.7cm into the advancing thigh at 90° flexion; the leftover pelvis+strand
|
||||||
|
# share is the drag, and the ANCHOR only redistributes the strand share, never
|
||||||
|
# the pants/pelvis share). SKIRT_PIN_FRONT=<share> post-passes EVERY vert in the
|
||||||
|
# front zone (az PIN_AZ_LO–HI off forward, drop fraction PIN_T_LO–HI) so its
|
||||||
|
# thigh total is AT LEAST <share> — 1.0 = glued to the thigh surface, "the bone
|
||||||
|
# that is already there" (Ozan: "can you just put the bone where the error
|
||||||
|
# persists"). 0 (default) = off.
|
||||||
|
PIN_FRONT = _envf("SKIRT_PIN_FRONT", 0.0)
|
||||||
|
PIN_AZ_LO = _envf("SKIRT_PIN_AZ_LO", 20.0)
|
||||||
|
PIN_AZ_HI = _envf("SKIRT_PIN_AZ_HI", 70.0)
|
||||||
|
PIN_T_LO = _envf("SKIRT_PIN_T_LO", -0.4)
|
||||||
|
PIN_T_HI = _envf("SKIRT_PIN_T_HI", 1.0)
|
||||||
|
PIN_AZ_FADE = _envf("SKIRT_PIN_AZ_FADE", 12.0) # edge fade width, degrees
|
||||||
|
# FLAP SNAP (2026-08-22, run cycle): inside the pin zone the L/R split factor
|
||||||
|
# f_l is a smooth ramp over hip width, so front-center verts land ~0.5/0.5
|
||||||
|
# (or 0.7/0.3) across BOTH thighs — at full leg split those verts park between
|
||||||
|
# the legs and the advancing thigh swallows them (PokeDiag run: pen -2.5cm at
|
||||||
|
# az~180 hem point, w 0.68-0.88 mixed). SKIRT_PIN_SNAP=1 snaps f_l to the
|
||||||
|
# vert's OWN side (±0.05 smooth transition at the centerline): the front hangs
|
||||||
|
# as two flaps, each glued to its own thigh, like a real wrap's overlap.
|
||||||
|
PIN_SNAP = _envf("SKIRT_PIN_SNAP", 0.0)
|
||||||
|
# CENTER STRETCH SEAM (2026-08-22 v31, Ozan: "two skirt ends connect at the
|
||||||
|
# center but don't cross the leg — stretch logic can be there"): with the pure
|
||||||
|
# snap the front opens into a V down to the crotch at full leg split. Verts
|
||||||
|
# within ±PIN_SEAM degrees of the front centerline override the snap with a
|
||||||
|
# RAMP from the flap's own thigh (zone edge) to an exact 50/50 split
|
||||||
|
# (centerline) — a narrow membrane that TIES the flaps together, stretches
|
||||||
|
# between the legs, and parks at the midpoint BETWEEN them (never inside a
|
||||||
|
# leg). 2026-08-23: flat 50/50 band → ramp (its 1.0→0.5 step read as a cut).
|
||||||
|
PIN_SEAM = _envf("SKIRT_PIN_SEAM", 0.0) # degrees each side of front center
|
||||||
MAX_INFLUENCES = 4
|
MAX_INFLUENCES = 4
|
||||||
|
|
||||||
COMP_FMT = {5120: "b", 5121: "B", 5122: "h", 5123: "H", 5125: "I", 5126: "f"}
|
COMP_FMT = {5120: "b", 5121: "B", 5122: "h", 5123: "H", 5125: "I", 5126: "f"}
|
||||||
@@ -446,13 +504,85 @@ def main():
|
|||||||
f"cloth hangs {span:.3f} below the ring top")
|
f"cloth hangs {span:.3f} below the ring top")
|
||||||
|
|
||||||
out_j, out_w = [], []
|
out_j, out_w = [], []
|
||||||
stats = {"waistband": 0, "follow_sum": 0.0, "anchored": 0}
|
stats = {"waistband": 0, "follow_sum": 0.0, "anchored": 0, "ride": 0, "pinned": 0}
|
||||||
per_strand = {k: 0.0 for k in body_ring}
|
per_strand = {k: 0.0 for k in body_ring}
|
||||||
thigh_total = 0.0
|
thigh_total = 0.0
|
||||||
|
|
||||||
for v in pos:
|
for v in pos:
|
||||||
w = {}
|
w = {}
|
||||||
if v[1] > y_top - BAND:
|
# ── hard front pin (see the SKIRT_PIN_FRONT header): MUST run before the
|
||||||
|
# pants/SOLID4 branches — those pack and `continue`, so a post-pass after
|
||||||
|
# them never sees the hem verts (first attempt changed nothing, measured).
|
||||||
|
pinned = False
|
||||||
|
if PIN_FRONT > 0.0 and tube_fwd is not None:
|
||||||
|
fwd_ang = math.atan2(tube_fwd[1], tube_fwd[0])
|
||||||
|
rel = abs((math.atan2(v[2] - pelvis[2], v[0] - pelvis[0])
|
||||||
|
- fwd_ang + math.pi) % (2 * math.pi) - math.pi)
|
||||||
|
t_rel = (y_top - v[1]) / span if span > 1e-9 else 0.0
|
||||||
|
# Edge-faded zone (Ozan 2026-08-22: "lower parts create triangle on
|
||||||
|
# the left while walking"): a HARD pin boundary tears a wedge where
|
||||||
|
# the pinned sector hugs the leg and the free midline hangs. Fade
|
||||||
|
# the share to 0 across PIN_AZ_FADE degrees on both sides instead.
|
||||||
|
fade = math.radians(PIN_AZ_FADE)
|
||||||
|
strength = (smoothstep((rel - (math.radians(PIN_AZ_LO) - fade)) / fade)
|
||||||
|
* (1.0 - smoothstep((rel - math.radians(PIN_AZ_HI)) / fade)))
|
||||||
|
if strength > 0.0 and PIN_T_LO <= t_rel <= PIN_T_HI:
|
||||||
|
share = PIN_FRONT * strength
|
||||||
|
hip_half = max(0.02, abs(origin["thigh_l"][0] - pelvis[0]))
|
||||||
|
side = max(-1.0, min(1.0, (v[0] - pelvis[0]) / hip_half))
|
||||||
|
sign_l = 1.0 if origin["thigh_l"][0] >= pelvis[0] else -1.0
|
||||||
|
f_l = 0.5 + 0.5 * side * sign_l
|
||||||
|
if PIN_SNAP > 0.0:
|
||||||
|
f_l = smoothstep((side * sign_l + 0.05) / 0.10)
|
||||||
|
if PIN_SEAM > 0.0 and rel < math.radians(PIN_SEAM):
|
||||||
|
# TIE the flap ends (2026-08-23, Ozan: "a sharp cut, two
|
||||||
|
# separate lines at full split — tie those end surfaces"):
|
||||||
|
# a FLAT 50/50 seam band steps 1.0→0.5 at its boundary and
|
||||||
|
# the step reads as a cut. Ramp f_l from the flap's own
|
||||||
|
# thigh at the zone edge to 0.5 at the centerline instead —
|
||||||
|
# the stretch distributes across the whole seam, no step.
|
||||||
|
u = rel / math.radians(PIN_SEAM)
|
||||||
|
f_own = 1.0 if (side * sign_l) >= 0.0 else 0.0
|
||||||
|
f_l = 0.5 + (f_own - 0.5) * u
|
||||||
|
w[slot["pelvis"]] = 1.0 - share
|
||||||
|
w[slot["thigh_l"]] = share * f_l
|
||||||
|
w[slot["thigh_r"]] = share * (1.0 - f_l)
|
||||||
|
stats["pinned"] += 1
|
||||||
|
pinned = True
|
||||||
|
if pinned:
|
||||||
|
pass
|
||||||
|
elif v[1] > y_top - BAND:
|
||||||
|
# Waistband ride band (see the SKIRT_RIDE_FRONT header): front-sector
|
||||||
|
# waistband verts get a thigh split so the advancing thigh CARRIES the
|
||||||
|
# junction cloth over its front instead of sweeping through it.
|
||||||
|
ridden = False
|
||||||
|
t_rel_r = (y_top - v[1]) / span if span > 1e-9 else 0.0
|
||||||
|
if RIDE_FRONT > 0.0 and tube_fwd is not None and t_rel_r >= RIDE_T_LO:
|
||||||
|
fwd_ang = math.atan2(tube_fwd[1], tube_fwd[0])
|
||||||
|
rel = abs((math.atan2(v[2] - pelvis[2], v[0] - pelvis[0])
|
||||||
|
- fwd_ang + math.pi) % (2 * math.pi) - math.pi)
|
||||||
|
fade_r = math.radians(RIDE_AZ_FADE)
|
||||||
|
if (math.radians(RIDE_AZ_LO) - fade_r <= rel
|
||||||
|
<= math.radians(RIDE_AZ_HI) + fade_r):
|
||||||
|
# Edge fade (2026-08-22 v31, Ozan: "too many triangles on
|
||||||
|
# top skirt, looks distorted"): the hard 22°/65° boundary
|
||||||
|
# put pelvis=1.0 verts next to pelvis=0.15 verts along the
|
||||||
|
# waistband — a weight step that animates as a sawtooth top
|
||||||
|
# edge. Fade the share over RIDE_AZ_FADE degrees both sides.
|
||||||
|
r_str = (smoothstep((rel - (math.radians(RIDE_AZ_LO) - fade_r)) / fade_r)
|
||||||
|
* (1.0 - smoothstep((rel - math.radians(RIDE_AZ_HI)) / fade_r)))
|
||||||
|
if r_str > 0.0:
|
||||||
|
ride = RIDE_FRONT * r_str
|
||||||
|
hip_half = max(0.02, abs(origin["thigh_l"][0] - pelvis[0]))
|
||||||
|
side = max(-1.0, min(1.0, (v[0] - pelvis[0]) / hip_half))
|
||||||
|
sign_l = 1.0 if origin["thigh_l"][0] >= pelvis[0] else -1.0
|
||||||
|
f_l = 0.5 + 0.5 * side * sign_l
|
||||||
|
w[slot["pelvis"]] = 1.0 - ride
|
||||||
|
w[slot["thigh_l"]] = ride * f_l
|
||||||
|
w[slot["thigh_r"]] = ride * (1.0 - f_l)
|
||||||
|
stats["ride"] += 1
|
||||||
|
ridden = True
|
||||||
|
if not ridden:
|
||||||
w[slot["pelvis"]] = 1.0
|
w[slot["pelvis"]] = 1.0
|
||||||
stats["waistband"] += 1
|
stats["waistband"] += 1
|
||||||
else:
|
else:
|
||||||
@@ -468,6 +598,13 @@ def main():
|
|||||||
if PANTS_FROM > 0.0 and t_cloth < PANTS_FROM:
|
if PANTS_FROM > 0.0 and t_cloth < PANTS_FROM:
|
||||||
# fade pants IN across the same-width band below the line
|
# fade pants IN across the same-width band below the line
|
||||||
pants_mix *= max(0.0, 1.0 - (PANTS_FROM - t_cloth) / blend_band)
|
pants_mix *= max(0.0, 1.0 - (PANTS_FROM - t_cloth) / blend_band)
|
||||||
|
if (PANTS_MIDLINE_AZ > 0.0 and tube_fwd is not None):
|
||||||
|
fwd_ang = math.atan2(tube_fwd[1], tube_fwd[0])
|
||||||
|
rel = abs((math.atan2(v[2] - pelvis[2], v[0] - pelvis[0])
|
||||||
|
- fwd_ang + math.pi) % (2 * math.pi) - math.pi)
|
||||||
|
deg = math.degrees(rel)
|
||||||
|
if PANTS_MIDLINE_AZ < deg < 80.0:
|
||||||
|
pants_mix = 0.0 # front sectors → strand/leg springs
|
||||||
if pants_mix > 0.0:
|
if pants_mix > 0.0:
|
||||||
hip_half = max(0.02, abs(origin["thigh_l"][0] - pelvis[0]))
|
hip_half = max(0.02, abs(origin["thigh_l"][0] - pelvis[0]))
|
||||||
side = max(-1.0, min(1.0, (v[0] - pelvis[0]) / hip_half))
|
side = max(-1.0, min(1.0, (v[0] - pelvis[0]) / hip_half))
|
||||||
@@ -668,6 +805,9 @@ def main():
|
|||||||
# ── gates FIRST: never leave a broken garment on disk ──
|
# ── gates FIRST: never leave a broken garment on disk ──
|
||||||
n = len(pos)
|
n = len(pos)
|
||||||
log(f"waistband (pelvis only): {stats['waistband']} verts")
|
log(f"waistband (pelvis only): {stats['waistband']} verts")
|
||||||
|
if RIDE_FRONT > 0.0:
|
||||||
|
log(f"waistband ride band: {stats['ride']} verts thigh-split "
|
||||||
|
f"(share {RIDE_FRONT:.2f}, az {RIDE_AZ_LO:.0f}–{RIDE_AZ_HI:.0f}°)")
|
||||||
log(f"mean thigh follow on panel verts: "
|
log(f"mean thigh follow on panel verts: "
|
||||||
f"{stats['follow_sum'] / max(1, n - stats['waistband']):.3f}")
|
f"{stats['follow_sum'] / max(1, n - stats['waistband']):.3f}")
|
||||||
if ANCHOR:
|
if ANCHOR:
|
||||||
|
|||||||
Reference in New Issue
Block a user