""" blender_author_boots.py (T-1089 wave 2, boots_modern + ankle-boot family) Authors ANKLE BOOTS as per-body offset shells: both feet PLUS the lower calf (seg_leg_lower cut to a boot shaft just above the ankle), with a chunky sole extension and painted eyelets/laces up the shaft. Reuses blender_author_offset_shell.py (via blender_author_denim_pants.py, imported as a module) for the shared machinery — scene build, join, offset, solidify, albedo material, GLB export — plus the two denim REQUIRED PRACTICES for bottoms/footwear: * boundary WELD of coincident segment-seam rings (denim.weld_boundaries): the ankle joins (foot<->leg_lower) duplicate a coincident vert band per segment; offsetting un-welded rings along diverging normals opens cracks. * open-rim FLATTENING: the shaft cut leaves a jagged "teeth" ring on each calf; both rims are pulled down onto one clean shared plane (the deeper valley of the two, so the boots match) before offsetting. What this companion adds, as reusable parameters (not hacks): * --shaft-frac: boot shaft height as a fraction of the ankle->knee calf span (0.35 cut => rim lands on the lower calf after rim flattening; larger values give combat/riding variants). * TOE-BOX MERGE: the skin mesh has individual toes; a boot must not. The foot region is Laplacian-smoothed pre-offset (mild on the whole foot, aggressive forward of the ball joint) so the toes fuse into one rounded leather toe box and anatomical detail (ankle knobs, heel tendon) reads as boot, not foot. Weights/UVs ride along untouched. * SKIN CONTAINMENT CLAMP: smoothing can pull the shell inside the skin it no longer follows (melted toe box vs real toes). A BVH of the ORIGINAL welded skin is kept, and after the offset every foot-region vert whose signed distance to the skin is below a minimum clearance is pushed back out along the skin normal — standoff guaranteed by construction, which is what the chromakey QA gates on (the runtime does not hide segments). * FOOT WEIGHT RE-BIND: smoothing + clamping RELOCATE shell verts but leave them carrying their ORIGIN vertex's bone weights, so material that ends up hovering over toe N flexes with the bone of toe M — under ball-joint flexion (Walk push-off, crouch) the shell diverges from the skin beneath it and the toes poke through (QA/preview evidence, wave 2 run 1/2). After shaping, every foot-region vert re-copies its vertex-group weights from the nearest vert of the original welded skin, so the boot flexes exactly with the anatomy each patch of leather actually covers. * SOLE geometry as an offset-shell param extension (--sole-drop): the under-foot surface created by the outward offset is flattened onto a slab plane sole_drop below the skin sole (solidify adds its thickness back, so the shipped slab bottom sits exactly sole_drop under the skin), and a feathered radial LIP (~5 mm) bulges the sole band outward for the chunky work-boot silhouette. * --shaft-flare: extra feathered radial stand-off toward the shaft rim. Serves the same crouch-fold purpose as the denim waist flare AND buys clearance so full-length pant hems (jeans/formal, 12 mm standoff) tuck INSIDE the boot shaft instead of z-fighting with it. * TEXEL-level feature painting (denim technique, boot field): one analytic field evaluation drives BOTH the painted albedo and the region mask so they always agree — lace bars + eyelet dots up the front of the shaft, welt stitching above the sole, padded collar shading at the rim. Regions (spec): sole -> R, upper + shaft -> G, laces + eyelets -> B. * a parked logo_uv TEXCOORD_1 layer (boots are not logo-capable, but toon_garment.gdshader samples UV2 unconditionally). All cut/mask/paint parameters derive PER BODY from that body's own calf bone landmarks and measured skin-sole plane, scaled by the calf-span ratio against average_m (clamped so the child keeps believable, not clown, proportions) — the same proportional-ratio philosophy as base.derive_thresholds. Per-body mode only (offset shells author per body, Q-060). Style pin (T-1089): modern only; identity carried by the texture. The albedo is painted at mid luma so toon_garment.gdshader's luma-preserving recolor stays faithful; the brown/black work-street default lives in the manifest default_tints (sole near-black, upper work brown, laces dark). Usage (boots_modern reference invocation): tooling/blender --background --python \ tooling/garment-fit/blender_author_boots.py -- \ client/assets/characters/bodies \ client/assets/characters/clothing/boots_modern \ [--bodies average_m,child,...] [--offset 0.013] [--shaft-frac 0.30] \ [--shaft-flare 0.008] [--sole-drop 0.020] [--base-rgb r,g,b] [--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 denim companion as a library (which itself imports the base # offset-shell module). Boot reuse: base machinery + denim's weld/parked-UV2. # -------------------------------------------------------------------------- _HERE = os.path.dirname(os.path.abspath(__file__)) _spec = importlib.util.spec_from_file_location( "denim_pants_lib", os.path.join(_HERE, "blender_author_denim_pants.py")) denim = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(denim) base = denim.base log = base.log FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (verified in base) # -------------------------------------------------------------------------- # Parameters # -------------------------------------------------------------------------- COVERED_SEGMENTS = [ "seg_foot_l", "seg_foot_r", "seg_leg_lower_l", "seg_leg_lower_r", ] SHAFT_FRAC = 0.35 # boot shaft = this fraction of the ankle->knee span BOOT_OFFSET_M = 0.013 # standoff along normals (leather sits off the skin) SHAFT_FLARE_M = 0.008 # extra radial stand-off at the shaft rim (feathered) SOLE_DROP_M = 0.020 # chunky sole slab depth below the skin sole (spec) SOLE_LIP_M = 0.008 # radial sole bulge (chunky work-boot lip) SOLE_LIP_TOP_M = 0.020 # lip feather reaches this far above the skin sole TEX_SIZE = 1024 # albedo + mask resolution (painted laces need >512) NOISE_SEED = 3089 # Convex toe box (shared base.convex_toe_box) — chunky work-boot cap. TOE_EXT_M = 0.010 # nose extension past the longest toe (m) TOE_WMARGIN_M = 0.006 # half-width padding (chunky) TOE_HCLEAR_M = 0.009 # vertical headroom above the toes (flex room) TOE_FEATHER_M = 0.020 # blend band behind the ball # Painted-detail metrics (metres on average_m; scaled by the calf-span ratio). LACE_PITCH_M = 0.017 # vertical distance between lace bars LACE_BAR_HW_M = 0.0026 # half-height of a painted lace bar LACE_PANEL_HW_M = 0.015 # half arc-width of the lace panel EYELET_R_M = 0.0032 # eyelet dot radius (at the panel edges) LACE_LO_FRAC = 0.0 # panel starts at the ankle joint (shaft only) COLLAR_H_M = 0.014 # padded collar band at the shaft rim (albedo shade) SOLE_TOP_M = 0.014 # R region reaches this far above the skin sole WELT_OFF_M = 0.0035 # welt stitch line offset above the sole top SEAM_HW_M = 0.0018 # painted stitch line half-width TOECAP_STITCH = True # painted toe-cap stitch line at the ball joint # Albedo tones (sRGB floats). Mid-luma leather so the toon_garment luma # recolor keeps its dynamic range; hue defaults come from manifest tints. LEATHER_RGB = (0.58, 0.44, 0.32) # light tan work leather SOLE_RGB = (0.30, 0.29, 0.28) # darker rubber (low luma -> reads black) LACE_RGB = (0.24, 0.21, 0.19) # dark laces EYELET_RGB = (0.85, 0.78, 0.60) # bright metal eyelet glint WELT_RGB = (0.78, 0.62, 0.40) # welt stitching thread PANEL_SHADE = 0.90 # lace-panel background darkening COLLAR_SHADE = 0.86 # padded collar darkening ALBEDO_NOISE = 0.020 # +/- grain jitter PLAIN = False # --plain: skip laces/eyelets/welt paint # Reference proportions (average_m) the scale ratio is anchored to. _REF_CALF_SPAN = 0.4559 # calf head z (0.5424) - calf tail z (0.0865) # -------------------------------------------------------------------------- # Per-body landmarks # -------------------------------------------------------------------------- class BootLandmarks: """Cut/mask/paint parameters from one body's calf bones + skin-sole plane.""" def __init__(self, armature, skin_min_z): bones = armature.data.bones calf_l = bones.get("calf_l") calf_r = bones.get("calf_r") if calf_l is None or calf_r is None: raise RuntimeError("calf_l/calf_r missing — not the 65-bone rig?") self.ankle_z = (calf_l.tail_local.z + calf_r.tail_local.z) / 2.0 self.knee_z = (calf_l.head_local.z + calf_r.head_local.z) / 2.0 self.skin_min_z = skin_min_z self.calf_span = self.knee_z - self.ankle_z # Detail scale: proportional to the calf span, clamped so small bodies # keep believable (not clown, not doll) sole/lace metrics. self.s = min(max(self.calf_span / _REF_CALF_SPAN, 0.55), 1.15) # Per-z leg axis control points (ankle -> knee) for the +x leg; # the right leg mirrors via the x sign. np.interp clamps outside. self.leg_z_pts = np.array([calf_l.tail_local.z, calf_l.head_local.z]) self.leg_x_pts = np.array( [abs(calf_l.tail_local.x), abs(calf_l.head_local.x)]) self.leg_y_pts = np.array([calf_l.tail_local.y, calf_l.head_local.y]) self.shaft_top_z = self.ankle_z + SHAFT_FRAC * self.calf_span self.rim_z = self.shaft_top_z # finalized after rim flattening # Ball joint (toe-box hinge): forward distance on the front axis. ball = bones.get("ball_l") self.ball_front = (ball.head_local.y * FRONT_Y_SIGN if ball is not None else None) def finalize(self, rim_z): self.rim_z = rim_z self.sole_top_z = self.skin_min_z + SOLE_TOP_M * self.s self.lace_lo = self.ankle_z - LACE_LO_FRAC * (self.ankle_z - self.skin_min_z) self.lace_hi = self.rim_z - 0.7 * COLLAR_H_M * self.s log(f"landmarks: ankle={self.ankle_z:.3f} knee={self.knee_z:.3f} " f"sole={self.skin_min_z:.3f} rim={self.rim_z:.3f} s={self.s:.2f} " f"sole_top={self.sole_top_z:.3f} " f"laces z=[{self.lace_lo:.3f},{self.lace_hi:.3f}]") # -------------------------------------------------------------------------- # Geometry: shaft cut + rim flatten + flare + sole # -------------------------------------------------------------------------- def shaft_cut(shell, lm): """Trim the calves above the boot-shaft plane (bone-derived).""" bm = bmesh.new() bm.from_mesh(shell.data) doomed = [v for v in bm.verts if v.co.z > lm.shaft_top_z] bmesh.ops.delete(bm, geom=doomed, context='VERTS') bm.to_mesh(shell.data) bm.free() shell.data.update() log(f"shaft cut at z={lm.shaft_top_z:.3f}: removed {len(doomed)} verts") def flatten_shaft_rims(shell, lm): """Pull both shaft-rim teeth rings onto ONE shared clean plane (pre-offset). The plane cut follows mesh topology, so each rim is a jagged ring of teeth. Both rings flatten DOWN to the common valley (deepest notch of either ring) — a clean straight rim, identical height on both boots, no invented coverage. Any stray open-boundary verts below the shaft (weld leftovers) are left alone and reported. Returns the rim plane z. """ 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: boundary.update(v.index for v in e.verts) low_thresh = lm.ankle_z + 0.25 * (lm.shaft_top_z - lm.ankle_z) rims = [i for i in boundary if bm.verts[i].co.z > low_thresh] stray = len(boundary) - len(rims) if not rims: bm.free() raise RuntimeError("no shaft rim boundary verts found") valley = min(bm.verts[i].co.z for i in rims) teeth = max(bm.verts[i].co.z for i in rims) - valley for i in rims: bm.verts[i].co.z = valley bm.to_mesh(me) bm.free() me.update() log(f"flattened shaft rims: {len(rims)} verts, teeth {teeth:.3f} m " f"-> z={valley:.3f}" + (f" (WARNING: {stray} stray low boundary verts left)" if stray else "")) return valley def shaft_flare(shell, lm, rim_z, flare): """Extra RADIAL stand-off toward the shaft rim, feathered from the ankle (post-offset, pre-solidify). Buys pant-hem clearance + chunky read.""" if flare <= 0.0: return span = max(rim_z - lm.ankle_z, 1e-6) bm = bmesh.new() bm.from_mesh(shell.data) bm.normal_update() n = 0 for v in bm.verts: if v.co.z > lm.ankle_z: t = min((v.co.z - lm.ankle_z) / span, 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"shaft flare: {n} verts, +{flare * 1000:.1f} mm radial at rim") def sole_shape(shell, lm, offset, sole_drop): """Chunky sole (post-offset, pre-solidify): feathered radial LIP around the sole band, then the under-foot offset surface flattened onto a slab plane. Solidify(use_rim) then grows its thickness outward (downward there), landing the shipped slab bottom exactly sole_drop below the skin sole.""" lip_top = lm.skin_min_z + SOLE_LIP_TOP_M * lm.s lip = SOLE_LIP_M * lm.s slab_z = lm.skin_min_z - (sole_drop * lm.s - base.CLOTH_THICKNESS_M) feather = max(lip_top - lm.skin_min_z, 1e-6) bm = bmesh.new() bm.from_mesh(shell.data) bm.normal_update() n_lip = 0 for v in bm.verts: if v.co.z < lip_top: t = min((lip_top - v.co.z) / feather, 1.0) nx, ny = v.normal.x, v.normal.y mag = (nx * nx + ny * ny) ** 0.5 if mag > 1e-6: v.co.x += lip * t * nx / mag v.co.y += lip * t * ny / mag n_lip += 1 n_flat = 0 for v in bm.verts: if v.co.z < lm.skin_min_z: v.co.z = slab_z n_flat += 1 bm.to_mesh(shell.data) bm.free() shell.data.update() log(f"sole: lip +{lip * 1000:.1f} mm on {n_lip} verts, " f"{n_flat} under-sole verts flattened -> z={slab_z:.3f} " f"(shipped bottom {sole_drop * lm.s * 1000:.0f} mm below skin sole)") def clamp_rim_residue(shell, rim_z): """Post-solidify safety clamp: verts still above the rim plane (solidify displacement on multi-triangle teeth) get squashed onto it.""" me = shell.data n = 0 for v in me.vertices: if v.co.z > rim_z: v.co.z = rim_z n += 1 me.update() if n: log(f"clamped {n} residual rim verts -> {rim_z:.3f}") # -------------------------------------------------------------------------- # Boot feature field (texel-level; drives albedo AND mask together) # -------------------------------------------------------------------------- def _boot_field(px, py, pz, lm): """Evaluate boot features at texel 3D positions (numpy arrays).""" s = lm.s 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 # Arc distance from the front meridian around the per-z leg axis — # constant metric width at any girth, mirrors correctly on both boots. arc_front = np.arccos(np.clip(dy * FRONT_Y_SIGN / r, -1.0, 1.0)) * r in_sole = pz <= lm.sole_top_z in_lace_z = (pz > lm.lace_lo) & (pz < lm.lace_hi) & ~in_sole panel = in_lace_z & (arc_front < LACE_PANEL_HW_M * s) pitch = LACE_PITCH_M * s ph = np.mod(pz - lm.lace_lo, pitch) laces = panel & (np.abs(ph - 0.5 * pitch) < LACE_BAR_HW_M * s) eyelets = ( in_lace_z & (np.abs(arc_front - LACE_PANEL_HW_M * s) < EYELET_R_M * s) & (np.abs(ph - 0.5 * pitch) < EYELET_R_M * s) ) welt = (~in_sole) & ( np.abs(pz - (lm.sole_top_z + WELT_OFF_M * s)) < SEAM_HW_M * s) if TOECAP_STITCH and lm.ball_front is not None: welt = welt | ( (~in_sole) & (pz < lm.lace_lo) & (np.abs(py * FRONT_Y_SIGN - lm.ball_front) < SEAM_HW_M * s)) collar = (~in_sole) & (pz > lm.rim_z - COLLAR_H_M * s) return in_sole, panel, laces, eyelets, welt, collar def _paint_texels(px, py, pz, noise, lm): """Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions.""" n = px.shape[0] in_sole, panel, laces, eyelets, welt, collar = _boot_field(px, py, pz, lm) # --- albedo ------------------------------------------------------------ alb = np.empty((n, 4), dtype=np.float32) for c in range(3): alb[:, c] = LEATHER_RGB[c] + noise alb[:, 3] = 1.0 alb[collar, :3] *= COLLAR_SHADE if not PLAIN: alb[panel, :3] *= PANEL_SHADE for c in range(3): alb[welt, c] = WELT_RGB[c] for c in range(3): alb[in_sole, c] = SOLE_RGB[c] + noise[in_sole] if not PLAIN: for c in range(3): alb[laces, c] = LACE_RGB[c] alb[eyelets, c] = EYELET_RGB[c] # --- region mask: sole R / upper+shaft G / laces+eyelets B --------------- mask = np.zeros((n, 4), dtype=np.float32) is_b = (laces | eyelets) & ~in_sole if not PLAIN else np.zeros(n, dtype=bool) is_g = ~(in_sole | is_b) mask[in_sole, 0] = 1.0 mask[is_b, 2] = 1.0 mask[is_g, 1] = 1.0 return alb, mask # -------------------------------------------------------------------------- # UV0 rasterization (denim technique: one pass paints albedo + 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 boot 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] = LEATHER_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 = upper 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"boot_albedo_{body}", albedo_path) _save(mask_buf, f"boot_mask_{body}", mask_path) log(f"saved albedo -> {albedo_path}") log(f"saved mask -> {mask_path}") return albedo_img # -------------------------------------------------------------------------- # Per-body authoring # -------------------------------------------------------------------------- def author_boot_shell(body_dir, out_dir, body, offset, sole_drop): base.clear_scene() base.COVERED_SEGMENTS = COVERED_SEGMENTS shell, armature = base.build_covered_mesh(body_dir) denim.weld_boundaries(shell) # ankle seams: foot <-> leg_lower skin_min_z = min(v.co.z for v in shell.data.vertices) lm = BootLandmarks(armature, skin_min_z) shaft_cut(shell, lm) rim_z = flatten_shaft_rims(shell, lm) # Smooth convex toe box (replaces the toe-merge + skin-conforming # containment clamp + per-toe weight re-bind, which re-imprinted the # individual toes and rippled under flex). The cap encloses the real skin # toes and rebinds uniformly to the ball bone; offset then adds standoff. base.convex_toe_box( shell, armature, extension=TOE_EXT_M * lm.s, width_margin=TOE_WMARGIN_M * lm.s, height_clear=TOE_HCLEAR_M * lm.s, feather_m=TOE_FEATHER_M * lm.s) base.offset_outward(shell, offset) shaft_flare(shell, lm, rim_z, SHAFT_FLARE_M * lm.s) sole_shape(shell, lm, offset, sole_drop) base.solidify(shell, base.CLOTH_THICKNESS_M) clamp_rim_residue(shell, rim_z) lm.finalize(rim_z) denim.author_parked_uv2(shell) # boots are not logo-capable 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 SHAFT_FRAC, SHAFT_FLARE_M, LEATHER_RGB, PLAIN argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] if len(argv) < 2: print("Usage: -- [--bodies a,b,c] " "[--offset M] [--shaft-frac F] [--shaft-flare M] " "[--sole-drop M] [--base-rgb r,g,b] [--plain]") sys.exit(1) bodies_root = argv[0] out_dir = argv[1] offset = BOOT_OFFSET_M sole_drop = SOLE_DROP_M if "--offset" in argv: offset = float(argv[argv.index("--offset") + 1]) if "--shaft-frac" in argv: SHAFT_FRAC = float(argv[argv.index("--shaft-frac") + 1]) if "--shaft-flare" in argv: SHAFT_FLARE_M = float(argv[argv.index("--shaft-flare") + 1]) if "--sole-drop" in argv: sole_drop = float(argv[argv.index("--sole-drop") + 1]) if "--base-rgb" in argv: LEATHER_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"boots per-body mode: {len(bodies)} bodies, offset " f"{offset * 1000:.0f} mm, shaft-frac {SHAFT_FRAC}, " f"sole-drop {sole_drop * 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_boot_shell(body_dir, out_dir, body, offset, sole_drop) 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()