feat(skirt-weights): inner-front anchor + import cache-bust
SKIRT_ANCHOR_IFP: pins the front-inner band of a garment to its thigh at W=1.0 (asymmetric sector: 100deg inner / 135deg outer so back cloth never pins to the thigh front) — the stitch answer to colliders being unable to move strand root joints (probe-measured 32mm penetration at full stride). save() now stamps asset.extras.skirt_weights_rev every run: weight-only rewrites left the .gltf byte-identical, Godot's source_md5 check skipped the reimport, and engines ran STALE weights (cost 2026-08-20 afternoon).
This commit is contained in:
@@ -46,6 +46,7 @@ import math
|
|||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
ANIM_ROOT = os.path.dirname(HERE)
|
ANIM_ROOT = os.path.dirname(HERE)
|
||||||
@@ -139,6 +140,32 @@ PANTS_FROM = _envf("SKIRT_PANTS_FROM", 0.0)
|
|||||||
TUBE = os.environ.get("SKIRT_FOLLOW_TUBE") == "1"
|
TUBE = os.environ.get("SKIRT_FOLLOW_TUBE") == "1"
|
||||||
TUBE_FRONT = _envf("SKIRT_TUBE_FRONT", 45.0) # half-angle of the full-follow front sector, degrees
|
TUBE_FRONT = _envf("SKIRT_TUBE_FRONT", 45.0) # half-angle of the full-follow front sector, degrees
|
||||||
TUBE_BACK = _envf("SKIRT_TUBE_BACK", 0.25) # follow multiplier directly behind the leg
|
TUBE_BACK = _envf("SKIRT_TUBE_BACK", 0.25) # follow multiplier directly behind the leg
|
||||||
|
# INNER-FRONT ANCHOR (2026-08-20, Ozan watching the male run: "the front pugu can
|
||||||
|
# go UNDER the balls — attach the clothing to the blue dots' furthest points;
|
||||||
|
# as legs move, cloth stretches freely"). Push-only colliders can never stop the
|
||||||
|
# panel sliding under the pusher balls — attachment can. The front panel's
|
||||||
|
# front-INNER band (the diagonal the InnerFrontPush_* colliders ride) is pinned
|
||||||
|
# to its thigh at SKIRT_ANCHOR_W, so the advancing leg CARRIES its cloth and
|
||||||
|
# only the span between the two anchored bands stays spring-driven (the stretch
|
||||||
|
# membrane). Requires SKIRT_FOLLOW_TUBE=1 (the anchor sector keys off tube_fwd).
|
||||||
|
ANCHOR = os.environ.get("SKIRT_ANCHOR_IFP") == "1"
|
||||||
|
ANCHOR_W = _envf("SKIRT_ANCHOR_W", 1.0) # pin strength at the sector center
|
||||||
|
ANCHOR_TOP = _envf("SKIRT_ANCHOR_TOP", 0.04) # band, fraction of cloth drop
|
||||||
|
ANCHOR_BOT = _envf("SKIRT_ANCHOR_BOT", 0.58)
|
||||||
|
ANCHOR_FADE = _envf("SKIRT_ANCHOR_FADE", 0.06) # vertical edge fade (no hard step)
|
||||||
|
ANCHOR_FRONT = _envf("SKIRT_ANCHOR_FRONT", 0.90) # diagonal mix — matches SKIRT_IFP_*
|
||||||
|
ANCHOR_INNER = _envf("SKIRT_ANCHOR_INNER", 0.45)
|
||||||
|
# 2026-08-20 Ozan: "two legs work independently — stitch the cloth to the balls'
|
||||||
|
# outer direction" + "the cloth trying to make an inner curve shows body —
|
||||||
|
# disable it". Sector widened 55°→80°: each leg's whole front quadrant rides its
|
||||||
|
# thigh rigidly; only the narrow midline strip stays spring (the stretch zone).
|
||||||
|
ANCHOR_SECTOR = _envf("SKIRT_ANCHOR_SECTOR", 100.0) # half-angle toward INNER, degrees
|
||||||
|
# Probe finding (2026-08-20): the strand-top penetrations drive the panel's top
|
||||||
|
# OUTER corners (110–135° from the diagonal — the hip/outer-front cloth), which
|
||||||
|
# the symmetric sector rejected; those corners are what slid under the flexed
|
||||||
|
# thigh. The outer side gets a wider gate; the inner side stays narrower so
|
||||||
|
# BACK-flap verts (147°+ from the diagonal) are never pinned to the thigh front.
|
||||||
|
ANCHOR_SECTOR_OUTER = _envf("SKIRT_ANCHOR_SECTOR_OUTER", 135.0)
|
||||||
# Azimuth spread, in strand-gap units (2026-08-19, long tifi): the classic
|
# 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
|
# 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
|
# tube creases into a hard triangle where the front-leg cloth meets the
|
||||||
@@ -253,6 +280,12 @@ class Gltf:
|
|||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
self.d["buffers"][0]["byteLength"] = len(self.buf)
|
self.d["buffers"][0]["byteLength"] = len(self.buf)
|
||||||
|
# CACHE-BUST (2026-08-20, cost an afternoon): weight-only rewrites leave the
|
||||||
|
# .gltf byte-identical → Godot's source_md5 check skips the reimport and the
|
||||||
|
# engine keeps running STALE weights. Stamp a changing extras token so the
|
||||||
|
# md5 moves every run and the importer always picks the new .bin up.
|
||||||
|
self.d.setdefault("asset", {})["extras"] = {
|
||||||
|
"skirt_weights_rev": time.time_ns()}
|
||||||
with open(self.bin_path, "wb") as f:
|
with open(self.bin_path, "wb") as f:
|
||||||
f.write(self.buf)
|
f.write(self.buf)
|
||||||
with open(self.path, "w") as f:
|
with open(self.path, "w") as f:
|
||||||
@@ -413,7 +446,7 @@ def main():
|
|||||||
f"cloth hangs {span:.3f} below the ring top")
|
f"cloth hangs {span:.3f} below the ring top")
|
||||||
|
|
||||||
out_j, out_w = [], []
|
out_j, out_w = [], []
|
||||||
stats = {"waistband": 0, "follow_sum": 0.0}
|
stats = {"waistband": 0, "follow_sum": 0.0, "anchored": 0}
|
||||||
per_strand = {k: 0.0 for k in body_ring}
|
per_strand = {k: 0.0 for k in body_ring}
|
||||||
thigh_total = 0.0
|
thigh_total = 0.0
|
||||||
|
|
||||||
@@ -541,7 +574,39 @@ def main():
|
|||||||
(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
|
||||||
|
# ── inner-front anchor strength for this vertex (0 = free cloth) ──
|
||||||
|
anchor = 0.0
|
||||||
|
if (ANCHOR and tube_fwd is not None and near is not None
|
||||||
|
and radial is not None and ANCHOR_TOP <= t <= ANCHOR_BOT):
|
||||||
|
rl = math.hypot(radial[0], radial[1])
|
||||||
|
if rl > 1e-6:
|
||||||
|
sign_l = 1.0 if origin["thigh_l"][0] >= pelvis[0] else -1.0
|
||||||
|
inner_x = -sign_l if near == "thigh_l" else sign_l
|
||||||
|
dx = tube_fwd[0] * ANCHOR_FRONT + inner_x * ANCHOR_INNER
|
||||||
|
dz = tube_fwd[1] * ANCHOR_FRONT
|
||||||
|
dn = math.hypot(dx, dz)
|
||||||
|
if dn > 1e-6:
|
||||||
|
cos_d = (radial[0] * dx + radial[1] * dz) / (rl * dn)
|
||||||
|
# Signed side of the diagonal: inner vs outer (mirrored
|
||||||
|
# per leg — inner_x flips the 2D cross sign).
|
||||||
|
cross = dx * radial[1] - dz * radial[0]
|
||||||
|
lim = (ANCHOR_SECTOR if cross * (-inner_x) > 0.0
|
||||||
|
else ANCHOR_SECTOR_OUTER)
|
||||||
|
cos_sec = math.cos(math.radians(lim))
|
||||||
|
if cos_d > cos_sec:
|
||||||
|
af = smoothstep((cos_d - cos_sec) / max(1e-6, 1.0 - cos_sec))
|
||||||
|
vf = (smoothstep((t - ANCHOR_TOP) / ANCHOR_FADE)
|
||||||
|
* (1.0 - smoothstep((t - (ANCHOR_BOT - ANCHOR_FADE))
|
||||||
|
/ ANCHOR_FADE)))
|
||||||
|
anchor = ANCHOR_W * af * vf
|
||||||
strand_scale = 1.0 - pants_mix
|
strand_scale = 1.0 - pants_mix
|
||||||
|
if ANCHOR and anchor > 0.0:
|
||||||
|
# Rigid pin of the front-inner band to its thigh; the ring keeps
|
||||||
|
# only what's left, so the panel between the bands is the stretch
|
||||||
|
# membrane (Ozan 2026-08-20).
|
||||||
|
w[slot[near]] = w.get(slot[near], 0.0) + anchor * strand_scale
|
||||||
|
stats["anchored"] += 1
|
||||||
|
strand_scale *= 1.0 - anchor
|
||||||
if PANTS_SOLID4 and pants_mix > 0.0:
|
if PANTS_SOLID4 and pants_mix > 0.0:
|
||||||
# exactly ONE strand bone: nearest strand by azimuth, its
|
# exactly ONE strand bone: nearest strand by azimuth, its
|
||||||
# height-matched segment — total influence count stays at 4.
|
# height-matched segment — total influence count stays at 4.
|
||||||
@@ -605,6 +670,9 @@ def main():
|
|||||||
log(f"waistband (pelvis only): {stats['waistband']} verts")
|
log(f"waistband (pelvis only): {stats['waistband']} verts")
|
||||||
log(f"mean thigh follow on panel verts: "
|
log(f"mean thigh follow on panel verts: "
|
||||||
f"{stats['follow_sum'] / max(1, n - stats['waistband']):.3f}")
|
f"{stats['follow_sum'] / max(1, n - stats['waistband']):.3f}")
|
||||||
|
if ANCHOR:
|
||||||
|
log(f"inner-front anchor: {stats['anchored']} verts pinned "
|
||||||
|
f"(band {ANCHOR_TOP:.2f}–{ANCHOR_BOT:.2f}, W={ANCHOR_W:.2f})")
|
||||||
log("per-strand ring weight (must be non-zero everywhere):")
|
log("per-strand ring weight (must be non-zero everywhere):")
|
||||||
for k in sorted(per_strand):
|
for k in sorted(per_strand):
|
||||||
peak = max(per_strand.values())
|
peak = max(per_strand.values())
|
||||||
|
|||||||
Reference in New Issue
Block a user