diff --git a/clothing/skirt_garment_weights.py b/clothing/skirt_garment_weights.py index e3ba90f..b731551 100644 --- a/clothing/skirt_garment_weights.py +++ b/clothing/skirt_garment_weights.py @@ -126,6 +126,19 @@ PANTS_SOLID4 = os.environ.get("SKIRT_PANTS_SOLID4") == "1" # 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) +# FOLLOW TUBE (2026-08-20, Ozan: "the leg is a tube pushing in and out, not just the +# line — govern the front quarter"): proximity follow keys off distance to the thigh +# AXIS, so cloth beside the stitch line follows the line while the leg's VOLUME +# sweeps through it. With SKIRT_FOLLOW_TUBE=1 the contact term is modulated by the +# vertex's radial direction around the leg: the FRONT sector (±TUBE_FRONT degrees +# of the body's forward axis, measured off foot→ball) follows at full strength, +# fading to TUBE_BACK directly behind. The advancing leg's front face then carries +# its cloth; cloth behind the leg barely follows (it never gets hauled upward on +# the backswing — the ride-up channel stays shut). 0 (default) = legacy radial +# follow, approved recipes bit-identical. +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_BACK = _envf("SKIRT_TUBE_BACK", 0.25) # follow multiplier directly behind the leg # 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 @@ -171,13 +184,14 @@ def mat4_inverse_translation(m): def point_seg_distance(p, a, b): - """Distance from p to segment ab (thigh head→knee).""" + """Distance from p to segment ab (thigh head→knee), plus the closest point + (the follow tube needs the radial direction around the leg, not just the gap).""" ab = tuple(b[i] - a[i] for i in range(3)) ap = tuple(p[i] - a[i] for i in range(3)) ll = sum(v * v for v in ab) t = 0.0 if ll < 1e-12 else max(0.0, min(1.0, sum(ap[i] * ab[i] for i in range(3)) / ll)) closest = tuple(a[i] + ab[i] * t for i in range(3)) - return math.sqrt(sum((p[i] - closest[i]) ** 2 for i in range(3))) + return math.sqrt(sum((p[i] - closest[i]) ** 2 for i in range(3))), closest class Gltf: @@ -270,6 +284,21 @@ def main(): body_ibms = body.read(body_skin["inverseBindMatrices"]) body_ibm = {n: m for n, m in zip(body_names, body_ibms) if n} + # Forward axis for the follow tube, measured off the BODY's foot→ball (toe) + # direction in the horizontal plane — never an assumed axis constant. + tube_fwd = None + if TUBE: + if "foot_l" in body_ibm and "ball_l" in body_ibm: + fa = mat4_inverse_translation(body_ibm["foot_l"]) + ta = mat4_inverse_translation(body_ibm["ball_l"]) + dx, dz = ta[0] - fa[0], ta[2] - fa[2] + n = math.hypot(dx, dz) + if n > 1e-6: + tube_fwd = (dx / n, dz / n) + if tube_fwd is None: + log("WARNING: SKIRT_FOLLOW_TUBE=1 but no foot_l/ball_l on the body — " + "falling back to legacy radial follow") + ibms = g.read(skin["inverseBindMatrices"]) # ── The RIG defines the ring; the garment must carry ALL of it ────────────── @@ -471,12 +500,24 @@ def main(): t = max(0.0, min(1.0, (y_top - v[1]) / span)) if span > 1e-9 else 0.0 # ── thigh follow by proximity, faded out across the free hem ── - near, dist = None, 1e9 + near, dist, radial = None, 1e9, None for bone, a, b in legs: - dd = point_seg_distance(v, a, b) + dd, cp = point_seg_distance(v, a, b) if dd < dist: near, dist = bone, dd + radial = (v[0] - cp[0], v[2] - cp[2]) contact = 1.0 - smoothstep((dist - CONTACT_R) / FALLOFF) + if tube_fwd is not None and radial is not None: + # Quarter-tube govern (Ozan 2026-08-20): the leg's front face owns + # its cloth; the back of the leg barely follows. + rl = math.hypot(radial[0], radial[1]) + if rl > 1e-6: + cos_fwd = (radial[0] * tube_fwd[0] + radial[1] * tube_fwd[1]) / rl + cos_front = math.cos(math.radians(TUBE_FRONT)) + if cos_fwd < cos_front: + # 0 at the sector edge → 1 directly behind the leg + u = (cos_front - cos_fwd) / (cos_front + 1.0) + contact *= 1.0 - (1.0 - TUBE_BACK) * smoothstep(u) hem_fade = 1.0 - smoothstep((t - (1.0 - HEM_FREE)) / HEM_FREE) follow = FOLLOW_MAX * contact * hem_fade stats["follow_sum"] += follow