A script scanned every tracked doc, rule, skill, agent, hook and source file for tooling/ paths that no longer exist, skipping historical records (sprints, discussions, workshops, governance, generated wiki pages). It found 62. The ones that tell a reader what to RUN now name the reach verb: - The atlas skill still sent agents to tooling/atlas, atlas-verify, atlas-update-field and atlas-commit-and-sync — about forty lines, all retired in T-1285. They now name the `reach atlas` verbs, and the skill records that commit-and-sync STAGES by default (--commit to commit) and takes --corridor as an option. - The clerk agent named tooling/clerk-review (now `reach dev clerk`). The Si and clerk briefings sent those agents to the retired tooling/db/decision and sqlite-query CLIs and to decisions/*.md paths that moved to governance/ in the pql migration. They now name pql. - The ticket-cli rule documented `pql decisions read`, which does not exist; `show` already includes the body. - The culture authoring guide and the RON sources name `reach validate ron`, with the same arguments as before. - The 41 Blender payloads' usage lines ran the retired tooling/blender wrapper, and the docstrings still cited pre-carve-out paths. They now read `reach blender run <payload>`. - Doc comments in server/, client/, wiki TOMLs and the domain modules. What is left is deliberate: "Formerly …" provenance, dated plans and findings docs, the retired-pipeline doc, and a build-artefact path. project.yaml 0.4.14 (mirrored to the client). Comment-only, but four touched files are in the canvas-version registry (trait_catalog_reader.rs, since T-1289, canvas_sources.py itself, and two client files). The gate is path-based and has no override. The previous push was rejected on exactly this. Three of the edits are stamped ledger sources, so systems.db is regenerated and the stamp is fresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
511 lines
22 KiB
Python
511 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):
|
|
reach blender run blender_author_joggers \
|
|
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()
|