From be1ee9470155a215b9f844b678afac252de4baa1 Mon Sep 17 00:00:00 2001 From: Ozan Bozkurt Date: Wed, 19 Aug 2026 03:49:25 +0100 Subject: [PATCH] feat(skirt-weights): partial-coverage + pants-top modes for the ariki-game pugu lane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two env-gated extensions, defaults unchanged (full skirts behave exactly as before): - SKIRT_ALLOW_DEAD=1 — a pugu loincloth has OPEN sides by design; its bare side strands downgrade the dead-strand FATAL to a warning. - SKIRT_PANTS_TOP= + SKIRT_PANTS_MIX (0.65) — the garment's top fraction is skinned like the pack's own Peasant pants (measured: crotch cloth = 49/49 both thighs, pelvis ≈ 0 below the waistband), mixed with the strand weights so leg colliders keep authority; blends into pure strands below. Consumed by ariki-game tools/build_pugu_tifi.py garments (pugu/tapa skirts). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BLpZsSvLufgVri2bhvmAS6 --- clothing/skirt_garment_weights.py | 69 +++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/clothing/skirt_garment_weights.py b/clothing/skirt_garment_weights.py index 952c004..489b251 100644 --- a/clothing/skirt_garment_weights.py +++ b/clothing/skirt_garment_weights.py @@ -89,6 +89,17 @@ FOLLOW_MAX = _envf("SKIRT_FOLLOW_MAX", 0.20) CONTACT_R = _envf("SKIRT_CONTACT_R", 0.13) # at/inside this distance from the thigh axis = full contact FALLOFF = _envf("SKIRT_FALLOFF", 0.10) # follow decays to 0 over this much extra distance HEM_FREE = _envf("SKIRT_HEM_FREE", 0.55) # bottom fraction of panel with NO thigh follow +# PANTS TOP (2026-08-19, measured off the pack's own Peasant pants): Quaternius skins +# crotch-spanning cloth to BOTH thighs (~49/49 at the center, pelvis ≈ 0 below the +# waistband). SKIRT_PANTS_TOP= gives the garment's top fraction that same +# treatment — thigh pair split by lateral position, pelvis fading — and blends into +# the strand weights below it. 0 (default) = off, pure strand skirt. +PANTS_TOP = _envf("SKIRT_PANTS_TOP", 0.0) +# Cap on the pants share: cloth with NO strand weight is collision-DEAF (colliders +# only move strand bones), and a fully pants-weighted midline gets sliced by the +# 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) MAX_INFLUENCES = 4 COMP_FMT = {5120: "b", 5121: "B", 5122: "h", 5123: "H", 5125: "I", 5126: "f"} @@ -350,6 +361,37 @@ def main(): w[slot["pelvis"]] = 1.0 stats["waistband"] += 1 else: + # ── PANTS-TOP zone: the pack's own crotch answer (thigh pair, no strands) ── + t_cloth = max(0.0, min(1.0, (y_top - v[1]) / span)) if span > 1e-9 else 0.0 + pants_mix = 0.0 + if PANTS_TOP > 0.0: + blend_band = 0.15 + if t_cloth < PANTS_TOP: + 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_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)) + 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 + blend_band)) + pants = {slot["pelvis"]: pelvis_w, + slot["thigh_l"]: (1.0 - pelvis_w) * f_l, + slot["thigh_r"]: (1.0 - pelvis_w) * (1.0 - f_l)} + if pants_mix >= 1.0: + for j, x in pants.items(): + w[j] = w.get(j, 0.0) + x + out_j_w_done = True + 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)) + stats["follow_sum"] += 0.0 + continue # ── azimuth → the two neighbouring strands, linear across the gap ── az = math.atan2(v[2] - pelvis[2], v[0] - pelvis[0]) lo = None @@ -386,10 +428,21 @@ 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)) + 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)) + 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 if follow > 0.0: - w[slot[near]] = w.get(slot[near], 0.0) + follow + w[slot[near]] = w.get(slot[near], 0.0) + follow * strand_scale for strand, sh in share.values(): - ring = (1.0 - follow) * sh + ring = (1.0 - follow) * sh * strand_scale if ring <= 0.0: continue segs, heads = strand["segs"], strand["heads"] @@ -436,8 +489,16 @@ def main(): log(f" strand {k}: {per_strand[k]:7.1f} {bar}") dead = [k for k in sorted(per_strand) if per_strand[k] <= 0.0] if dead: - raise SystemExit(f"[skirt_weights] FATAL: strands {dead} got no weight — " - "the ring is not covered, the spring sim will tear the skirt") + # PARTIAL-COVERAGE garments (SKIRT_ALLOW_DEAD=1): a pugu loincloth is a front + # panel + back flap with OPEN sides by design — the side strands legitimately + # carry no cloth, and a bare strand just swings unskinned (harmless). Full + # skirts keep the hard gate: a dead strand under cloth tears the skirt. + if os.environ.get("SKIRT_ALLOW_DEAD") == "1": + log(f"WARNING: strands {dead} carry no weight — accepted " + "(SKIRT_ALLOW_DEAD=1, partial-coverage garment)") + else: + raise SystemExit(f"[skirt_weights] FATAL: strands {dead} got no weight — " + "the ring is not covered, the spring sim will tear the skirt") # Per-strand totals track VERTEX DENSITY per azimuth, not correctness — a # wrapped piupiu really does carry more geometry at the front overlap. The # gate that matters is the one above: every strand must be driving something,