""" blender_author_sneakers.py (T-1089 wave 2, sneakers_modern + trainer family) Authors low-top TRAINERS as per-body offset shells over BOTH feet (seg_foot_l + seg_foot_r joined into ONE garment — the peasant_shoes both-feet convention), reusing blender_author_offset_shell.py as a library (scene build, join, offset, solidify, GLB export) and blender_author_denim_pants.py's boundary weld. What footwear needs beyond the bottoms family, as reusable parameters: * boundary WELD first (denim practice) — the raw foot segment is NOT one surface: the sole is a separate coincident-vert patch and the ankle cut leaves floating shards (probe: average_m = 4 islands, muscular_m/child = 8). remove_doubles at 0.5 mm fuses everything into one watertight-except-ankle shell per foot; weights identical by origin, so skinning is unaffected. * COLLAR cut + rim flatten — the segment splitter's ankle boundary is jagged weight-threshold teeth (3.3-8.3 cm across bodies, back-biased). Verts above the collar plane (a fraction of the ankle-joint height) are deleted, then every remaining boundary vert is pulled ONTO the collar plane — a clean horizontal low-top opening. Re-flattened after the outward offset because rim-vert normals have a +z bias that would lift the rim. * SOLE slab (this family's new offset-shell param) — the rim-flatten practice applied to the ground plane, built as cut + flatten + extrude + fill: the shell's underside band is cut away (with the toe-knuckle lobes that survive smoothing), the open rim is flattened onto the cut plane and its outline relaxed, then extruded straight down to a plane `--sole-drop` (scaled per body) below the body's own foot-bottom and closed with a flat bottom — a clean prism slab; Solidify thickens it and a final clamp guarantees the outer sole is planar. (Snapping the band in place instead collapses mesh rows into crumpled slivers — melted-wax scallops on the probe renders.) * TOE-BOX SMOOTHING + ROUNDING — the body feet have INDIVIDUAL TOES; a raw offset shell reads as a foot-shaped slipper (verified on average_m). Heavy iterative vertex smoothing over the toe region (feathered, boundary rim pinned) melts the toe creases into one volume, a light global pass de-lumps the ankle anatomy, then an extra normal-along inflation feathered toward the toe tip restores the lost volume as a rounded sneaker toe box. * COLLAR FLARE — small feathered radial stand-off at the opening (the jeans waist-flare practice) for ankle-flex clearance under Walk/Crouch. * TEXEL-level feature painting (denim practice): one analytic field drives BOTH the albedo and the region mask, so they always agree: - albedo: painted lace cross-straps over a darker tongue panel, toe cap + border line, foxing stripe at the sole top, heel tab, collar band, vamp + heel-counter panel lines (swoosh-free — no brand marks). Everyday default: white/grey, flat toon-friendly tones. - mask: sole -> R, upper -> G, laces + trim (collar band, toe cap, heel tab) -> B (spec: sole=R, upper=G, laces+trim=B). * a parked logo_uv TEXCOORD_1 layer (all UVs at (2,2)) — not logo-capable, but toon_garment.gdshader samples UV2 unconditionally. All parameters derive PER BODY from that body's own bone landmarks (foot_l / ball_l) and measured mesh extents (foot length, half-width, ground plane), scaled by foot length against the hand-calibrated average_m reference — the same proportional-ratio philosophy as base.derive_thresholds. Per-body mode only (offset shells author per body, Q-060). Left/right feet share body-atlas UV space whose islands may overlap; every painted feature is |x|-mirror symmetric, so overlapping texels agree by construction. Usage (sneakers_modern reference invocation): reach blender run blender_author_sneakers \ client/assets/characters/bodies \ client/assets/characters/clothing/sneakers_modern \ [--bodies average_m,child,...] [--offset 0.009] [--sole-drop 0.015] \ [--sole-snap-frac 0.55] [--collar-frac 0.95] [--collar-flare 0.002] \ [--toe-round 0.004] [--toe-smooth 12] [--laces 4] [--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 + the denim module (weld utility) # -------------------------------------------------------------------------- _HERE = os.path.dirname(os.path.abspath(__file__)) def _load(mod_name, file_name): spec = importlib.util.spec_from_file_location( mod_name, os.path.join(_HERE, file_name)) 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 (CLI-overridable ones are module globals) # -------------------------------------------------------------------------- COVERED_SEGMENTS = ["seg_foot_l", "seg_foot_r"] OFFSET_M = 0.009 # shoe standoff — snugger than cloth (12 mm) SOLE_DROP_M = 0.015 # sole slab depth below the body's own foot bottom # (scaled by foot length per body) SOLE_SNAP_FRAC = 0.55 # sole CUT height as a fraction of the sole rise # (ground -> foxing top): everything below is cut # away and rebuilt as an extruded prism slab (see # build_sole_slab) — kills the toe-knuckle underside # lobes that survive smoothing (probe, average_m) COLLAR_FRAC = 0.95 # collar plane as fraction of ankle-joint height COLLAR_FLARE_M = 0.002 # radial stand-off at the opening (ankle-flex room) SMOOTH_GLOBAL_ITERS = 2 # light instep/ankle de-lumping passes (behind ball; # the toe box is built analytically, not smoothed) SMOOTH_FACTOR = 0.5 # Convex toe box (shared base.convex_toe_box) — rounded, roomy trainer cap. TOE_EXT_M = 0.012 # nose extension past the longest toe (m) TOE_WMARGIN_M = 0.006 # half-width padding around the widest toe (roomy) TOE_HCLEAR_M = 0.010 # vertical headroom above the toes (flex room) TOE_FEATHER_M = 0.022 # blend band behind the ball N_LACES = 4 # painted cross-straps PLAIN = False # --plain: flat upper, no painted features TEX_SIZE = 1024 # painted lace/panel lines need > 512 NOISE_SEED = 3089 # Vertical proportions — fractions of the collar height above the ground. SOLE_TOP_FRAC = 0.30 # sole sidewall (foxing) top -> R region below this COLLAR_BAND_FRAC = 0.15 # collar trim band height (B region) VAMP_LINE_FRAC = 0.42 # side panel line height between sole top and collar # Foot-axis proportions — fractions of foot length / half-width. TOE_CAP_FRAC = 0.18 # toe cap depth from the toe tip (B region) HEEL_LINE_FRAC = 0.22 # heel-counter panel line from the heel tip LACE_T0, LACE_T1 = 0.18, 0.80 # lace panel span along the foot bone LACE_HALFW_FRAC = 0.40 # lace panel half-width, of foot half-width LACE_STRIPE_DUTY = 0.44 # stripe thickness as a fraction of stripe spacing LINE_W_M = 0.0035 # painted panel/border line half-width (scaled) HEEL_TAB_HALFW_M = 0.012 # heel tab half-width (scaled) # Everyday default: white/grey, flat toon-friendly tones (sRGB floats). UPPER_RGB = (0.880, 0.880, 0.890) SOLE_RGB = (0.780, 0.790, 0.800) TREAD_RGB = (0.450, 0.460, 0.480) # below-ground outsole TOE_RGB = (0.920, 0.920, 0.930) # toe bumper LACE_RGB = (0.960, 0.960, 0.965) TONGUE_SHADE = 0.90 # lace-zone panel behind the straps COLLAR_SHADE = 0.88 # collar band darkening HEEL_TAB_SHADE = 0.72 LINE_SHADE = 0.74 # painted panel/border lines ALBEDO_NOISE = 0.015 # Reference proportions (average_m) the fractions were calibrated against. _REF_FOOT_LEN = 0.2704 # heel y (0.1374) - toe tip y (-0.1330) # -------------------------------------------------------------------------- # Per-body landmarks # -------------------------------------------------------------------------- class FootLandmarks: """Cut/mask/paint parameters from one body's foot bones + measured mesh.""" def __init__(self, armature, shell): 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?") # Bone landmarks (left foot; the right mirrors via |x|). self.ankle_y = foot.head_local.y self.ankle_z = foot.head_local.z self.ball_y = foot.tail_local.y self.ball_z = foot.tail_local.z self.toe_y = ball.tail_local.y # Measured mesh extents (left-foot verts; feet are x-mirror symmetric). lx = [v.co.x for v in shell.data.vertices if v.co.x > 0.0] ly = [v.co.y for v in shell.data.vertices if v.co.x > 0.0] zs = [v.co.z for v in shell.data.vertices] self.foot_cx = (min(lx) + max(lx)) / 2.0 self.half_w = (max(lx) - min(lx)) / 2.0 self.heel_y = max(ly) self.toe_tip_y = min(ly) self.ground_z = min(zs) self.foot_len = self.heel_y - self.toe_tip_y self.s = self.foot_len / _REF_FOOT_LEN self.collar_z = self.ground_z + COLLAR_FRAC * (self.ankle_z - self.ground_z) self.sole_drop = SOLE_DROP_M * self.s self.sole_bottom = self.ground_z - self.sole_drop rise = self.collar_z - self.ground_z self.sole_top = self.ground_z + SOLE_TOP_FRAC * rise self.band_h = COLLAR_BAND_FRAC * rise self.vamp_z = self.sole_top + VAMP_LINE_FRAC * (self.collar_z - self.sole_top) self.cap_y = self.toe_tip_y + TOE_CAP_FRAC * self.foot_len self.heel_line_y = self.heel_y - HEEL_LINE_FRAC * self.foot_len self.lace_halfw = LACE_HALFW_FRAC * self.half_w self.line_w = LINE_W_M * self.s log(f"landmarks: ankle_z={self.ankle_z:.4f} collar_z={self.collar_z:.4f} " f"ground={self.ground_z:.4f} sole_bottom={self.sole_bottom:.4f} " f"sole_top={self.sole_top:.4f} foot_len={self.foot_len:.4f} " f"half_w={self.half_w:.4f} cx={self.foot_cx:.4f} s={self.s:.3f}") # -------------------------------------------------------------------------- # Geometry: collar cut + flatten, sole slab, toe round, collar flare # -------------------------------------------------------------------------- def make_normals_consistent(shell): """Outward-consistent normals BEFORE the offset — the raw foot's sole patch winds independently of the upper, so post-weld normals need one recalc or the offset would pull the sole inward.""" bpy.ops.object.select_all(action='DESELECT') shell.select_set(True) bpy.context.view_layer.objects.active = shell bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.object.mode_set(mode='OBJECT') log("recalculated outward-consistent normals") def collar_cut(shell, collar_z): """Delete verts above the collar plane (bone-plane-cut practice).""" bm = bmesh.new() bm.from_mesh(shell.data) doomed = [v for v in bm.verts if v.co.z > collar_z] bmesh.ops.delete(bm, geom=doomed, context='VERTS') bm.to_mesh(shell.data) bm.free() shell.data.update() log(f"collar cut at z={collar_z:.4f}: removed {len(doomed)} verts") def flatten_collar_rims(shell, collar_z, label): """Pull every open-boundary vert ONTO the collar plane (rim-flatten practice). After the weld + collar cut the only open boundary is the two collar rims, so a single pass flattens both feet.""" me = shell.data bm = bmesh.new() bm.from_mesh(me) bm.verts.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) if not boundary: bm.free() raise RuntimeError("no open collar boundary found after cut") zs = [bm.verts[i].co.z for i in boundary] for i in boundary: bm.verts[i].co.z = collar_z bm.to_mesh(me) bm.free() me.update() log(f"flattened collar rims ({label}): {len(boundary)} verts, " f"z {min(zs):.4f}..{max(zs):.4f} -> {collar_z:.4f}") def smooth_shell(shell, lm): """De-lump the instep/ankle anatomy only (BEHIND the ball joint): light iterative vertex smoothing with the open collar boundary pinned. The toe box itself is built analytically by base.convex_toe_box, so the toe zone (forward of the ball) is deliberately excluded here — smoothing it would shrink the toes the toe box must still enclose.""" me = shell.data bm = bmesh.new() bm.from_mesh(me) bm.verts.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) ball_u = lm.ball_y * FRONT_Y_SIGN # interior verts BEHIND the ball joint (u < ball_u): instep, arch, heel. heel = [v for v in bm.verts if v.index not in boundary and (v.co.y * FRONT_Y_SIGN) < ball_u] for _ in range(SMOOTH_GLOBAL_ITERS): bmesh.ops.smooth_vert(bm, verts=heel, factor=SMOOTH_FACTOR, use_axis_x=True, use_axis_y=True, use_axis_z=True) bm.to_mesh(me) bm.free() me.update() log(f"smoothed instep/heel: {SMOOTH_GLOBAL_ITERS} passes " f"({len(heel)} verts behind ball u<{ball_u:.4f})") RIM_RELAX_ITERS = 3 # along-ring XY relaxation of the cut rim outline def build_sole_slab(shell, lm): """Rim-flatten practice applied to the GROUND plane, done PROPERLY as a cut + flatten + extrude (snapping a whole z-band onto the plane collapses multiple mesh rows into crumpled slivers that read as melted-wax scallops — probe renders v3-v5): 1. DELETE everything below the cut height (SOLE_SNAP_FRAC of the sole rise) — removes the toe-knuckle underside lobes outright. 2. RIM-FLATTEN the resulting open bottom boundary onto the cut plane (exactly the denim ankle practice), then relax the ring outline in XY along the ring only — a smooth footprint curve, rounded toe. 3. EXTRUDE the ring straight down to the sole plane — a clean vertical prism wall (extruded verts inherit the rim verts' deform weights, so the sole still flexes at the ball joint). 4. FILL the bottom ring with faces — a closed flat underside. Solidify then grows the outer surface CLOTH_THICKNESS_M further down, landing the visible outsole on lm.sole_bottom (guaranteed by clamp_residue after solidify).""" plane = lm.sole_bottom + base.CLOTH_THICKNESS_M hi = lm.ground_z + SOLE_SNAP_FRAC * (lm.sole_top - lm.ground_z) me = shell.data bm = bmesh.new() bm.from_mesh(me) # 1. cut doomed = [v for v in bm.verts if v.co.z < hi] bmesh.ops.delete(bm, geom=doomed, context='VERTS') # 2. rim-flatten (the collar rim is also open — split boundaries by z) boundary = {v for e in bm.edges if len(e.link_faces) == 1 for v in e.verts} z_mid = (lm.collar_z + hi) / 2.0 sole_rim = {v for v in boundary if v.co.z < z_mid} if not sole_rim: bm.free() raise RuntimeError("no sole rim found after cut — check cut height") for v in sole_rim: v.co.z = hi # along-ring XY relax: average each rim vert with its ring neighbours # only (bmesh smooth_vert would pull toward the upper rows and shrink) for _ in range(RIM_RELAX_ITERS): new_pos = {} for v in sole_rim: ring_nbrs = [e.other_vert(v) for e in v.link_edges if len(e.link_faces) == 1 and e.other_vert(v) in sole_rim] if len(ring_nbrs) >= 2: ax = sum(n.co.x for n in ring_nbrs) / len(ring_nbrs) ay = sum(n.co.y for n in ring_nbrs) / len(ring_nbrs) new_pos[v] = (v.co.x + 0.5 * (ax - v.co.x), v.co.y + 0.5 * (ay - v.co.y)) for v, (x, y) in new_pos.items(): v.co.x = x v.co.y = y # 3. extrude the rim edges straight down to the sole plane rim_edges = [e for e in bm.edges if len(e.link_faces) == 1 and e.verts[0] in sole_rim and e.verts[1] in sole_rim] ret = bmesh.ops.extrude_edge_only(bm, edges=rim_edges) new_verts = [g for g in ret["geom"] if isinstance(g, bmesh.types.BMVert)] for v in new_verts: v.co.z = plane # 4. close the bottom bottom_edges = [e for e in bm.edges if len(e.link_faces) == 1 and all(abs(v.co.z - plane) < 1e-6 for v in e.verts)] filled = bmesh.ops.holes_fill(bm, edges=bottom_edges, sides=0) # 5. UV-park the new faces. The fill n-gon's default loop UVs span the # whole enclosed UV region — its texel bake overwrites painted islands # (probe v6: mottled patches, features erased); wall quads' copied UVs # sit ON the island boundary where bilinear sampling picks up # background. ONE park point per foot (per-face points sample texels of # varying paint state and stripe the wall — probe v7): all new faces on # a side park on the UV centre of that side's LARGEST-UV-area # rim-adjacent face — big enough that the bake reliably rasterizes its # interior, and its centre lies in the sole band (z <= sole_top), so # the sampled texel is the flat sole tone with an R-region mask. uv_layer = bm.loops.layers.uv[0] if len(bm.loops.layers.uv) else None if uv_layer is not None: new_faces = [g for g in ret["geom"] if isinstance(g, bmesh.types.BMFace)] new_faces += list(filled["faces"]) new_face_set = set(new_faces) best = {} # x-sign side -> (uv_area, centre uv); feet never cross x=0 for v in sole_rim: side = 1 if v.co.x >= 0.0 else -1 for loop in v.link_loops: f = loop.face if f in new_face_set: continue us = [lp[uv_layer].uv for lp in f.loops] area = 0.0 for i in range(len(us)): j = (i + 1) % len(us) area += us[i].x * us[j].y - us[j].x * us[i].y area = abs(area) * 0.5 if side not in best or area > best[side][0]: best[side] = (area, (sum(u.x for u in us) / len(us), sum(u.y for u in us) / len(us))) for f in new_faces: side = 1 if sum(v.co.x for v in f.verts) >= 0.0 else -1 if side not in best: side = -side uv = best[side][1] for loop in f.loops: loop[uv_layer].uv = uv bm.to_mesh(me) bm.free() me.update() log(f"sole slab: cut {len(doomed)} verts (z < {hi:.4f}), rim " f"{len(sole_rim)} verts -> z={hi:.4f}, extruded {len(new_verts)} " f"verts -> z={plane:.4f}, filled {len(filled['faces'])} bottom faces") def collar_flare(shell, lm, flare): """Feathered radial stand-off at the opening (waist-flare practice) — ankle-flex clearance under Walk/Crouch.""" if flare <= 0.0: return z0 = lm.collar_z - 2.0 * lm.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 * lm.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"collar flare: {n} verts, +{flare * 1000:.1f} mm radial at rim") def clamp_residue(shell, lm): """Post-solidify safety clamps: the rim cap can push verts above the collar plane, and blended edge normals leave the outsole slightly uneven — squash both back onto their planes.""" me = shell.data n_top = n_bot = 0 for v in me.vertices: if v.co.z > lm.collar_z: v.co.z = lm.collar_z n_top += 1 elif v.co.z < lm.sole_bottom: v.co.z = lm.sole_bottom n_bot += 1 me.update() log(f"clamped residue: {n_top} collar verts -> {lm.collar_z:.4f}, " f"{n_bot} sole verts -> {lm.sole_bottom:.4f}") # -------------------------------------------------------------------------- # Sneaker feature field (texel-level; drives albedo AND mask together). # Every feature is |x|-mirror symmetric so the (possibly overlapping) # left/right UV islands paint identical values. # -------------------------------------------------------------------------- def _paint_texels(px, py, pz, noise, lm): """Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions.""" n = px.shape[0] side = np.where(px >= 0.0, 1.0, -1.0) dx = px - side * lm.foot_cx # signed offset from the foot centre adx = np.abs(dx) in_sole = pz <= lm.sole_top # Tread = the flat underside plane only (never visible from the side). # The sole SIDE WALL is snapped geometry whose UV triangles are stretched # slivers — any tonal variation there (noise, a dark tread band) smears # into vertical streaks under bilinear magnification, so the whole band # above the underside is painted ONE flat tone. in_tread = pz <= lm.sole_bottom + 0.0015 in_band = (pz >= lm.collar_z - lm.band_h) & ~in_sole # Lace panel: param t along the foot bone (ankle head -> ball tail) in # the (y,z) plane; texels above the bone line, near the centreline. dy_ax = lm.ball_y - lm.ankle_y dz_ax = lm.ball_z - lm.ankle_z l2 = dy_ax * dy_ax + dz_ax * dz_ax t = ((py - lm.ankle_y) * dy_ax + (pz - lm.ankle_z) * dz_ax) / l2 above_bone = pz > (lm.ankle_z + t * dz_ax + 0.002 * lm.s) in_tongue = (~in_sole & above_bone & (adx < lm.lace_halfw) & (t >= LACE_T0) & (t <= LACE_T1)) frac = (t - LACE_T0) / (LACE_T1 - LACE_T0) stripe_pos = frac * N_LACES stripe_d = np.abs(stripe_pos - (np.floor(stripe_pos) + 0.5)) laces = in_tongue & (stripe_d < 0.5 * LACE_STRIPE_DUTY) toe_cap = (py < lm.cap_y) & ~in_sole heel_tab = ((adx < HEEL_TAB_HALFW_M * lm.s) & ~in_sole & (py > lm.ankle_y + 0.55 * (lm.heel_y - lm.ankle_y))) # --- albedo ------------------------------------------------------------- alb = np.empty((n, 4), dtype=np.float32) for c in range(3): alb[:, c] = UPPER_RGB[c] + noise alb[:, 3] = 1.0 for c in range(3): alb[in_sole, c] = SOLE_RGB[c] # flat, noise-free (sliver UVs) alb[in_tread, c] = TREAD_RGB[c] # underside plane only if not PLAIN: for c in range(3): alb[toe_cap & ~in_sole, c] = TOE_RGB[c] + noise[toe_cap & ~in_sole] alb[in_tongue, :3] *= TONGUE_SHADE for c in range(3): alb[laces, c] = LACE_RGB[c] + noise[laces] alb[in_band & ~laces, :3] *= COLLAR_SHADE alb[heel_tab & ~in_band, :3] *= HEEL_TAB_SHADE # Painted panel lines (albedo only — swoosh-free). foxing = np.abs(pz - lm.sole_top) < lm.line_w cap_border = (np.abs(py - lm.cap_y) < lm.line_w) & ~in_sole vamp = ((np.abs(pz - lm.vamp_z) < lm.line_w) & ~in_sole & (adx > lm.lace_halfw * 0.8) & (py > lm.cap_y) & (py < lm.heel_line_y)) heel_ctr = ((np.abs(py - lm.heel_line_y) < lm.line_w) & ~in_sole & (pz < lm.collar_z - lm.band_h)) lines = (foxing | cap_border | vamp | heel_ctr) & ~laces alb[lines, :3] *= LINE_SHADE # --- region mask: sole R / upper G / laces+trim B ------------------------- mask = np.zeros((n, 4), dtype=np.float32) is_b = (laces | toe_cap | heel_tab | in_band) & ~in_sole mask[in_sole, 0] = 1.0 mask[is_b, 2] = 1.0 mask[~(in_sole | is_b), 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 sneaker 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] = UPPER_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() counts = [float(mask_buf[:, :, c].sum()) for c in range(3)] total = max(sum(counts), 1.0) log(f"painted {tri_count} UV triangles ({W}x{H}); mask texels " f"R={100 * counts[0] / total:.1f}% G={100 * counts[1] / total:.1f}% " f"B={100 * counts[2] / total:.1f}%") 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"sneaker_albedo_{body}", albedo_path) _save(mask_buf, f"sneaker_mask_{body}", mask_path) log(f"saved albedo -> {albedo_path}") log(f"saved mask -> {mask_path}") return albedo_img # -------------------------------------------------------------------------- # Per-body authoring # -------------------------------------------------------------------------- def author_sneaker_shell(body_dir, out_dir, body, offset): base.clear_scene() base.COVERED_SEGMENTS = COVERED_SEGMENTS shell, armature = base.build_covered_mesh(body_dir) denim.weld_boundaries(shell) # fuse sole patch + ankle shards make_normals_consistent(shell) lm = FootLandmarks(armature, shell) collar_cut(shell, lm.collar_z) # Convex toe box FIRST, on the raw skin foot (so it encloses the real # toes), then de-lump only behind the ball. 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) smooth_shell(shell, lm) flatten_collar_rims(shell, lm.collar_z, "pre-offset") base.offset_outward(shell, offset) flatten_collar_rims(shell, lm.collar_z, "post-offset") # rim normals lift it build_sole_slab(shell, lm) collar_flare(shell, lm, COLLAR_FLARE_M * lm.s) base.solidify(shell, base.CLOTH_THICKNESS_M) clamp_residue(shell, lm) denim.author_parked_uv2(shell) # not logo-capable; shader needs UV2 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_M, SOLE_DROP_M, COLLAR_FRAC, COLLAR_FLARE_M global N_LACES, PLAIN, SOLE_SNAP_FRAC global TOE_EXT_M, TOE_WMARGIN_M, TOE_HCLEAR_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] [--sole-drop M] [--sole-snap-frac F] " "[--collar-frac F] [--collar-flare M] [--toe-ext M] " "[--toe-wmargin M] [--toe-hclear M] " "[--laces N] [--plain]") sys.exit(1) bodies_root = argv[0] out_dir = argv[1] if "--offset" in argv: OFFSET_M = float(argv[argv.index("--offset") + 1]) if "--sole-drop" in argv: SOLE_DROP_M = float(argv[argv.index("--sole-drop") + 1]) if "--sole-snap-frac" in argv: SOLE_SNAP_FRAC = float(argv[argv.index("--sole-snap-frac") + 1]) if "--collar-frac" in argv: COLLAR_FRAC = float(argv[argv.index("--collar-frac") + 1]) if "--collar-flare" in argv: COLLAR_FLARE_M = float(argv[argv.index("--collar-flare") + 1]) if "--toe-ext" in argv: TOE_EXT_M = float(argv[argv.index("--toe-ext") + 1]) if "--toe-wmargin" in argv: TOE_WMARGIN_M = float(argv[argv.index("--toe-wmargin") + 1]) if "--toe-hclear" in argv: TOE_HCLEAR_M = float(argv[argv.index("--toe-hclear") + 1]) if "--laces" in argv: N_LACES = int(argv[argv.index("--laces") + 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"sneaker per-body mode: {len(bodies)} bodies, offset " f"{OFFSET_M * 1000:.0f} mm, sole-drop {SOLE_DROP_M * 1000:.0f} mm, " f"collar-frac {COLLAR_FRAC}, laces {N_LACES}, 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_sneaker_shell(body_dir, out_dir, body, OFFSET_M) 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()