""" blender_author_shoes_formal.py (T-1089 wave 2, footwear: shoes_formal) Authors FORMAL SHOES (both feet, one garment — the peasant_shoes convention) as per-body offset shells, reusing blender_author_offset_shell.py as a library (scene build, join, offset, solidify, GLB export) and the denim companion's required bottoms/footwear practices (boundary WELD, parked logo UV2). What footwear adds, as reusable parameters (sneakers/boots can re-drive this): * both-feet build: seg_foot_l + seg_foot_r joined into ONE skinned mesh; the two shells stay disjoint (weights by construction, one draw call). * boundary WELD is load-bearing here, not just hygienic: a raw seg_foot is 3-8 mesh islands (upper + a separate flat SOLE PLATE sheet + ankle-band slivers). remove_doubles(0.5 mm) zips them into one shell per foot whose only open boundary is the ankle ring (verified on average_m + child). * ankle TOPLINE cut: the welded ankle ring is jagged weight-threshold teeth (~5 cm on average_m). Verts above the ring's own valley (optionally +--topline-lift) are deleted and the fresh boundary is flattened UP onto the topline plane — a clean horizontal low-profile shoe opening. Guard rails clamp the topline to [0.70, 0.95] x the foot-bone head height. The rim ring is then RELAXED in XY (neighbour averaging, z pinned) so the throat reads as a smooth loafer opening instead of a jagged U. * TOE-BOX smoothing: the skin mesh has individual toe bumps; offsetting them verbatim yields a five-finger foot-glove. Forefoot verts are Laplacian-smoothed in feathered bands (mild at the metatarsal, strong at the toes) into one smooth formal toe box, then given a small extra standoff (--toe-extra) to buy back the clearance smoothing costs over the toe bumps. Band reps are deliberately MODERATE: smoothing migrates shell verts away from the skin they are weighted like, and under toe flex (Walk heel-strike / Sprint push-off) the migration error scales with sin(flex angle) — first authoring pass used (2,4,8) reps and the animated toes visibly overtook the toe box. * CLEARANCE ENFORCEMENT (the fix that guards all of the above): a BVH snapshot of the post-cut, PRE-smoothing skin surface; after offset + toe-extra, every shell vert closer than the shell offset to the skin (signed, along the skin normal) is pushed out to exactly that standoff. Guaranteed rest-pose clearance by construction, no matter how far the toe smoothing migrated verts. * SOLE as an offset-shell param extension: pre-offset, downward-facing verts (normal.z < -0.5) are recorded; post-offset they are flattened onto a plane so the finished exterior bottom sits --sole-mm (default 8 mm) below the skin's lowest point once Solidify adds cloth thickness. A final clamp keeps every vert on/above the plane — thin flat sole. * UV0 NORMALIZATION: feet occupy a tiny corner of the body texture atlas; since BOTH the painted albedo and the region mask are authored here on UV0 (skin textures are dropped), the used UV bbox is rescaled to fill [0,1] — ~10x texel density for the painted seams at no cost. Mirrored L/R UV islands may overlap; all painted features are x-symmetric, so overlap is harmless by construction. * texel-level feature painting (denim's albedo+mask-from-one-field idea): - albedo: painted TOE CAP LINE across the vamp, sole-edge welt stitch, topline edge stitch, heel counter seam — flat toon-friendly, identity carried by luminance (the toon_garment shader tints by luma, so lines survive a black default tint). - mask: sole -> R, upper -> G, toe cap -> B (spec regions). * parked logo UV2 (shoes are not logo-capable; shader samples UV2 anyway). All parameters derive PER BODY from that body's own bone landmarks (foot_l / ball_l) and measured mesh extents (toe/heel y, skin bottom z, per-foot ankle ring valley), scaled by foot length against the average_m reference — the same proportional-ratio philosophy as base.derive_thresholds. Per-body mode only (offset shells author per body, Q-060). Usage (shoes_formal reference invocation): tooling/blender --background --python \ tooling/scripts/blender/blender_author_shoes_formal.py -- \ client/assets/characters/bodies \ client/assets/characters/clothing/shoes_formal \ [--bodies average_m,child,...] [--offset 0.006] [--sole-mm 0.008] \ [--topline-lift 0.0] [--cap-frac 0.35] [--base-rgb r,g,b] \ [--sole-rgb r,g,b] [--plain] [--no-uv-normalize] 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) Coverage note (the peasant_shoes convention): footwear ships hides: [] — the foot skin stays VISIBLE at runtime because the shoe throat legitimately shows the instep (hiding the feet would open a see-through hole there). The chromakey QA still gates: its config passes an explicit "covers": ["foot_l","foot_r"] so the feet are keyed even though coverage.json hides nothing. 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 + the denim companion (shared utilities) # -------------------------------------------------------------------------- _HERE = os.path.dirname(os.path.abspath(__file__)) def _load(mod_name, fname): spec = importlib.util.spec_from_file_location( mod_name, os.path.join(_HERE, fname)) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod base = _load("offset_shell_base", "blender_author_offset_shell.py") denim = _load("denim_pants_lib", "blender_author_denim_pants.py") log = base.log FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y; toes point -Y # -------------------------------------------------------------------------- # Parameters (all reusable across the footwear family) # -------------------------------------------------------------------------- COVERED_SEGMENTS = ["seg_foot_l", "seg_foot_r"] SHELL_OFFSET_M = 0.006 # slim low-profile standoff (weights exact per body) SOLE_TOTAL_M = 0.008 # finished exterior sole drop below the skin bottom TOPLINE_LIFT_M = 0.0 # extra height above the ankle-ring valley TOPLINE_FLOOR_FRAC = 0.70 # topline >= this frac of foot-bone head z TOPLINE_CEIL_FRAC = 0.95 # topline <= this frac of foot-bone head z CAP_BALL_FRAC = 0.35 # toe-cap line along the ball bone (head -> tail) SOLE_BAND_M = 0.010 # sole side band height on average_m (R region) SEAM_W_M = 0.0030 # painted seam width on average_m SEAM_W_MIN_M = 0.0016 # floor so child seams don't alias away HEEL_SEAM_FRAC = 0.16 # heel counter seam, fraction of foot len from heel MIN_ISLAND_VERTS = 10 # post-cut sliver cleanup threshold RIM_RELAX_PASSES = 2 # XY neighbour-average passes on the topline ring # Convex toe box (shared base.convex_toe_box) — sleek, low, tapered but SMOOTH. TOE_EXT_M = 0.008 # nose extension past the longest toe (m) TOE_WMARGIN_M = 0.0025 # half-width padding (snug formal last) TOE_HCLEAR_M = 0.004 # vertical headroom above the toes (low profile) TOE_FEATHER_M = 0.018 # blend band behind the ball TEX_SIZE = 1024 UV_NORMALIZE = True # rescale used UV bbox to fill [0,1] PLAIN = False # --plain: skip painted stitch lines # Formal leather style (luma carries the detail; runtime tints recolor). LEATHER_RGB = (0.320, 0.315, 0.330) # upper mid-grey leather SOLE_RGB = (0.235, 0.230, 0.240) # sole band slightly darker STITCH_RGB = (0.560, 0.550, 0.570) # painted seam thread (lighter luma) ALBEDO_NOISE = 0.015 NOISE_SEED = 3089 # Reference proportions (average_m) the fractions were calibrated against. _REF_FOOT_LEN = 0.2704 # heel y (+0.1374) - toe y (-0.1330) # -------------------------------------------------------------------------- # Per-body landmarks # -------------------------------------------------------------------------- class FootLandmarks: """Cut/mask/paint parameters derived from one body's bones + mesh.""" def __init__(self, armature, mesh): bones = armature.data.bones foot = bones.get("foot_l") ball = bones.get("ball_l") if foot is None or ball is None: raise RuntimeError("foot_l/ball_l missing — not the 65-bone rig?") self.ankle_z = foot.head_local.z ys = [v.co.y for v in mesh.vertices] zs = [v.co.z for v in mesh.vertices] self.toe_y = min(ys) # toes point -Y (FRONT_Y_SIGN) self.heel_y = max(ys) self.skin_min_z = min(zs) self.foot_len = self.heel_y - self.toe_y self.s = self.foot_len / _REF_FOOT_LEN # Toe-cap line sits along the ball bone (metatarsal -> toes). self.ball_head_y = ball.head_local.y self.ball_tail_y = ball.tail_local.y self.cap_y = self.ball_head_y + CAP_BALL_FRAC * ( self.ball_tail_y - self.ball_head_y) self.heel_seam_y = self.heel_y - HEEL_SEAM_FRAC * self.foot_len # Geometry planes: Solidify adds CLOTH_THICKNESS_M outward (down at # the sole), so the pre-solidify flatten plane sits thickness higher. self.sole_plane = (self.skin_min_z - SOLE_TOTAL_M + base.CLOTH_THICKNESS_M) self.sole_top = self.sole_plane + SOLE_BAND_M * self.s self.seam_w = max(SEAM_W_M * self.s, SEAM_W_MIN_M) self.topline = {} # per foot side ('L'/'R'), set by ankle cut log(f"landmarks: ankle_z={self.ankle_z:.4f} foot_len={self.foot_len:.4f} " f"(s={self.s:.3f}) cap_y={self.cap_y:.4f} " f"heel_seam_y={self.heel_seam_y:.4f} skin_min_z={self.skin_min_z:.4f} " f"sole_plane={self.sole_plane:.4f} sole_top={self.sole_top:.4f} " f"seam_w={self.seam_w * 1000:.1f}mm") # -------------------------------------------------------------------------- # Geometry # -------------------------------------------------------------------------- def recalc_normals(shell): """Consistent outward face normals pre-offset (the raw foot carries a separately-authored sole plate whose orientation is not guaranteed).""" bm = bmesh.new() bm.from_mesh(shell.data) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) bm.to_mesh(shell.data) bm.free() shell.data.update() log("recalculated outward face normals") def _side(x): return "L" if x >= 0.0 else "R" def ankle_cut_and_flatten(shell, lm, lift): """Per foot: topline = clamp(ankle-ring valley + lift); delete verts above it; drop disconnected slivers; flatten the fresh boundary UP onto the plane. Lifting (not lowering) is safe for footwear: the rim sits a full shell-offset OUTSIDE the skin, so raising it only deepens the overlap with the (visible) ankle skin — cloth over skin, never a gap.""" me = shell.data bm = bmesh.new() bm.from_mesh(me) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() # 1. Per-foot ankle-ring valley (the welded shell's only open boundary). ring_z = {"L": [], "R": []} for e in bm.edges: if len(e.link_faces) == 1: for v in e.verts: ring_z[_side(v.co.x)].append(v.co.z) floor_z = TOPLINE_FLOOR_FRAC * lm.ankle_z ceil_z = TOPLINE_CEIL_FRAC * lm.ankle_z for side in ("L", "R"): if not ring_z[side]: raise RuntimeError(f"no ankle boundary ring on side {side}") valley = min(ring_z[side]) lm.topline[side] = min(max(valley + lift, floor_z), ceil_z) log(f"topline {side}: valley={valley:.4f} -> {lm.topline[side]:.4f} " f"(guards [{floor_z:.4f}, {ceil_z:.4f}], ring teeth " f"{max(ring_z[side]) - valley:.4f} m)") # 2. Cut above the topline. doomed = [v for v in bm.verts if v.co.z > lm.topline[_side(v.co.x)]] bmesh.ops.delete(bm, geom=doomed, context='VERTS') log(f"ankle cut removed {len(doomed)} verts") # 3. Sliver cleanup: islands the cut disconnected. bm.verts.ensure_lookup_table() seen = set() doomed_isl = [] for v in bm.verts: if v.index in seen: continue stack, isl = [v], set() while stack: cur = stack.pop() if cur.index in isl: continue isl.add(cur.index) for e in cur.link_edges: o = e.other_vert(cur) if o.index not in isl: stack.append(o) seen |= isl if len(isl) < MIN_ISLAND_VERTS: doomed_isl.extend(isl) if doomed_isl: bm.verts.ensure_lookup_table() bmesh.ops.delete(bm, geom=[bm.verts[i] for i in doomed_isl], context='VERTS') log(f"removed {len(doomed_isl)} sliver-island verts") # 4. Flatten the fresh jagged boundary UP onto the topline plane. bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() lifted = 0 for e in bm.edges: if len(e.link_faces) == 1: for v in e.verts: tl = lm.topline[_side(v.co.x)] if v.co.z != tl: v.co.z = tl lifted += 1 bm.to_mesh(me) bm.free() me.update() log(f"flattened ankle rims: {lifted} boundary verts -> topline planes") def relax_rim(shell, lm, passes): """XY neighbour-averaging over the topline boundary ring (z pinned to the topline) so the throat opening reads smooth. Mild by design: the ring sits a full shell-offset outside the skin, and 2 half-weight passes stay well inside that budget.""" if passes <= 0: return me = shell.data bm = bmesh.new() bm.from_mesh(me) bm.verts.ensure_lookup_table() adj = {} for e in bm.edges: if len(e.link_faces) == 1: a, b = e.verts adj.setdefault(a.index, []).append(b.index) adj.setdefault(b.index, []).append(a.index) for _ in range(passes): new_xy = {} for i, nbrs in adj.items(): if not nbrs: continue ax = sum(bm.verts[j].co.x for j in nbrs) / len(nbrs) ay = sum(bm.verts[j].co.y for j in nbrs) / len(nbrs) v = bm.verts[i] new_xy[i] = (0.5 * v.co.x + 0.5 * ax, 0.5 * v.co.y + 0.5 * ay) for i, (x, y) in new_xy.items(): bm.verts[i].co.x = x bm.verts[i].co.y = y bm.to_mesh(me) bm.free() me.update() log(f"relaxed topline rim: {len(adj)} verts, {passes} XY passes") def snapshot_skin_bvh(shell): """BVH of the current (post-cut, pre-smoothing) skin surface — at this stage the shell verts still ARE the skin verts, so this is the reference every later deformation is measured against.""" import mathutils.bvhtree bm = bmesh.new() bm.from_mesh(shell.data) bvh = mathutils.bvhtree.BVHTree.FromBMesh(bm) bm.free() log("snapshotted skin surface BVH (clearance reference)") return bvh def enforce_clearance(shell, skin_bvh, min_clearance, skip_forward_of_u=None): """Push any shell vert closer than `min_clearance` to the skin snapshot out to exactly that standoff (along the skin normal). Protects the instep / throat, where the rim relax can migrate verts toward the skin. The toe zone is EXCLUDED (skip_forward_of_u): base.convex_toe_box builds an analytic cap that already stands off the skin by construction; re-snapping it to the skin here would re-imprint the individual toes (the original bug). Verts with forward coord u = y*FRONT_Y_SIGN > skip_forward_of_u are left untouched.""" me = shell.data pushed = 0 worst = 0.0 for v in me.vertices: if skip_forward_of_u is not None and \ (v.co.y * FRONT_Y_SIGN) > skip_forward_of_u: continue hit = skin_bvh.find_nearest(v.co) if hit is None or hit[0] is None: continue location, normal, _idx, _dist = hit signed = (v.co - location).dot(normal) if signed < min_clearance: v.co = location + normal * min_clearance pushed += 1 worst = max(worst, min_clearance - signed) me.update() log(f"enforced clearance {min_clearance * 1000:.1f} mm: pushed {pushed} " f"verts (worst deficit {worst * 1000:.1f} mm)") def classify_sole_verts(shell): """Indices of downward-facing verts (the foot underside), pre-offset.""" bm = bmesh.new() bm.from_mesh(shell.data) bm.normal_update() idx = [v.index for v in bm.verts if v.normal.z < -0.5] bm.free() log(f"classified {idx and len(idx) or 0} sole (downward-normal) verts") return idx def flatten_sole(shell, sole_idx, sole_plane): """Post-offset: pull the underside onto the flat sole plane, then clamp everything on/above it (toe rounding can dip below after the offset).""" me = shell.data for i in sole_idx: me.vertices[i].co.z = sole_plane clamped = 0 for v in me.vertices: if v.co.z < sole_plane: v.co.z = sole_plane clamped += 1 me.update() log(f"flattened sole: {len(sole_idx)} verts -> z={sole_plane:.4f} " f"(+{clamped} clamped)") def clamp_topline_residue(shell, lm): """Post-solidify safety clamp: rim-adjacent verts the Solidify pushed above the topline get squashed back onto it (denim waist pattern).""" me = shell.data n = 0 for v in me.vertices: tl = lm.topline[_side(v.co.x)] if v.co.z > tl: v.co.z = tl n += 1 me.update() if n: log(f"clamped {n} residual topline verts") def normalize_uv0(shell): """Rescale the used UV0 bbox to fill [0,1] (uniform scale, aspect kept). Feet use a tiny corner of the body atlas; both the albedo and the mask are authored here on UV0, so reclaiming the space is free texel density.""" me = shell.data bm = bmesh.new() bm.from_mesh(me) uvl = bm.loops.layers.uv[0] us, vs = [], [] for f in bm.faces: for lo in f.loops: us.append(lo[uvl].uv.x) vs.append(lo[uvl].uv.y) u0, u1, v0, v1 = min(us), max(us), min(vs), max(vs) span = max(u1 - u0, v1 - v0) if span < 1e-6: bm.free() log("WARNING: degenerate UV bbox — normalization skipped") return scale = 0.96 / span for f in bm.faces: for lo in f.loops: uv = lo[uvl].uv uv.x = 0.02 + (uv.x - u0) * scale uv.y = 0.02 + (uv.y - v0) * scale bm.to_mesh(me) bm.free() me.update() log(f"normalized UV0: bbox ({u0:.3f},{v0:.3f})..({u1:.3f},{v1:.3f}) " f"-> [0.02,0.98] (x{scale:.1f} density)") # -------------------------------------------------------------------------- # Feature field (texel-level; drives albedo AND mask together) # -------------------------------------------------------------------------- def _paint_texels(px, py, pz, noise, lm): """Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions. Regions (spec): sole -> R, upper -> G, toe cap -> B. Painted lines (albedo only): toe cap line, sole welt stitch, topline edge stitch, heel counter seam. """ n = px.shape[0] tl = np.where(px >= 0.0, lm.topline.get("L", 1.0), lm.topline.get("R", 1.0)) w2 = lm.seam_w * 0.5 sole = pz < lm.sole_top cap = (~sole) & (py <= lm.cap_y) # --- albedo ------------------------------------------------------------- alb = np.empty((n, 4), dtype=np.float32) for c in range(3): alb[:, c] = LEATHER_RGB[c] + noise alb[:, 3] = 1.0 for c in range(3): alb[sole, c] = SOLE_RGB[c] + noise[sole] if not PLAIN: capline = (~sole) & (np.abs(py - lm.cap_y) < w2) welt = np.abs(pz - lm.sole_top) < w2 topstitch = (~sole) & (np.abs(pz - (tl - 3.0 * w2)) < w2) heelseam = (~sole) & (py > 0.0) \ & (np.abs(py - lm.heel_seam_y) < w2) thread = capline | welt | topstitch | heelseam for c in range(3): alb[thread, c] = STITCH_RGB[c] # --- region mask: sole R / upper G / toe cap B --------------------------- mask = np.zeros((n, 4), dtype=np.float32) mask[sole, 0] = 1.0 mask[cap, 2] = 1.0 mask[~(sole | cap), 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 footwear 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"shoes_albedo_{body}", albedo_path) _save(mask_buf, f"shoes_mask_{body}", mask_path) log(f"saved albedo -> {albedo_path}") log(f"saved mask -> {mask_path}") # Re-save the albedo under the SHARED sidecar name and leave the image # datablock pointing there: the glTF exporter derives the embedded image # name from the filepath basename, so the GLB carries "base_albedo" and # Godot's extract-on-import lands exactly on _base_albedo.png (the # wave-1 convention) instead of doubling to __base_albedo.png. # The pixels at base_albedo.png are THIS body's during its export; main() # restores the reference body's copy after the loop. shared_path = os.path.join(os.path.dirname(albedo_path), "base_albedo.png") albedo_img.filepath_raw = shared_path albedo_img.save() return albedo_img # -------------------------------------------------------------------------- # Per-body authoring # -------------------------------------------------------------------------- def author_shoes(body_dir, out_dir, body, offset, topline_lift): base.clear_scene() base.COVERED_SEGMENTS = COVERED_SEGMENTS shell, armature = base.build_covered_mesh(body_dir) denim.weld_boundaries(shell) # zips upper + sole plate + ankle slivers recalc_normals(shell) lm = FootLandmarks(armature, shell.data) ankle_cut_and_flatten(shell, lm, topline_lift) relax_rim(shell, lm, RIM_RELAX_PASSES) sole_idx = classify_sole_verts(shell) # skin downward normals skin_bvh = snapshot_skin_bvh(shell) # instep clearance reference # Smooth convex toe box (replaces Laplacian smooth + skin-conforming clamp, # which re-imprinted the individual toes). Runs on the raw skin toe so the # cap encloses the real toes; offset then adds standoff. ball_u = lm.ball_head_y * FRONT_Y_SIGN 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) # Instep/throat clearance only — the toe zone is excluded so the analytic # cap is never re-snapped to the skin toes. enforce_clearance(shell, skin_bvh, offset, skip_forward_of_u=ball_u) flatten_sole(shell, sole_idx, lm.sole_plane) base.solidify(shell, base.CLOTH_THICKNESS_M) clamp_topline_residue(shell, lm) if UV_NORMALIZE: normalize_uv0(shell) denim.author_parked_uv2(shell) # shoes 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 CAP_BALL_FRAC, LEATHER_RGB, SOLE_RGB, SOLE_TOTAL_M, PLAIN global UV_NORMALIZE argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] if len(argv) < 2: print("Usage: -- [--bodies a,b,c] " "[--offset M] [--sole-mm M] [--topline-lift M] [--cap-frac F] " "[--base-rgb r,g,b] [--sole-rgb r,g,b] [--plain] " "[--no-uv-normalize]") sys.exit(1) bodies_root = argv[0] out_dir = argv[1] offset = SHELL_OFFSET_M topline_lift = TOPLINE_LIFT_M if "--offset" in argv: offset = float(argv[argv.index("--offset") + 1]) if "--sole-mm" in argv: SOLE_TOTAL_M = float(argv[argv.index("--sole-mm") + 1]) if "--topline-lift" in argv: topline_lift = float(argv[argv.index("--topline-lift") + 1]) if "--cap-frac" in argv: CAP_BALL_FRAC = float(argv[argv.index("--cap-frac") + 1]) if "--base-rgb" in argv: LEATHER_RGB = tuple( float(v) for v in argv[argv.index("--base-rgb") + 1].split(",")) if "--sole-rgb" in argv: SOLE_RGB = tuple( float(v) for v in argv[argv.index("--sole-rgb") + 1].split(",")) if "--plain" in argv: PLAIN = True if "--no-uv-normalize" in argv: UV_NORMALIZE = False 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"shoes per-body mode: {len(bodies)} bodies, offset " f"{offset * 1000:.0f} mm, sole {SOLE_TOTAL_M * 1000:.0f} mm, " f"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_shoes(body_dir, out_dir, body, offset, topline_lift) 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()