From fc0afd7534e1b9b885e54bbe8c4ab2e15021f356 Mon Sep 17 00:00:00 2001 From: Ozan Bozkurt Date: Wed, 19 Aug 2026 08:03:46 +0100 Subject: [PATCH] =?UTF-8?q?feat(skirt-weights):=20AZ=5FSPREAD=20cosine=20s?= =?UTF-8?q?trand=20blend,=20PANTS=20solid-4=20zone,=20edge=20mix,=20band?= =?UTF-8?q?=20ramp,=20PANTS=5FFROM=20=E2=80=94=20all=20env-gated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BLpZsSvLufgVri2bhvmAS6 --- clothing/skirt_garment_weights.py | 104 ++++++++++++++++++++++++++---- 1 file changed, 93 insertions(+), 11 deletions(-) diff --git a/clothing/skirt_garment_weights.py b/clothing/skirt_garment_weights.py index 489b251..e3ba90f 100644 --- a/clothing/skirt_garment_weights.py +++ b/clothing/skirt_garment_weights.py @@ -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():