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:
@@ -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
|
# 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.
|
# ~35% strand share lets the cage push the cloth forward off the leg.
|
||||||
PANTS_MIX = _envf("SKIRT_PANTS_MIX", 0.65)
|
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
|
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"}
|
||||||
@@ -370,6 +403,9 @@ def main():
|
|||||||
pants_mix = PANTS_MIX
|
pants_mix = PANTS_MIX
|
||||||
elif t_cloth < PANTS_TOP + blend_band:
|
elif t_cloth < PANTS_TOP + blend_band:
|
||||||
pants_mix = PANTS_MIX * (1.0 - (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:
|
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))
|
||||||
@@ -394,23 +430,40 @@ def main():
|
|||||||
continue
|
continue
|
||||||
# ── azimuth → the two neighbouring strands, linear across the gap ──
|
# ── azimuth → the two neighbouring strands, linear across the gap ──
|
||||||
az = math.atan2(v[2] - pelvis[2], v[0] - pelvis[0])
|
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
|
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)]
|
nxt = strands[(i + 1) % len(strands)]
|
||||||
d = (nxt["az"] - s["az"]) % (2 * math.pi)
|
d = (nxt["az"] - s["az"]) % (2 * math.pi)
|
||||||
off = (az - s["az"]) % (2 * math.pi)
|
off = (az - s["az"]) % (2 * math.pi)
|
||||||
if off <= d:
|
if off <= d:
|
||||||
lo, hi, frac = s, nxt, (off / d if d > 1e-9 else 0.0)
|
lo, hi, frac = s, nxt, (off / d if d > 1e-9 else 0.0)
|
||||||
break
|
break
|
||||||
if lo is None: # numerically outside every gap
|
if share is None:
|
||||||
lo = hi = min(strands, key=lambda s: abs((az - s["az"] + math.pi)
|
if lo is None: # numerically outside every gap
|
||||||
% (2 * math.pi) - math.pi))
|
lo = hi = min(strands, key=lambda s: abs((az - s["az"] + math.pi)
|
||||||
frac = 0.0
|
% (2 * math.pi) - math.pi))
|
||||||
share = {id(lo): (lo, 1.0 - frac)}
|
frac = 0.0
|
||||||
if id(hi) in share:
|
share = {id(lo): (lo, 1.0 - frac)}
|
||||||
share[id(hi)] = (hi, share[id(hi)][1] + frac)
|
if id(hi) in share:
|
||||||
else:
|
share[id(hi)] = (hi, share[id(hi)][1] + frac)
|
||||||
share[id(hi)] = (hi, frac)
|
else:
|
||||||
|
share[id(hi)] = (hi, frac)
|
||||||
|
|
||||||
# ── height → which segment(s) of the strand ──
|
# ── height → which segment(s) of the strand ──
|
||||||
# Blend across the neighbouring pair rather than snapping, or the seam
|
# Blend across the neighbouring pair rather than snapping, or the seam
|
||||||
@@ -428,17 +481,46 @@ def main():
|
|||||||
follow = FOLLOW_MAX * contact * hem_fade
|
follow = FOLLOW_MAX * contact * hem_fade
|
||||||
stats["follow_sum"] += follow
|
stats["follow_sum"] += follow
|
||||||
|
|
||||||
strand_scale = 1.0 - pants_mix
|
|
||||||
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))
|
||||||
|
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
|
sign_l = 1.0 if origin["thigh_l"][0] >= pelvis[0] else -1.0
|
||||||
f_l = 0.5 + 0.5 * side * sign_l
|
f_l = 0.5 + 0.5 * side * sign_l
|
||||||
pelvis_w = 0.15 * (1.0 - t_cloth / max(1e-5, PANTS_TOP + 0.15))
|
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),
|
for j, x in ((slot["pelvis"], pelvis_w),
|
||||||
(slot["thigh_l"], (1.0 - pelvis_w) * f_l),
|
(slot["thigh_l"], (1.0 - pelvis_w) * f_l),
|
||||||
(slot["thigh_r"], (1.0 - pelvis_w) * (1.0 - f_l))):
|
(slot["thigh_r"], (1.0 - pelvis_w) * (1.0 - f_l))):
|
||||||
w[j] = w.get(j, 0.0) + x * pants_mix
|
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:
|
if follow > 0.0:
|
||||||
w[slot[near]] = w.get(slot[near], 0.0) + follow * strand_scale
|
w[slot[near]] = w.get(slot[near], 0.0) + follow * strand_scale
|
||||||
for strand, sh in share.values():
|
for strand, sh in share.values():
|
||||||
|
|||||||
Reference in New Issue
Block a user