35 payloads move to tooling/scripts/blender/ and stay outside package scope. They run under Blender's bundled Python, which cannot see the repo venv, so they physically cannot import tooling.core — holding them to the D-263 contract would either fail the gate forever or force the contract to be weakened for everyone, and the second is how a gate stops meaning anything. Count verified by import rather than filename: 33 import bpy/bmesh directly, and the two that do not are still payloads per their own usage lines. garment-fit/make_logo.py is the one genuine non-payload and stays for T-1290. The bash wrapper is retired rather than kept. Keeping it would have put the install-resolution logic in two places, which is the duplication T-1286 had just finished collapsing three copies of. domains/blender/service.py owns the decisions — resolve_blender (native beats flatpak, ordering preserved), resolve_payload, absolutise — and only run_payload performs. test_blender.py pins all of them without launching Blender, which matters here more than usual: the thing being launched is a 200 MB GUI application that writes GLBs. `reach blender run` takes a registered payload name OR a path to any script, because the wrapper served both — the spikes and the glb-gen skill hand it one-off scripts of their own. An unknown name enumerates all 35 and exits 2. The exclusion now defends itself. check_carve_out_stays_carved fails if `scripts` is added to PACKAGE_ROOTS, if the payload directory empties (an empty exclusion proves nothing), or if an __init__.py appears there (which would make the payloads importable — the coupling the carve-out exists to prevent). All three arms mutation-proved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
512 lines
22 KiB
Python
512 lines
22 KiB
Python
"""
|
|
blender_author_joggers.py (T-1089 wave 2, joggers_modern + sweatpant family)
|
|
|
|
Authors JOGGERS/sweatpants as per-body offset shells: hips + full legs,
|
|
tapered toward the ankle into snug cuff bands, painted waist drawstring, and
|
|
a track SIDE STRIPE carried as its own tint region. Reuses
|
|
blender_author_offset_shell.py (via blender_author_denim_pants.py's module
|
|
instance) for scene build / join / solidify / export, and reuses the denim
|
|
companion's REQUIRED lower-body practices directly (not re-derived):
|
|
|
|
* boundary WELD of coincident segment-seam rings before offsetting
|
|
(denim.weld_boundaries) — un-welded rings offset apart along diverging
|
|
normals and open cracks at the waist/knee joins.
|
|
* open-rim FLATTENING (denim.flatten_open_rims) — the segment splitter
|
|
leaves 4.5-7.6 cm jagged teeth at the waist/ankle rims; boundary verts are
|
|
pulled onto clean planes pre-offset. The flattened ankle plane IS the
|
|
jogger cuff hem.
|
|
* waist flare + post-solidify residue clamp (denim.waist_flare,
|
|
denim.clamp_waist_residue) — deep-crouch waist-fold clip mitigation.
|
|
* per-body leg-axis landmarks (denim.LegLandmarks) — thigh_l/calf_l control
|
|
points drive azimuth math for the side stripe and the cuff/band rib paint,
|
|
so every parameter derives from the body's own bones (Q-060 per-body mode).
|
|
|
|
What this companion adds, as reusable parameters (not hacks):
|
|
|
|
* TAPERED variable offset — constant baggy standoff (--offset) above the
|
|
knee, smoothstep taper to --mid-offset at the cuff top, then a short
|
|
feathered step down to the snug --cuff-offset inside the cuff band: the
|
|
sweatpant silhouette (baggy thigh -> tapered shin -> gripping cuff).
|
|
* ankle CUFF band (--cuff-frac of the garment span) — its own mask region
|
|
(A channel) with painted knit RIB (azimuth-arc alternating shades, constant
|
|
metric rib width via the per-z leg axis).
|
|
* SIDE STRIPE (--stripe-width metres, scaled by hip width) — constant-width
|
|
azimuth band along the outer leg from waistband to cuff, painted bright
|
|
AND masked to its own region (B channel) for independent tinting.
|
|
* painted elastic waistband (subtle vertical rib) + centre-front DRAWSTRING:
|
|
two eyelets and two hanging cords with a slight outward slant (albedo
|
|
only; dark cords stay legible under the luma-scaled region tint).
|
|
|
|
Regions (RGBA mask, toon_garment.gdshader): waistband -> R (tint_0),
|
|
legs -> G (tint_1), side stripe -> B (tint_2), ankle cuffs -> A (tint_3).
|
|
A parked logo_uv TEXCOORD_1 layer is authored (denim.author_parked_uv2) —
|
|
not logo-capable, but the shader samples UV2 unconditionally.
|
|
|
|
Covered segments: seg_hips + seg_leg_upper_l/r + seg_leg_lower_l/r. Natural
|
|
boundaries give the waist opening and ankle hems for free. Per-body mode only
|
|
(offset shells author per body, Q-060).
|
|
|
|
Usage (joggers_modern reference invocation):
|
|
tooling/blender --background --python \
|
|
tooling/garment-fit/blender_author_joggers.py -- \
|
|
client/assets/characters/bodies \
|
|
client/assets/characters/clothing/joggers_modern \
|
|
[--bodies average_m,child,...] [--offset 0.014] [--mid-offset 0.010] \
|
|
[--cuff-offset 0.0065] [--band-frac 0.055] [--cuff-frac 0.075] \
|
|
[--stripe-width 0.034] [--fabric 0.55,0.56,0.58] \
|
|
[--stripe-rgb 0.88,0.89,0.91] [--waist-flare 0.007] [--seed N] [--plain]
|
|
|
|
Writes per body: <out_dir>/<body>.glb (skinned, albedo embedded)
|
|
<out_dir>/<body>_mask.png (RGBA region mask, UV0)
|
|
<out_dir>/<body>_base_albedo.png
|
|
Plus: <out_dir>/base_albedo.png (average_m's, shared sidecar)
|
|
<out_dir>/reference_mask.png (average_m's, runtime fallback)
|
|
|
|
Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house
|
|
wardrobe), Q-060 (per-body offset shells).
|
|
"""
|
|
|
|
import importlib.util
|
|
import os
|
|
import shutil
|
|
import sys
|
|
|
|
import bmesh
|
|
import bpy
|
|
import numpy as np
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Import the denim companion as a module — it carries the shared lower-body
|
|
# practices (weld, rim flatten, waist flare, clamp, landmarks, parked UV2) and
|
|
# its own instance of the base offset-shell library.
|
|
# --------------------------------------------------------------------------
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
_spec = importlib.util.spec_from_file_location(
|
|
"denim_pants", os.path.join(_HERE, "blender_author_denim_pants.py"))
|
|
denim = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(denim)
|
|
|
|
base = denim.base # single shared base-module instance
|
|
log = base.log
|
|
FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (verified in base)
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Parameters (defaults = joggers_modern)
|
|
# --------------------------------------------------------------------------
|
|
COVERED_SEGMENTS = [
|
|
"seg_hips",
|
|
"seg_leg_upper_l", "seg_leg_upper_r",
|
|
"seg_leg_lower_l", "seg_leg_lower_r",
|
|
]
|
|
|
|
TEX_SIZE = 1024 # albedo + mask resolution (rib + cords need >512)
|
|
WAIST_FLARE_M = 0.007 # denim-proven deep-crouch waist-fold mitigation
|
|
|
|
# Tapered-offset profile (metres).
|
|
OFFSET_HI_M = 0.014 # hips/thigh standoff — baggy sweatpant volume
|
|
OFFSET_MID_M = 0.010 # taper target at the cuff top (shin)
|
|
OFFSET_CUFF_M = 0.0065 # snug cuff standoff (ankles barely deform)
|
|
CUFF_STEP_W_M = 0.010 # feather width of the taper->cuff step
|
|
|
|
# Vertical proportions — fractions of the garment span (waist_z - hem_z).
|
|
BAND_FRAC = 0.055 # elastic waistband height (R region)
|
|
CUFF_FRAC = 0.075 # knit cuff band height (A region) — taller than a hem
|
|
SEAM_W_FRAC = 0.008 # painted border-stitch line width
|
|
CORD_DROP_FRAC = 0.050 # drawstring cord length below the waistband
|
|
CORD_SLANT = 0.18 # outward cord slant (dx per unit hang depth)
|
|
|
|
# Horizontal / arc-metric proportions (scaled by hip half-width ratio lm.sh).
|
|
STRIPE_W_M = 0.034 # side stripe metric width (B region)
|
|
RIB_PERIOD_M = 0.009 # cuff knit-rib period (arc metres)
|
|
BAND_RIB_PERIOD_M = 0.006 # waistband elastic-rib period (arc metres)
|
|
CORD_X_M = 0.016 # drawstring eyelet/cord |x| at the band
|
|
CORD_W_M = 0.005 # cord width
|
|
EYELET_R_M = 0.0045 # eyelet dot radius
|
|
|
|
# Fleece style (sRGB floats; flat toon-friendly, texture carries identity).
|
|
FLEECE_RGB = (0.55, 0.56, 0.58) # heather grey, luma ~0.56 (tint-faithful)
|
|
STRIPE_RGB = (0.88, 0.89, 0.91) # near-white track stripe
|
|
CORD_RGB = (0.17, 0.17, 0.19) # charcoal drawstring
|
|
ALBEDO_NOISE = 0.020 # +/- fleece jitter
|
|
BAND_SHADE = 0.94 # waistband overall darkening
|
|
CUFF_SHADE = 0.96 # cuff overall darkening
|
|
RIB_LO = 0.86 # cuff rib dark-stripe factor
|
|
BAND_RIB_LO = 0.94 # waistband rib dark-stripe factor
|
|
STITCH_SHADE = 0.80 # band/cuff border-stitch darkening
|
|
PLAIN = False # --plain: skip drawstring/rib/stitch paint
|
|
NOISE_SEED = 2090 # distinct from denim (2089)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Joggers landmark finalization (denim.LegLandmarks carries the leg axis; the
|
|
# band/cuff fractions here are jogger parameters, not denim's)
|
|
# --------------------------------------------------------------------------
|
|
|
|
def finalize_jogger_landmarks(lm, waist_plane, ankle_plane,
|
|
band_frac, cuff_frac):
|
|
lm.waist_z = waist_plane
|
|
lm.hem_z = ankle_plane
|
|
lm.span = waist_plane - ankle_plane
|
|
lm.sh = lm.hip_x / denim._REF_HIP_X
|
|
lm.band_h = band_frac * lm.span
|
|
lm.cuff_h = cuff_frac * lm.span
|
|
lm.seam_w = SEAM_W_FRAC * lm.span
|
|
lm.band_z = waist_plane - lm.band_h
|
|
lm.cuff_top = ankle_plane + lm.cuff_h
|
|
log(f"landmarks: waist={lm.waist_z:.3f} hem={lm.hem_z:.3f} "
|
|
f"span={lm.span:.3f} hip_x={lm.hip_x:.3f} band_z={lm.band_z:.3f} "
|
|
f"cuff_top={lm.cuff_top:.3f} sh={lm.sh:.2f}")
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Tapered offset — the jogger silhouette
|
|
# --------------------------------------------------------------------------
|
|
|
|
def _smoothstep(u):
|
|
u = min(max(u, 0.0), 1.0)
|
|
return u * u * (3.0 - 2.0 * u)
|
|
|
|
|
|
def tapered_offset(shell, knee_z, cuff_top, off_hi, off_mid, off_cuff,
|
|
step_w):
|
|
"""Per-vertex outward offset along smoothed normals: `off_hi` above the
|
|
knee, smoothstep taper to `off_mid` at the cuff top, then a feathered step
|
|
down to the snug `off_cuff` inside the cuff band. The step reads as the
|
|
cuff ledge; the A-region tint + painted rib carry the rest."""
|
|
me = shell.data
|
|
bm = bmesh.new()
|
|
bm.from_mesh(me)
|
|
bm.normal_update()
|
|
denom = max(knee_z - cuff_top, 1e-6)
|
|
lo = hi = None
|
|
for v in bm.verts:
|
|
z = v.co.z
|
|
t = off_hi + (off_mid - off_hi) * _smoothstep((knee_z - z) / denom)
|
|
s = _smoothstep((cuff_top + step_w - z) / step_w)
|
|
t = t + (off_cuff - t) * s
|
|
v.co += v.normal * t
|
|
lo = t if lo is None else min(lo, t)
|
|
hi = t if hi is None else max(hi, t)
|
|
bm.to_mesh(me)
|
|
bm.free()
|
|
me.update()
|
|
log(f"tapered offset: {hi * 1000:.1f} mm (thigh) -> {lo * 1000:.1f} mm "
|
|
f"(cuff); knee_z={knee_z:.3f} cuff_top={cuff_top:.3f}")
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Jogger feature field (texel-level; drives albedo AND mask together)
|
|
# --------------------------------------------------------------------------
|
|
|
|
def _jogger_field(px, py, pz, lm):
|
|
"""Evaluate jogger features at texel 3D positions (numpy arrays).
|
|
|
|
Returns (front, in_band, in_cuff, stripe, arc): `stripe` feeds the mask B
|
|
channel, `in_band`/`in_cuff` feed R/A, `arc` is the azimuth arc-length
|
|
coordinate around the per-z leg axis used by the rib paint.
|
|
"""
|
|
front = py * FRONT_Y_SIGN > 0.004
|
|
in_band = pz >= lm.band_z
|
|
in_cuff = pz <= lm.cuff_top
|
|
mid = (~in_band) & (~in_cuff)
|
|
|
|
# Per-z leg axis, mirrored by x sign (denim.LegLandmarks control points).
|
|
side = np.where(px >= 0.0, 1.0, -1.0)
|
|
cx = side * np.interp(pz, lm.leg_z_pts, lm.leg_x_pts)
|
|
cy = np.interp(pz, lm.leg_z_pts, lm.leg_y_pts)
|
|
dx = px - cx
|
|
dy = py - cy
|
|
r = np.hypot(dx, dy) + 1e-9
|
|
|
|
# Side stripe: azimuth toward the outer (+/-x) direction; constant metric
|
|
# width via arc distance (same construction as the denim outseam).
|
|
arc_out = np.arccos(np.clip(dx * side / r, -1.0, 1.0)) * r
|
|
stripe = mid & (arc_out < 0.5 * STRIPE_W_M * lm.sh)
|
|
|
|
# Rib coordinate: signed azimuth arc length (0 at the outseam; the +/-pi
|
|
# wrap sits at the inner ankle / body centre where it cannot be seen).
|
|
arc = np.arctan2(dy, dx * side) * r
|
|
return front, in_band, in_cuff, stripe, arc
|
|
|
|
|
|
def _paint_texels(px, py, pz, noise, lm):
|
|
"""Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions."""
|
|
n = px.shape[0]
|
|
front, in_band, in_cuff, stripe, arc = _jogger_field(px, py, pz, lm)
|
|
|
|
# --- albedo ------------------------------------------------------------
|
|
alb = np.empty((n, 4), dtype=np.float32)
|
|
for c in range(3):
|
|
alb[:, c] = FLEECE_RGB[c] + noise
|
|
alb[:, 3] = 1.0
|
|
|
|
# Side stripe: bright, low-noise (crisp edge carries the sport read).
|
|
for c in range(3):
|
|
alb[stripe, c] = STRIPE_RGB[c] + 0.5 * noise[stripe]
|
|
|
|
if not PLAIN:
|
|
# Cuff knit rib: alternating shade stripes, constant metric width.
|
|
rib_dark = (np.floor(arc / (RIB_PERIOD_M * lm.sh)).astype(np.int64)
|
|
% 2) == 0
|
|
cuff_f = np.where(rib_dark, RIB_LO, 1.0) * CUFF_SHADE
|
|
alb[in_cuff, :3] *= cuff_f[in_cuff, None]
|
|
|
|
# Waistband elastic rib: same construction, subtler.
|
|
band_dark = (np.floor(arc / (BAND_RIB_PERIOD_M * lm.sh))
|
|
.astype(np.int64) % 2) == 0
|
|
band_f = np.where(band_dark, BAND_RIB_LO, 1.0) * BAND_SHADE
|
|
alb[in_band, :3] *= band_f[in_band, None]
|
|
|
|
# Border stitching: darker lines under the band and above the cuff.
|
|
w2 = lm.seam_w * 0.5
|
|
stitch = (np.abs(pz - lm.band_z) < w2) | (np.abs(pz - lm.cuff_top) < w2)
|
|
alb[stitch, :3] *= STITCH_SHADE
|
|
|
|
# Drawstring: two eyelets on the band + two slanted hanging cords.
|
|
ex = CORD_X_M * lm.sh
|
|
eyelet_z = lm.band_z + 0.35 * lm.band_h
|
|
eyelets = front & (
|
|
np.hypot(np.abs(px) - ex, pz - eyelet_z) < EYELET_R_M * lm.sh)
|
|
drop = CORD_DROP_FRAC * lm.span
|
|
hang = np.clip(eyelet_z - pz, 0.0, None)
|
|
cord_cx = ex + CORD_SLANT * hang
|
|
cords = front & (pz > eyelet_z - drop) & (pz <= eyelet_z) \
|
|
& (np.abs(np.abs(px) - cord_cx) < 0.5 * CORD_W_M * lm.sh)
|
|
for c in range(3):
|
|
alb[eyelets | cords, c] = CORD_RGB[c]
|
|
else:
|
|
alb[in_cuff, :3] *= CUFF_SHADE
|
|
alb[in_band, :3] *= BAND_SHADE
|
|
|
|
# --- region mask: band R / legs G / stripe B / cuffs A -------------------
|
|
mask = np.zeros((n, 4), dtype=np.float32)
|
|
is_b = stripe & ~in_band & ~in_cuff
|
|
is_g = ~(in_band | in_cuff | is_b)
|
|
mask[in_band, 0] = 1.0
|
|
mask[is_g, 1] = 1.0
|
|
mask[is_b, 2] = 1.0
|
|
mask[in_cuff, 3] = 1.0
|
|
return alb, mask
|
|
|
|
|
|
def _raster_tri_paint(alb_buf, mask_buf, noise_buf, uvs, cos, lm, W, H):
|
|
"""Barycentric texel fill of one UV triangle: interpolate 3D positions,
|
|
evaluate the jogger field, write albedo + mask together (same rasterizer
|
|
construction as the denim companion; the paint field is what differs)."""
|
|
a, b, c = uvs
|
|
A, B, C = cos
|
|
ax, ay = a.x * (W - 1), a.y * (H - 1)
|
|
bx, by = b.x * (W - 1), b.y * (H - 1)
|
|
cx, cy = c.x * (W - 1), c.y * (H - 1)
|
|
minx = max(int(np.floor(min(ax, bx, cx))), 0)
|
|
maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1)
|
|
miny = max(int(np.floor(min(ay, by, cy))), 0)
|
|
maxy = min(int(np.ceil(max(ay, by, cy))), H - 1)
|
|
if minx > maxx or miny > maxy:
|
|
return
|
|
denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
|
|
if abs(denom) < 1e-9:
|
|
return
|
|
ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1]
|
|
pxg = xs + 0.5
|
|
pyg = ys + 0.5
|
|
w0 = ((by - cy) * (pxg - cx) + (cx - bx) * (pyg - cy)) / denom
|
|
w1 = ((cy - ay) * (pxg - cx) + (ax - cx) * (pyg - cy)) / denom
|
|
w2 = 1.0 - w0 - w1
|
|
inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4)
|
|
if not inside.any():
|
|
return
|
|
w0i, w1i, w2i = w0[inside], w1[inside], w2[inside]
|
|
px3 = w0i * A.x + w1i * B.x + w2i * C.x
|
|
py3 = w0i * A.y + w1i * B.y + w2i * C.y
|
|
pz3 = w0i * A.z + w1i * B.z + w2i * C.z
|
|
ysin = ys[inside]
|
|
xsin = xs[inside]
|
|
alb, mask = _paint_texels(px3, py3, pz3, noise_buf[ysin, xsin], lm)
|
|
alb_buf[ysin, xsin] = alb
|
|
mask_buf[ysin, xsin] = mask
|
|
|
|
|
|
def paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body):
|
|
"""Rasterize all UV0 triangles once, producing the painted albedo and the
|
|
region mask from one shared feature-field evaluation per texel."""
|
|
W = H = TEX_SIZE
|
|
rng = np.random.default_rng(NOISE_SEED)
|
|
noise_buf = ((rng.random((H, W), dtype=np.float32) - 0.5)
|
|
* 2.0 * ALBEDO_NOISE)
|
|
|
|
alb_buf = np.empty((H, W, 4), dtype=np.float32)
|
|
for c in range(3):
|
|
alb_buf[:, :, c] = FLEECE_RGB[c] + noise_buf
|
|
alb_buf[:, :, 3] = 1.0
|
|
mask_buf = np.zeros((H, W, 4), dtype=np.float32)
|
|
mask_buf[:, :, 1] = 1.0 # background = legs green (bleed-safe)
|
|
|
|
me = shell.data
|
|
bm = bmesh.new()
|
|
bm.from_mesh(me)
|
|
bm.faces.ensure_lookup_table()
|
|
if not len(bm.loops.layers.uv):
|
|
raise RuntimeError("no UV layer for albedo/mask paint")
|
|
uv_layer = bm.loops.layers.uv[0]
|
|
|
|
tri_count = 0
|
|
for face in bm.faces:
|
|
loops = face.loops[:]
|
|
uvs = [loop[uv_layer].uv.copy() for loop in loops]
|
|
cos = [loop.vert.co.copy() for loop in loops]
|
|
for i in range(1, len(uvs) - 1):
|
|
_raster_tri_paint(
|
|
alb_buf, mask_buf, noise_buf,
|
|
(uvs[0], uvs[i], uvs[i + 1]),
|
|
(cos[0], cos[i], cos[i + 1]),
|
|
lm, W, H)
|
|
tri_count += 1
|
|
bm.free()
|
|
log(f"painted {tri_count} UV triangles -> albedo + mask ({W}x{H})")
|
|
|
|
def _save(buf, name, path):
|
|
img = bpy.data.images.new(name, W, H, alpha=True)
|
|
img.pixels.foreach_set(buf.reshape(-1))
|
|
img.update()
|
|
img.filepath_raw = path
|
|
img.file_format = 'PNG'
|
|
img.save()
|
|
return img
|
|
|
|
albedo_img = _save(alb_buf, f"jogger_albedo_{body}", albedo_path)
|
|
_save(mask_buf, f"jogger_mask_{body}", mask_path)
|
|
log(f"saved albedo -> {albedo_path}")
|
|
log(f"saved mask -> {mask_path}")
|
|
return albedo_img
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Per-body authoring
|
|
# --------------------------------------------------------------------------
|
|
|
|
def author_jogger_shell(body_dir, out_dir, body):
|
|
crotch_z, _hips_top = denim.probe_hips_bounds(body_dir)
|
|
|
|
base.clear_scene()
|
|
base.COVERED_SEGMENTS = COVERED_SEGMENTS
|
|
shell, armature = base.build_covered_mesh(body_dir)
|
|
denim.weld_boundaries(shell)
|
|
|
|
zs = [v.co.z for v in shell.data.vertices]
|
|
waist_z, ankle_z = max(zs), min(zs)
|
|
lm = denim.LegLandmarks(armature, waist_z, ankle_z, crotch_z + 0.005)
|
|
|
|
# Full-length hem AT the ankle joint (calf tail); rim teeth flattened onto
|
|
# clean planes pre-offset (denim practice — no jagged silhouette).
|
|
ankle_plane = float(lm.leg_z_pts[0])
|
|
waist_plane, ankle_plane = denim.flatten_open_rims(shell, ankle_plane)
|
|
|
|
span = waist_plane - ankle_plane
|
|
knee_z = float(lm.leg_z_pts[1]) # calf head
|
|
cuff_top = ankle_plane + CUFF_FRAC * span
|
|
tapered_offset(shell, knee_z, cuff_top, OFFSET_HI_M, OFFSET_MID_M,
|
|
OFFSET_CUFF_M, CUFF_STEP_W_M)
|
|
denim.waist_flare(shell, waist_plane, BAND_FRAC * span, WAIST_FLARE_M)
|
|
base.solidify(shell, base.CLOTH_THICKNESS_M)
|
|
denim.clamp_waist_residue(shell, waist_plane)
|
|
|
|
# Landmarks reference the CLEAN rims for the paint/mask pass.
|
|
finalize_jogger_landmarks(lm, waist_plane, ankle_plane, BAND_FRAC,
|
|
CUFF_FRAC)
|
|
denim.author_parked_uv2(shell)
|
|
|
|
albedo_path = os.path.join(out_dir, f"{body}_base_albedo.png")
|
|
mask_path = os.path.join(out_dir, f"{body}_mask.png")
|
|
albedo_img = paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body)
|
|
base.assign_fabric_material(shell, albedo_img)
|
|
|
|
base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb"))
|
|
|
|
|
|
def main():
|
|
global OFFSET_HI_M, OFFSET_MID_M, OFFSET_CUFF_M, BAND_FRAC, CUFF_FRAC
|
|
global STRIPE_W_M, FLEECE_RGB, STRIPE_RGB, WAIST_FLARE_M, NOISE_SEED, PLAIN
|
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
|
if len(argv) < 2:
|
|
print("Usage: -- <bodies_root> <out_dir> [--bodies a,b,c] "
|
|
"[--offset M] [--mid-offset M] [--cuff-offset M] "
|
|
"[--band-frac F] [--cuff-frac F] [--stripe-width M] "
|
|
"[--fabric r,g,b] [--stripe-rgb r,g,b] [--waist-flare M] "
|
|
"[--seed N] [--plain]")
|
|
sys.exit(1)
|
|
bodies_root = argv[0]
|
|
out_dir = argv[1]
|
|
if "--offset" in argv:
|
|
OFFSET_HI_M = float(argv[argv.index("--offset") + 1])
|
|
if "--mid-offset" in argv:
|
|
OFFSET_MID_M = float(argv[argv.index("--mid-offset") + 1])
|
|
if "--cuff-offset" in argv:
|
|
OFFSET_CUFF_M = float(argv[argv.index("--cuff-offset") + 1])
|
|
if "--band-frac" in argv:
|
|
BAND_FRAC = float(argv[argv.index("--band-frac") + 1])
|
|
if "--cuff-frac" in argv:
|
|
CUFF_FRAC = float(argv[argv.index("--cuff-frac") + 1])
|
|
if "--stripe-width" in argv:
|
|
STRIPE_W_M = float(argv[argv.index("--stripe-width") + 1])
|
|
if "--fabric" in argv:
|
|
FLEECE_RGB = tuple(
|
|
float(v) for v in argv[argv.index("--fabric") + 1].split(","))
|
|
if "--stripe-rgb" in argv:
|
|
STRIPE_RGB = tuple(
|
|
float(v) for v in argv[argv.index("--stripe-rgb") + 1].split(","))
|
|
if "--waist-flare" in argv:
|
|
WAIST_FLARE_M = float(argv[argv.index("--waist-flare") + 1])
|
|
if "--seed" in argv:
|
|
NOISE_SEED = int(argv[argv.index("--seed") + 1])
|
|
if "--plain" in argv:
|
|
PLAIN = True
|
|
bodies = base.BODY_TYPES
|
|
if "--bodies" in argv:
|
|
bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")]
|
|
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
log(f"joggers per-body mode: {len(bodies)} bodies, offset "
|
|
f"{OFFSET_HI_M * 1000:.0f}->{OFFSET_CUFF_M * 1000:.1f} mm, "
|
|
f"band_frac={BAND_FRAC} cuff_frac={CUFF_FRAC} "
|
|
f"stripe={STRIPE_W_M * 1000:.0f} mm, plain={PLAIN}")
|
|
|
|
results = []
|
|
for body in bodies:
|
|
body_dir = os.path.join(bodies_root, body)
|
|
log(f"=== {body} ===")
|
|
if not os.path.isdir(body_dir):
|
|
results.append((body, "skipped: body dir missing"))
|
|
continue
|
|
try:
|
|
author_jogger_shell(body_dir, out_dir, body)
|
|
results.append((body, "ok"))
|
|
except Exception as exc:
|
|
log(f"ERROR {body}: {exc}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
results.append((body, f"error: {exc}"))
|
|
|
|
ref = base.REFERENCE_BODY
|
|
ref_mask = os.path.join(out_dir, f"{ref}_mask.png")
|
|
if os.path.isfile(ref_mask):
|
|
shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png"))
|
|
log(f"copied {ref}_mask.png -> reference_mask.png (fallback)")
|
|
ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png")
|
|
if os.path.isfile(ref_alb):
|
|
shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png"))
|
|
log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)")
|
|
|
|
log("=" * 50)
|
|
for body, status in results:
|
|
log(f" {body:12s} {status}")
|
|
ok = sum(1 for _, s in results if s == "ok")
|
|
log(f"OK={ok}/{len(results)}")
|
|
if ok != len(results):
|
|
sys.exit(1)
|
|
log("DONE")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|