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
|
||||
# ~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,14 +430,31 @@ 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 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))
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user