""" blender_author_denim_pants.py (T-1089, jeans_modern + denim pants family) Authors full-length DENIM pants as per-body offset shells, reusing blender_author_offset_shell.py as a library (scene build, join, offset, solidify, GLB export). Sibling companions already cover the plain lower-body family (blender_author_offset_shell_legs.py — shorts, 2-region; blender_author_lower_shell.py — formal, crease lines); this one adds what denim needs and they don't provide, as reusable parameters: * TEXEL-level feature painting: every UV0 triangle is rasterized with barycentric-interpolated 3D positions, and an analytic denim feature field is evaluated per texel. ONE field evaluation drives BOTH outputs, so the painted albedo and the region mask always agree: - albedo: painted seam thread lines (outseam/inseam azimuth around the per-z leg axis, centre-front fly, back yoke V), front pocket arcs, back pocket outlines, belt loops, waist button, waist/cuff border stitching — flat toon-friendly, identity carried by the texture (style pin: modern only). - mask: waistband -> R, legs -> G, seams + cuff band -> B (spec: waistband=R, legs=G, seams/cuffs=B). Seam LINES are in the B channel, not just the cuff band, so contrast-thread recoloring works (toon_garment.gdshader channel-blends). * boundary WELD before offsetting — the waist join (hips<->leg_upper) and knee join (leg_upper<->leg_lower) carry duplicated boundary-ring verts per segment; offsetting un-welded rings along diverging normals opens cracks, so coincident verts are merged first (weights identical by origin, so skinning is unaffected). * --hem-frac: fraction of the hip->ankle span covered, measured up from the ankle (1.0 = full length to the ankle = jeans; lower values give cropped variants; cut rims are capped by Solidify(use_rim) like the base sleeves). * a parked logo_uv TEXCOORD_1 layer (all UVs at (2,2)): pants are not logo-capable, but toon_garment.gdshader samples UV2 unconditionally, so every multi_region garment ships a well-defined logo channel. * open-rim FLATTENING: the segment splitter cuts along weight thresholds, so the waist and ankle boundary rings are jagged "teeth" (~5-6 cm deep on average_m); boundary verts are pulled onto clean planes (waist ring down to its own valley, ankle rings onto the ankle-joint plane) pre-offset. * --waist-flare: extra feathered radial stand-off at the waistband rim — deep-crouch waist-fold clip mitigation (QA evidence, peasant + jeans). Covered segments: seg_hips + seg_leg_upper_l/r + seg_leg_lower_l/r. Natural boundaries give the waist opening (seg_hips top rim) and the ankle hems (seg_leg_lower bottom) for free. All cut/mask/paint parameters derive PER BODY from that body's own bone landmarks (thigh_l/calf_l line) and measured mesh extents (waist rim, ankle rim, crotch = seg_hips lowest ring), scaled by the body's garment span and hip half-width — the same proportional-ratio philosophy as base.derive_thresholds, so the denim details stay consistent across all 11 bodies. Per-body mode only (offset shells author per body, Q-060). Usage (jeans_modern reference invocation): tooling/blender --background --python \ tooling/scripts/blender/blender_author_denim_pants.py -- \ client/assets/characters/bodies \ client/assets/characters/clothing/jeans_modern \ [--bodies average_m,child,...] [--offset 0.012] [--hem-frac 1.0] \ [--waist-flare 0.007] [--base-rgb 0.24,0.32,0.45] [--plain] Writes per body: /.glb (skinned, albedo embedded) /_mask.png (RGBA region mask, UV0) /_base_albedo.png Plus: /base_albedo.png (average_m's, shared sidecar) /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 base offset-shell module (shared machinery) # -------------------------------------------------------------------------- _HERE = os.path.dirname(os.path.abspath(__file__)) _spec = importlib.util.spec_from_file_location( "offset_shell_base", os.path.join(_HERE, "blender_author_offset_shell.py")) base = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(base) log = base.log FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (verified in base) # -------------------------------------------------------------------------- # Parameters # -------------------------------------------------------------------------- COVERED_SEGMENTS = [ "seg_hips", "seg_leg_upper_l", "seg_leg_upper_r", "seg_leg_lower_l", "seg_leg_lower_r", ] HEM_FRAC = 1.0 # 1.0 = full leg to the ankle (jeans) WELD_DIST = 5e-4 # boundary-ring weld tolerance (0.5 mm) TEX_SIZE = 1024 # albedo + mask resolution (painted seams need >512) WAIST_FLARE_M = 0.007 # extra radial stand-off at the waistband rim # (deep-crouch waist-fold mitigation, feathered) # Vertical proportions — fractions of the garment span (waist_z - hem_z). BAND_FRAC = 0.055 # waistband height (R region) CUFF_FRAC = 0.032 # cuff band height (B region) SEAM_W_FRAC = 0.0095 # painted seam line width YOKE_DROP_FRAC = 0.055 # back yoke centre depth below the waistband YOKE_RISE_FRAC = 0.030 # yoke V rise toward the sides BPOCKET_TOP_FRAC = 0.075 # back pocket top below the waistband BPOCKET_HH_FRAC = 0.048 # back pocket half-height FPOCKET_RZ_FRAC = 0.14 # front pocket arc vertical radius # Horizontal proportions — fractions of the hip half-width (|thigh head x|). BPOCKET_CX_FRAC = 0.80 # back pocket centre BPOCKET_HW_FRAC = 0.52 # back pocket half-width FPOCKET_CX_FRAC = 1.30 # front pocket arc centre (near the outseam corner) FPOCKET_RX_FRAC = 0.75 # front pocket arc horizontal radius LOOP_X_FRACS = (0.55, 1.30) # belt-loop |x| positions (front + back pairs) LOOP_W_M = 0.016 # belt loop width (m, scaled by hip width) # Denim style (sRGB floats; saved as-is — matches the proven base pipeline). DENIM_RGB = (0.240, 0.320, 0.450) # indigo denim THREAD_RGB = (0.800, 0.620, 0.340) # contrast stitching thread BUTTON_RGB = (0.850, 0.720, 0.480) # waist button metal ALBEDO_NOISE = 0.020 # +/- woven jitter CUFF_SHADE = 0.90 # cuff band albedo darkening LOOP_SHADE = 0.80 # belt-loop albedo darkening PLAIN = False # --plain: skip thread/pocket/button paint NOISE_SEED = 2089 # Reference proportions (average_m) the fractions were calibrated against. _REF_SPAN = 1.007 # waist rim z (1.093) - ankle z (0.086) _REF_HIP_X = 0.0906 # |thigh_l head x| # -------------------------------------------------------------------------- # Per-body landmarks # -------------------------------------------------------------------------- class LegLandmarks: """Cut/mask/paint parameters derived from one body's bones + mesh.""" def __init__(self, armature, waist_z, ankle_z, crotch_z): bones = armature.data.bones thigh = bones.get("thigh_l") calf = bones.get("calf_l") if thigh is None or calf is None: raise RuntimeError("thigh_l/calf_l missing — not the 65-bone rig?") self.waist_z = waist_z self.ankle_z = ankle_z self.crotch_z = crotch_z self.hip_x = abs(thigh.head_local.x) # Leg axis control points (z-increasing) for azimuth seam placement. # x values are the +x (left) leg; the right leg mirrors via sign. self.leg_z_pts = np.array( [calf.tail_local.z, calf.head_local.z, thigh.head_local.z]) self.leg_x_pts = np.array( [abs(calf.tail_local.x), abs(calf.head_local.x), abs(thigh.head_local.x)]) self.leg_y_pts = np.array( [calf.tail_local.y, calf.head_local.y, thigh.head_local.y]) self.hem_z = ankle_z # finalized after the hem cut def finalize(self, hem_z): self.hem_z = hem_z self.span = self.waist_z - self.hem_z self.sh = self.hip_x / _REF_HIP_X self.band_h = BAND_FRAC * self.span self.cuff_h = CUFF_FRAC * self.span self.seam_w = SEAM_W_FRAC * self.span self.band_z = self.waist_z - self.band_h self.cuff_top = self.hem_z + self.cuff_h log(f"landmarks: waist={self.waist_z:.3f} hem={self.hem_z:.3f} " f"crotch={self.crotch_z:.3f} hip_x={self.hip_x:.3f} " f"band_z={self.band_z:.3f} cuff_top={self.cuff_top:.3f} " f"seam_w={self.seam_w * 1000:.1f}mm") def probe_hips_bounds(body_dir): """Import seg_hips alone to measure the crotch (its lowest ring) exactly.""" base.clear_scene() objs = base.import_glb(os.path.join(body_dir, "seg_hips.glb")) zs = [] for o in objs: if base.is_body_mesh(o): zs.extend(v.co.z for v in o.data.vertices) if not zs: raise RuntimeError("seg_hips.glb yielded no skinned mesh") return min(zs), max(zs) # -------------------------------------------------------------------------- # Geometry: weld + hem cut # -------------------------------------------------------------------------- def weld_boundaries(shell): """Merge coincident segment-boundary verts so the offset can't open cracks.""" me = shell.data bm = bmesh.new() bm.from_mesh(me) before = len(bm.verts) bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=WELD_DIST) merged = before - len(bm.verts) bm.to_mesh(me) bm.free() me.update() log(f"welded segment boundaries: {merged} verts merged " f"({before} -> {len(me.vertices)})") def flatten_open_rims(shell, ankle_plane): """Pull the open boundary rings onto clean planes (pre-offset). The body segment splitter cuts along weight thresholds, not edge loops, so the seg_hips top edge and the seg_leg_lower ankle edges are jagged rings of "teeth" (~5-6 cm on average_m). On the skin this hides under the neighbouring segment; on a garment shell it becomes a ragged silhouette. The top ring's verts are pulled DOWN to the ring's own valley (deepest notch) — a clean straight waist edge without inventing coverage. The bottom rings' verts are moved onto `ankle_plane` (the ankle joint for full-length pants, or the hem-cut plane for cropped variants) — a clean straight hem AT the ankle. Only boundary verts move; interior verts are untouched, and weights/UVs ride along, so skinning and painting are unaffected. Returns (waist_plane, ankle_plane). """ me = shell.data bm = bmesh.new() bm.from_mesh(me) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() boundary = set() for e in bm.edges: if len(e.link_faces) == 1: # open boundary boundary.update(v.index for v in e.verts) zs = [bm.verts[i].co.z for i in boundary] z_mid = (min(zs) + max(zs)) / 2.0 top = [i for i in boundary if bm.verts[i].co.z > z_mid] bottom = [i for i in boundary if bm.verts[i].co.z <= z_mid] waist_plane = min(bm.verts[i].co.z for i in top) teeth_top = max(bm.verts[i].co.z for i in top) - waist_plane teeth_bot_lo = min(bm.verts[i].co.z for i in bottom) teeth_bot_hi = max(bm.verts[i].co.z for i in bottom) for i in top: bm.verts[i].co.z = waist_plane for i in bottom: bm.verts[i].co.z = ankle_plane bm.to_mesh(me) bm.free() me.update() log(f"flattened open rims: waist ring ({len(top)} verts, teeth " f"{teeth_top:.3f} m) -> {waist_plane:.3f}; ankle rings " f"({len(bottom)} verts, z {teeth_bot_lo:.3f}..{teeth_bot_hi:.3f}) " f"-> {ankle_plane:.3f}") return waist_plane, ankle_plane def waist_flare(shell, waist_plane, band_h, flare): """Extra RADIAL stand-off at the waistband rim, feathered over 2x the band height (pre-solidify). QA evidence: in deep crouch the lower-back skin folds over the waist rim (the peasant set's known 'deep-crouch waist gap', worst on small bodies). Flaring the band outward gives the fold room and reads as a natural jeans waistband stand-off.""" if flare <= 0.0: return z0 = waist_plane - 2.0 * band_h bm = bmesh.new() bm.from_mesh(shell.data) bm.normal_update() n = 0 for v in bm.verts: if v.co.z > z0: t = min((v.co.z - z0) / (2.0 * band_h), 1.0) nx, ny = v.normal.x, v.normal.y mag = (nx * nx + ny * ny) ** 0.5 if mag > 1e-6: v.co.x += flare * t * nx / mag v.co.y += flare * t * ny / mag n += 1 bm.to_mesh(shell.data) bm.free() shell.data.update() log(f"waist flare: {n} verts, +{flare * 1000:.1f} mm radial at rim " f"(feathered from z={z0:.3f})") def clamp_waist_residue(shell, waist_plane): """Post-solidify safety clamp: any interior tooth verts still above the waist plane (multi-triangle teeth) get squashed onto it.""" me = shell.data n = 0 for v in me.vertices: if v.co.z > waist_plane: v.co.z = waist_plane n += 1 me.update() if n: log(f"clamped {n} residual waist verts -> {waist_plane:.3f}") def hem_cut(shell, lm, hem_frac): """Trim the legs below the hem plane. 1.0 keeps the full ankle length.""" if hem_frac >= 0.999: return lm.ankle_z hip_z = float(lm.leg_z_pts[-1]) hem_z = lm.ankle_z + (1.0 - hem_frac) * (hip_z - lm.ankle_z) bm = bmesh.new() bm.from_mesh(shell.data) doomed = [v for v in bm.verts if v.co.z < hem_z] bmesh.ops.delete(bm, geom=doomed, context='VERTS') bm.to_mesh(shell.data) bm.free() shell.data.update() log(f"hem cut at z={hem_z:.3f}: removed {len(doomed)} verts") return hem_z # -------------------------------------------------------------------------- # Denim feature field (texel-level; drives albedo AND mask together) # -------------------------------------------------------------------------- def _seam_field(px, py, pz, lm): """Evaluate denim features at texel 3D positions (numpy arrays). Returns bool arrays (seams, thread, in_cuff, in_band, front): `seams` feeds the mask B channel; `thread` is every painted stitch line. """ w2 = lm.seam_w * 0.5 front = py * FRONT_Y_SIGN > 0.004 backside = 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. 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 # Outseam: azimuth toward the outer (+/-x) direction; constant metric # width via arc distance. Inseam: inner direction, below the crotch only. arc_out = np.arccos(np.clip(dx * side / r, -1.0, 1.0)) * r outseam = mid & (arc_out < w2) arc_in = np.arccos(np.clip(-dx * side / r, -1.0, 1.0)) * r inseam = mid & (arc_in < w2) & (pz < lm.crotch_z - 0.01 * lm.span / _REF_SPAN) # Centre-front fly stitch (slightly off-centre, classic J-front). fly = front & mid & (np.abs(px - 0.012 * lm.sh) < w2) \ & (pz > lm.crotch_z + 0.015 * lm.span / _REF_SPAN) # Back yoke: shallow V, higher toward the sides. yoke_z = (lm.band_z - YOKE_DROP_FRAC * lm.span + YOKE_RISE_FRAC * lm.span * np.minimum(np.abs(px) / (1.5 * lm.hip_x), 1.0)) yoke = backside & mid & (np.abs(pz - yoke_z) < w2) \ & (np.abs(px) < 1.6 * lm.hip_x) seams = outseam | inseam | fly | yoke # Stitch-only lines (albedo, not mask): waist + cuff border stitching. wstitch = np.abs(pz - lm.band_z) < w2 cstitch = np.abs(pz - lm.cuff_top) < w2 # Back pocket outlines (rectangle rings on the seat). bhw = BPOCKET_HW_FRAC * lm.hip_x bhh = BPOCKET_HH_FRAC * lm.span bcz = lm.band_z - BPOCKET_TOP_FRAC * lm.span - bhh adx = np.abs(np.abs(px) - BPOCKET_CX_FRAC * lm.hip_x) adz = np.abs(pz - bcz) bpocket = backside & (adx < bhw + w2) & (adz < bhh + w2) \ & ~((adx < bhw - w2) & (adz < bhh - w2)) # Front pocket arcs (quarter-ellipse from waistband toward the outseam). fcx = FPOCKET_CX_FRAC * lm.hip_x frx = FPOCKET_RX_FRAC * lm.hip_x frz = FPOCKET_RZ_FRAC * lm.span ex = (np.abs(px) - fcx) / frx ez = (pz - lm.band_z) / frz fdist = (np.sqrt(ex * ex + ez * ez) - 1.0) * (0.5 * (frx + frz)) fpocket = front & (np.abs(fdist) < w2) & (pz <= lm.band_z) \ & (np.abs(px) <= fcx) thread = seams | wstitch | cstitch | bpocket | fpocket return seams, thread, in_cuff, in_band, front def _paint_texels(px, py, pz, noise, lm): """Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions.""" n = px.shape[0] seams, thread, in_cuff, in_band, front = _seam_field(px, py, pz, lm) # --- albedo ------------------------------------------------------------ alb = np.empty((n, 4), dtype=np.float32) for c in range(3): alb[:, c] = DENIM_RGB[c] + noise alb[:, 3] = 1.0 alb[in_cuff, :3] *= CUFF_SHADE if not PLAIN: # Belt loops: darkened denim bands on the waistband. loop_w = LOOP_W_M * lm.sh loops = np.zeros(n, dtype=bool) for fx in LOOP_X_FRACS: loops |= np.abs(np.abs(px) - fx * lm.hip_x) < loop_w * 0.5 loops |= (~front) & (np.abs(px) < loop_w * 0.5) # centre-back loop loops &= in_band alb[loops, :3] *= LOOP_SHADE alb[thread, 0] = THREAD_RGB[0] alb[thread, 1] = THREAD_RGB[1] alb[thread, 2] = THREAD_RGB[2] # Waist button (front, mid-band). btn = front & (np.hypot(px, pz - (lm.band_z + 0.5 * lm.band_h)) < 0.009 * lm.sh) alb[btn, 0] = BUTTON_RGB[0] alb[btn, 1] = BUTTON_RGB[1] alb[btn, 2] = BUTTON_RGB[2] # --- region mask: waistband R / legs G / seams+cuffs B ------------------ mask = np.zeros((n, 4), dtype=np.float32) is_b = (in_cuff | seams) & ~in_band is_g = ~(in_band | is_b) mask[in_band, 0] = 1.0 mask[is_b, 2] = 1.0 mask[is_g, 1] = 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 denim field, write albedo + mask together.""" 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] = DENIM_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"denim_albedo_{body}", albedo_path) _save(mask_buf, f"denim_mask_{body}", mask_path) log(f"saved albedo -> {albedo_path}") log(f"saved mask -> {mask_path}") return albedo_img # -------------------------------------------------------------------------- # Parked logo UV2 (pants are not logo-capable; the shader still samples UV2) # -------------------------------------------------------------------------- def author_parked_uv2(shell): me = shell.data while len(me.uv_layers) > 1: me.uv_layers.remove(me.uv_layers[-1]) me.uv_layers.new(name="logo_uv") me.uv_layers.active = me.uv_layers[0] bm = bmesh.new() bm.from_mesh(me) uvl = bm.loops.layers.uv.get("logo_uv") for face in bm.faces: for loop in face.loops: loop[uvl].uv = (2.0, 2.0) bm.to_mesh(me) bm.free() me.update() log("logo UV2 authored fully parked (not logo-capable)") # -------------------------------------------------------------------------- # Per-body authoring # -------------------------------------------------------------------------- def author_denim_shell(body_dir, out_dir, body, offset, hem_frac): crotch_z, _hips_top = probe_hips_bounds(body_dir) base.clear_scene() base.COVERED_SEGMENTS = COVERED_SEGMENTS shell, armature = base.build_covered_mesh(body_dir) weld_boundaries(shell) zs = [v.co.z for v in shell.data.vertices] waist_z, ankle_z = max(zs), min(zs) lm = LegLandmarks(armature, waist_z, ankle_z, crotch_z + 0.005) hem_z = hem_cut(shell, lm, hem_frac) # Full-length pants hem AT the ankle joint (calf tail); cropped variants # hem at the cut plane. ankle_plane = float(lm.leg_z_pts[0]) if hem_frac >= 0.999 else hem_z waist_plane, ankle_plane = flatten_open_rims(shell, ankle_plane) base.offset_outward(shell, offset) waist_flare(shell, waist_plane, BAND_FRAC * (waist_plane - ankle_plane), WAIST_FLARE_M) base.solidify(shell, base.CLOTH_THICKNESS_M) clamp_waist_residue(shell, waist_plane) # Landmarks reference the CLEAN rims (band under the flattened waist edge, # cuff above the flattened hem). lm.waist_z = waist_plane lm.finalize(ankle_plane) 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 HEM_FRAC, DENIM_RGB, PLAIN, WAIST_FLARE_M argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] if len(argv) < 2: print("Usage: -- [--bodies a,b,c] " "[--offset M] [--hem-frac F] [--waist-flare M] " "[--base-rgb r,g,b] [--plain]") sys.exit(1) bodies_root = argv[0] out_dir = argv[1] offset = base.PER_BODY_OFFSET_M if "--offset" in argv: offset = float(argv[argv.index("--offset") + 1]) if "--hem-frac" in argv: HEM_FRAC = float(argv[argv.index("--hem-frac") + 1]) if "--waist-flare" in argv: WAIST_FLARE_M = float(argv[argv.index("--waist-flare") + 1]) if "--base-rgb" in argv: DENIM_RGB = tuple( float(v) for v in argv[argv.index("--base-rgb") + 1].split(",")) 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"denim per-body mode: {len(bodies)} bodies, offset " f"{offset * 1000:.0f} mm, hem-frac {HEM_FRAC}, 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_denim_shell(body_dir, out_dir, body, offset, HEM_FRAC) 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()