feat(skirt-weights): AZ_SPREAD cosine strand blend, PANTS solid-4 zone, edge mix, band ramp, PANTS_FROM — all env-gated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BLpZsSvLufgVri2bhvmAS6
This commit is contained in:
2026-08-19 08:03:46 +01:00
parent be1ee94701
commit fc0afd7534
+93 -11
View File
@@ -100,6 +100,39 @@ PANTS_TOP = _envf("SKIRT_PANTS_TOP", 0.0)
# advancing thigh's inner edge (the 50/50 average can't move with one leg). Keeping
# ~35% strand share lets the cage push the cloth forward off the leg.
PANTS_MIX = _envf("SKIRT_PANTS_MIX", 0.65)
# LATERAL EDGE MIX (2026-08-19, Superhero male pugu): a hem corner at PANTS_MIX
# 0.65 follows only 65% of its thigh's swing — the remaining strand share is
# pelvis-anchored, so the thigh surface overtakes the cloth by 35% of its travel.
# That deficit scales with the thigh's front protrusion, which is why only the
# widest body showed it ("kiyafet alt koseleri bacak hala yiyor"). Verts near the
# panel's lateral edges belong unambiguously to ONE thigh, so they can follow it
# almost fully; the midline keeps PANTS_MIX (its 50/50 thigh average must stay
# collision-driven or the advancing thigh's inner edge slices it — measured).
# < 0 (default) = off, edges behave exactly like the midline.
PANTS_MIX_EDGE = _envf("SKIRT_PANTS_MIX_EDGE", -1.0)
# SOLID-4 pants zone (2026-08-19, female pugu): in the mixed pants zone a vertex
# can accumulate 7-8 influences (pelvis + both thighs + strand-pair x segment-pair
# + proximity follow) and the top-4 truncation keeps DIFFERENT survivor sets on
# neighbouring verts — mid-swing they diverge and single-vertex holes open in the
# panel. With SKIRT_PANTS_SOLID4=1 the pants zone builds exactly four influences
# (pelvis, thigh_l, thigh_r, nearest strand's height-matched segment): nothing is
# ever truncated, neighbours stay consistent. The strand share is small there
# (<= 1-PANTS_MIX), so collapsing its azimuth/segment blends is invisible.
PANTS_SOLID4 = os.environ.get("SKIRT_PANTS_SOLID4") == "1"
# Lower bound of the pants zone (fraction of garment height from the top).
# Bone-rigid cloth CANNOT track the hip-crease skin bulge at peak thigh flexion —
# on the female pugu the crease punched holes through the pants-weighted upper
# panel that no static clearance could absorb (2026-08-19). Above this line 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.
PANTS_FROM = _envf("SKIRT_PANTS_FROM", 0.0)
# Azimuth spread, in strand-gap units (2026-08-19, long tifi): the classic
# 2-strand linear blend makes the hem fold PIECEWISE-LINEAR — mid-stride the
# tube creases into a hard triangle where the front-leg cloth meets the
# back-leg cloth ("strict triangle ... should be smoothed ... more like cloth").
# > 0 blends each vertex across every strand within this many gaps using a
# raised-cosine kernel, so folds curve instead of cornering. 0 = classic.
AZ_SPREAD = _envf("SKIRT_AZ_SPREAD", 0.0)
MAX_INFLUENCES = 4
COMP_FMT = {5120: "b", 5121: "B", 5122: "h", 5123: "H", 5125: "I", 5126: "f"}
@@ -370,6 +403,9 @@ def main():
pants_mix = PANTS_MIX
elif t_cloth < PANTS_TOP + blend_band:
pants_mix = PANTS_MIX * (1.0 - (t_cloth - PANTS_TOP) / blend_band)
if PANTS_FROM > 0.0 and t_cloth < PANTS_FROM:
# fade pants IN across the same-width band below the line
pants_mix *= max(0.0, 1.0 - (PANTS_FROM - t_cloth) / blend_band)
if pants_mix > 0.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))
@@ -394,23 +430,40 @@ def main():
continue
# ── azimuth → the two neighbouring strands, linear across the gap ──
az = math.atan2(v[2] - pelvis[2], v[0] - pelvis[0])
if AZ_SPREAD > 0.0:
gap = 2 * math.pi / max(1, len(strands))
width = AZ_SPREAD * gap
share = {}
for st in strands:
d = abs((az - st["az"] + math.pi) % (2 * math.pi) - math.pi)
if d < width:
share[id(st)] = (st, 0.5 * (1.0 + math.cos(math.pi * d / width)))
if not share:
nearest = min(strands, key=lambda st: abs((az - st["az"] + math.pi)
% (2 * math.pi) - math.pi))
share[id(nearest)] = (nearest, 1.0)
total = sum(x for _, x in share.values())
share = {k: (st, x / total) for k, (st, x) in share.items()}
else:
share = None
lo = None
for i, s in enumerate(strands):
for i, s in enumerate(strands) if share is None else []:
nxt = strands[(i + 1) % len(strands)]
d = (nxt["az"] - s["az"]) % (2 * math.pi)
off = (az - s["az"]) % (2 * math.pi)
if off <= d:
lo, hi, frac = s, nxt, (off / d if d > 1e-9 else 0.0)
break
if lo is None: # numerically outside every gap
lo = hi = min(strands, key=lambda s: abs((az - s["az"] + math.pi)
% (2 * math.pi) - math.pi))
frac = 0.0
share = {id(lo): (lo, 1.0 - frac)}
if id(hi) in share:
share[id(hi)] = (hi, share[id(hi)][1] + frac)
else:
share[id(hi)] = (hi, frac)
if share is None:
if lo is None: # numerically outside every gap
lo = hi = min(strands, key=lambda s: abs((az - s["az"] + math.pi)
% (2 * math.pi) - math.pi))
frac = 0.0
share = {id(lo): (lo, 1.0 - frac)}
if id(hi) in share:
share[id(hi)] = (hi, share[id(hi)][1] + frac)
else:
share[id(hi)] = (hi, frac)
# ── height → which segment(s) of the strand ──
# Blend across the neighbouring pair rather than snapping, or the seam
@@ -428,17 +481,46 @@ def main():
follow = FOLLOW_MAX * contact * hem_fade
stats["follow_sum"] += follow
strand_scale = 1.0 - pants_mix
if pants_mix > 0.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))
if PANTS_MIX_EDGE >= 0.0:
pants_mix = pants_mix + (PANTS_MIX_EDGE - pants_mix) * abs(side)
sign_l = 1.0 if origin["thigh_l"][0] >= pelvis[0] else -1.0
f_l = 0.5 + 0.5 * side * sign_l
pelvis_w = 0.15 * (1.0 - t_cloth / max(1e-5, PANTS_TOP + 0.15))
if os.environ.get("SKIRT_BAND_RAMP") == "1":
# Ramp out of the pelvis-1.0 waistband over 5cm — the hard
# step from band to pants opened a single-vert speck at the
# boundary mid-swing (2026-08-19). Env-gated: OFF reproduces
# the approved male pugu weights bit-for-bit.
band_ramp = max(0.0, 1.0 - (y_top - BAND - v[1]) / 0.05)
pelvis_w = max(pelvis_w, band_ramp)
for j, x in ((slot["pelvis"], pelvis_w),
(slot["thigh_l"], (1.0 - pelvis_w) * f_l),
(slot["thigh_r"], (1.0 - pelvis_w) * (1.0 - f_l))):
w[j] = w.get(j, 0.0) + x * pants_mix
strand_scale = 1.0 - pants_mix
if PANTS_SOLID4 and pants_mix > 0.0:
# exactly ONE strand bone: nearest strand by azimuth, its
# height-matched segment — total influence count stays at 4.
best = max(share.values(), key=lambda p: p[1])[0]
segs, heads = best["segs"], best["heads"]
si = len(segs) - 1
for i in range(len(heads) - 1):
if v[1] > heads[i + 1]:
si = i
break
w[slot[segs[si]]] = w.get(slot[segs[si]], 0.0) + strand_scale
per_strand[best["k"]] += strand_scale
top4 = sorted(w.items(), key=lambda kv: -kv[1])[:MAX_INFLUENCES]
total = sum(x for _, x in top4) or 1.0
top4 = [(j, x / total) for j, x in top4]
while len(top4) < MAX_INFLUENCES:
top4.append((0, 0.0))
out_j.append(tuple(j for j, _ in top4))
out_w.append(tuple(x for _, x in top4))
continue
if follow > 0.0:
w[slot[near]] = w.get(slot[near], 0.0) + follow * strand_scale
for strand, sh in share.values():