""" blender_author_track_jacket.py (T-1089 wave 2 — track_jacket, sport family) Authors the TRACK JACKET as per-body offset shells, reusing blender_author_offset_shell.py as a library (scene build, segment join, bone-ratio thresholds, offset, solidify, logo UV2, skinned GLB export). Sport-top sibling of blender_author_outerwear.py (jacket_modern); what track kit needs beyond that script is delivered as reusable parameters, not hacks: * SLEEVE STRIPES AS THEIR OWN REGION (the recolorable team stripe): two parallel stripes run along the TOP of each sleeve from the shoulder seam to the cuff. Stripe placement uses a per-|x| arm-axis interpolation from the upperarm/lowerarm bone landmarks (the denim script's per-z leg-axis technique rotated onto the arms), with constant-metric arc widths, so the stripes stay parallel on every body. * Region layout per spec: collar=R, body=G, sleeve stripes=B, cuffs + hem band=A (toon_garment.gdshader channel-blends 4 tints). * TEXEL-level combined bake (denim best practice): every UV0 triangle is rasterized once with barycentric-interpolated 3D positions and ONE analytic feature-field evaluation drives BOTH the painted albedo and the region mask, so paint and regions always agree. * Painted albedo identity (luma-carried, survives any tint): full front ZIP (bright teeth dashes + darker placket + edge stitching) running hem -> through the stand collar; STAND COLLAR raised look via texture shading (base seam ring + cast-shadow band under the collar + brighter gradient toward the collar rim — geometry stays stock); ribbed knit cuff + hem bands with border stitch lines; stripe edge piping. * Required bottoms/footwear practices adopted for this top (blender_author_denim_pants.py): boundary WELD of coincident segment-seam rings before offsetting (elbow/shoulder/chest seams offset as one surface, no gap rings) and open-rim FLATTENING — the segment splitter leaves ~4.5-7.6 cm jagged teeth at the torso-bottom hem ring, and the vert-threshold wrist cut leaves jagged sleeve ends. The hem ring is pulled UP to its own valley (clean elastic hem, no invented coverage) with a post-solidify residue clamp; the wrist rings are pulled OUT to the nominal cut plane (clean cuff edge over real forearm skin). * LEFT-BREAST logo patch (logo-capable): the base UV2 chest box shifts off-centre — the zip owns the centre line — using the outerwear ratios. PER-BODY ONLY (Q-060: offset shells are authored per body, never SD-fit): tooling/blender --background --python \ tooling/garment-fit/blender_author_track_jacket.py -- \ client/assets/characters/bodies \ client/assets/characters/clothing/track_jacket \ [--bodies average_m,child,...] [--offset 0.018] [--wrist-keep 0.90] 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 (pre-fitted per body), D-251 (in-house wardrobe), Q-060. """ 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) def log(msg): print(f"[track-jacket] {msg}") # -------------------------------------------------------------------------- # Parameters — track_jacket. A sport-top variant should only touch this block. # -------------------------------------------------------------------------- GARMENT_ID = "track_jacket" SEGMENTS = [ "seg_torso", "seg_torso_upper", "seg_arm_upper_l", "seg_arm_upper_r", "seg_arm_lower_l", "seg_arm_lower_r", ] OFFSET_M = 0.018 # sport shell drape: between the 16 mm hoodie and the # 22 mm casual jacket (per-body construction # guarantees body clearance either way) CLOTH_THICKNESS_M = 0.005 # tricot/poly shell weight WRIST_KEEP_FRAC = 0.90 # fraction of the lowerarm kept (hands show) CUFF_BAND_FRAC = 0.18 # last fraction of the KEPT forearm = knit cuff (A) WELD_DIST = 5e-4 # boundary-ring weld tolerance (0.5 mm) TEX_SIZE = 1024 # albedo + mask resolution (zip teeth need > 512) # Stand collar (outerwear-calibrated: taller + wider than the tee band). COLLAR_DROP_NECK_FRAC = 0.55 # collar band starts this far below the neck head COLLAR_X_MULT = 1.15 # slightly wider than the tee band # Sleeve stripes — metric widths on average_m, scaled by shoulder ratio. STRIPE_GAP_HALF_M = 0.006 # half-gap between the two stripes (about the top line) STRIPE_W_M = 0.018 # width of each stripe STRIPE_EDGE_W_M = 0.0035 # painted piping line at each stripe border # Front zip (hem -> through the collar), slimmer than jacket_modern's placket. ZIP_HALF_FRAC = 0.022 / base._REF_SHOULDER_X # placket half-width / shoulder |x| TEETH_HALF_W = 0.006 # zip teeth strip half-width, metres TEETH_PERIOD = 0.024 # dash period along Z, metres TEETH_DUTY = 0.014 # bright dash length within a period, metres # Hem band height as fraction of the garment span (collar_z_min - hem plane). HEM_BAND_FRAC = 0.07 RIB_PERIOD_M = 0.009 # knit rib period on cuff/hem bands LINE_W_M = 0.004 # seam / border stitch line half-width # Collar raised-look shading. SHADOW_H_FRAC = 0.30 # under-collar cast-shadow band height, x neck_len COLLAR_GRAD = 0.06 # extra luma toward the collar rim (raised read) COLLAR_REACH_MULT = 1.35 # seam/shadow lateral reach, x collar_x_abs # Left-breast logo patch (outerwear ratios; centre line belongs to the zip). CHEST_X_LO_FRAC = 0.035 / base._REF_SHOULDER_X CHEST_X_HI_FRAC = 0.115 / base._REF_SHOULDER_X CHEST_Z_LO_FRAC = (1.30 - base._REF_SPINE_LO_Z) / (base._REF_NECK_Z - base._REF_SPINE_LO_Z) CHEST_Z_HI_FRAC = (1.42 - base._REF_SPINE_LO_Z) / (base._REF_NECK_Z - base._REF_SPINE_LO_Z) # Painted-albedo luma palette (toon_garment recolors by luma: tint*(luma*1.5+0.2)). BASE_LUMA = 0.55 # tricot shell STRIPE_LUMA = 0.62 # stripes slightly lighter (read under same-tint too) STRIPE_EDGE_LUMA = 0.36 # stripe piping COLLAR_LUMA = 0.57 # stand collar band COLLAR_SEAM_LUMA = 0.30 # collar base seam ring COLLAR_SHADOW_LUMA = 0.42 # cast shadow just under the collar base PLACKET_LUMA = 0.48 # zip placket band ZIP_EDGE_LUMA = 0.34 # placket edge stitching TEETH_LUMA = 0.88 # bright zip teeth dashes TEETH_GAP_LUMA = 0.34 # dark tape between dashes BAND_LUMA = 0.46 # ribbed cuff + hem bands BAND_LINE_LUMA = 0.32 # band border stitch line RIB_DELTA = 0.035 # knit rib darkening within bands ALBEDO_NOISE = 0.02 # woven jitter FABRIC_HUE = np.array([0.97, 0.98, 1.03], dtype=np.float32) # faint cool cast NOISE_SEED = 2094 # -------------------------------------------------------------------------- # Thresholds — base derivation + track-jacket extras from the same armature # -------------------------------------------------------------------------- def track_thresholds(armature, wrist_keep): thr = base.derive_thresholds(armature) bones = armature.data.bones neck = bones.get("neck_01") ua_l = bones.get("upperarm_l") ua_r = bones.get("upperarm_r") la_l = bones.get("lowerarm_l") spine01 = bones.get("spine_01") if ua_l and ua_r: shoulder_x = (abs(ua_l.head_local.x) + abs(ua_r.head_local.x)) / 2.0 else: shoulder_x = base._REF_SHOULDER_X thr["sh"] = shoulder_x / base._REF_SHOULDER_X # Taller, wider stand collar. if neck: neck_len = neck.tail_local.z - neck.head_local.z thr["collar_z_min"] = neck.head_local.z - COLLAR_DROP_NECK_FRAC * neck_len thr["neck_len"] = neck_len else: thr["neck_len"] = base._REF_NECK_LEN thr["collar_x_abs"] = thr["collar_x_abs"] * COLLAR_X_MULT # Front zip placket. thr["zip_half_x"] = shoulder_x * ZIP_HALF_FRAC # Wrist cut planes + knit cuff start on the lowerarm bones. cut_planes = [] cuff_abs = [] for bone_name, sign in [("lowerarm_l", +1), ("lowerarm_r", -1)]: b = bones.get(bone_name) if b is None: log(f"WARNING: bone {bone_name} missing — sleeve left full-length") continue head_x = b.head_local.x tail_x = b.tail_local.x cut_x = head_x + wrist_keep * (tail_x - head_x) band_x = head_x + (wrist_keep - CUFF_BAND_FRAC) * (tail_x - head_x) cut_planes.append((sign, cut_x)) cuff_abs.append(abs(band_x)) log(f"wrist cut {bone_name}: keep |x| up to {cut_x:.3f} " f"(elbow {head_x:.3f} -> wrist {tail_x:.3f}), cuff from {band_x:.3f}") thr["wrist_cut_planes"] = cut_planes thr["cuff_x_abs"] = min(cuff_abs) if cuff_abs else 1e9 # Per-|x| arm axis (shoulder -> elbow -> wrist) for stripe placement — # the denim per-z leg-axis technique rotated onto the arms. The rig is # x-mirrored, so the left-arm landmarks serve both sides via |x|. if ua_l and la_l: pts = [ua_l.head_local, la_l.head_local, la_l.tail_local] order = np.argsort([abs(p.x) for p in pts]) thr["arm_x"] = np.array([abs(pts[i].x) for i in order]) thr["arm_y"] = np.array([pts[i].y for i in order]) thr["arm_z"] = np.array([pts[i].z for i in order]) else: log("WARNING: arm landmark bones missing — stripes disabled") thr["arm_x"] = None # Left-breast logo patch (replaces the base full-chest box). thr["chest_x"] = (shoulder_x * CHEST_X_LO_FRAC, shoulder_x * CHEST_X_HI_FRAC) if neck and spine01: spine_lo = spine01.head_local.z span = neck.head_local.z - spine_lo thr["chest_z"] = (spine_lo + CHEST_Z_LO_FRAC * span, spine_lo + CHEST_Z_HI_FRAC * span) log(f"track thresholds: collar z>={thr['collar_z_min']:.3f} " f"|x|<{thr['collar_x_abs']:.3f} zip half {thr['zip_half_x']:.3f} " f"cuff |x|>={thr['cuff_x_abs']:.3f} sh {thr['sh']:.3f} " f"chest x=({thr['chest_x'][0]:.3f},{thr['chest_x'][1]:.3f}) " f"z=({thr['chest_z'][0]:.3f},{thr['chest_z'][1]:.3f})") return thr # -------------------------------------------------------------------------- # Geometry: weld + wrist cut + open-rim flattening (denim required practices) # -------------------------------------------------------------------------- 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 wrist_cut(shell, thr): """Delete forearm verts beyond the wrist plane on each side.""" cut_planes = thr.get("wrist_cut_planes", []) if not cut_planes: log("WARNING: no wrist cut planes — sleeves stay full length") return bm = bmesh.new() bm.from_mesh(shell.data) bm.verts.ensure_lookup_table() to_delete = [] for v in bm.verts: for sign, thr_x in cut_planes: if sign > 0 and v.co.x > thr_x: to_delete.append(v) break if sign < 0 and v.co.x < thr_x: to_delete.append(v) break bmesh.ops.delete(bm, geom=to_delete, context='VERTS') bm.to_mesh(shell.data) bm.free() shell.data.update() log(f"wrist cut removed {len(to_delete)} verts; " f"{len(shell.data.vertices)} remain") def flatten_open_rims(shell, thr): """Pull the open boundary rings onto clean planes (pre-offset). The segment splitter cuts along weight thresholds, so the torso-bottom hem ring is a jagged ring of teeth (the denim script measured ~4.5-7.6 cm across bodies), and the vert-threshold wrist cut leaves jagged sleeve ends. Three rims are cleaned: * WRIST rings (|x| >= sleeve_x_abs, per side): pulled OUT to the nominal bone-plane cut — a clean vertical cuff edge; the forearm skin continues well past the plane, so no clip risk is invented. * HEM ring (bottom of the remaining boundary): pulled UP to its own valley (the shallowest notch) — a clean straight elastic hem without inventing coverage below the authored shell. * NECK ring (top): left natural — every wave-1 top ships the natural neckline boundary and the collar band owns that edge visually. Only boundary verts move; weights and loop UVs ride along. Returns the hem plane z (needed for the hem band paint + post-solidify clamp). """ 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) sleeve_x = thr["sleeve_x_abs"] cut_by_sign = {sign: cut_x for sign, cut_x in thr.get("wrist_cut_planes", [])} wrist = [i for i in boundary if abs(bm.verts[i].co.x) >= sleeve_x] torso = [i for i in boundary if abs(bm.verts[i].co.x) < sleeve_x] zs = [bm.verts[i].co.z for i in torso] z_mid = (min(zs) + max(zs)) / 2.0 hem = [i for i in torso if bm.verts[i].co.z <= z_mid] neck = [i for i in torso if bm.verts[i].co.z > z_mid] moved_wrist = 0 for i in wrist: v = bm.verts[i] sign = 1 if v.co.x >= 0.0 else -1 if sign in cut_by_sign: v.co.x = cut_by_sign[sign] moved_wrist += 1 hem_lo = min(bm.verts[i].co.z for i in hem) hem_plane = max(bm.verts[i].co.z for i in hem) # the valley (shallowest notch) for i in hem: bm.verts[i].co.z = hem_plane bm.to_mesh(me) bm.free() me.update() log(f"flattened open rims: {moved_wrist} wrist verts -> nominal cut planes; " f"hem ring ({len(hem)} verts, teeth {hem_plane - hem_lo:.3f} m) -> " f"z={hem_plane:.3f}; neck ring left natural ({len(neck)} verts)") return hem_plane def clamp_hem_residue(shell, hem_plane): """Post-solidify safety clamp: interior tooth verts still below the hem plane (multi-triangle teeth) get squashed onto it.""" me = shell.data n = 0 for v in me.vertices: if v.co.z < hem_plane: v.co.z = hem_plane n += 1 me.update() if n: log(f"clamped {n} residual hem verts -> {hem_plane:.3f}") # -------------------------------------------------------------------------- # Track feature field (texel-level; drives albedo AND mask together) # -------------------------------------------------------------------------- def _paint_texels(X, Y, Z, noise, thr, hem_plane, hem_top): """Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions. ONE feature evaluation drives both outputs (denim practice), so the painted albedo and the region mask always agree. Region priority: cuff/hem band (A) > sleeve stripes (B) > collar (R) > body (G). """ n = X.shape[0] ax = np.abs(X) front = Y * base.FRONT_Y_SIGN > 0.004 sh = thr["sh"] on_sleeve = ax >= thr["sleeve_x_abs"] in_cuff = ax >= thr["cuff_x_abs"] in_hem = (~on_sleeve) & (Z <= hem_top) czmin = thr["collar_z_min"] in_collar = (~on_sleeve) & (Z >= czmin) & (ax < thr["collar_x_abs"]) # Sleeve stripes: arc distance from the top line of the per-|x| arm axis. if thr.get("arm_x") is not None: cy = np.interp(ax, thr["arm_x"], thr["arm_y"]) cz = np.interp(ax, thr["arm_x"], thr["arm_z"]) dy = Y - cy dz = Z - cz r = np.hypot(dy, dz) + 1e-9 arc = np.arctan2(np.abs(dy), dz) * r # 0 on the sleeve top line g0 = STRIPE_GAP_HALF_M * sh w = STRIPE_W_M * sh stripe_zone = on_sleeve & ~in_cuff stripe = stripe_zone & (arc >= g0) & (arc <= g0 + w) ew = STRIPE_EDGE_W_M * sh stripe_edge = stripe_zone & ( (np.abs(arc - g0) < ew) | (np.abs(arc - (g0 + w)) < ew)) else: arc = np.zeros(n, dtype=np.float32) stripe = np.zeros(n, dtype=bool) stripe_edge = np.zeros(n, dtype=bool) # --- region mask (collar=R, body=G, stripes=B, cuffs/hem=A) ------------- mask = np.zeros((n, 4), dtype=np.float32) is_a = in_cuff | in_hem is_b = stripe & ~is_a is_r = in_collar & ~is_a & ~is_b is_g = ~(is_a | is_b | is_r) mask[is_a, 3] = 1.0 mask[is_b, 2] = 1.0 mask[is_r, 0] = 1.0 mask[is_g, 1] = 1.0 # --- painted luma detail ------------------------------------------------- v = np.full(n, BASE_LUMA, dtype=np.float32) lw = LINE_W_M * sh # Stripes + piping. v[stripe] = STRIPE_LUMA v[stripe_edge] = STRIPE_EDGE_LUMA # Stand collar raised look: band luma + brighter gradient toward the rim, # cast-shadow band + seam ring at the collar base (texture-carried depth). reach = thr["collar_x_abs"] * COLLAR_REACH_MULT v[in_collar] = COLLAR_LUMA grad = np.clip((Z - czmin) / max(thr["neck_len"], 1e-6), 0.0, 1.0) v[in_collar] += COLLAR_GRAD * grad[in_collar] shadow_h = SHADOW_H_FRAC * thr["neck_len"] under = (~in_collar) & (~on_sleeve) & (ax < reach) \ & (Z < czmin) & (Z >= czmin - shadow_h) t = np.clip((czmin - Z) / max(shadow_h, 1e-6), 0.0, 1.0) v[under] = COLLAR_SHADOW_LUMA \ + (BASE_LUMA - COLLAR_SHADOW_LUMA) * t[under] seam = (~on_sleeve) & (np.abs(Z - czmin) < lw) & (ax < reach) v[seam] = COLLAR_SEAM_LUMA # Ribbed cuff + hem bands: band luma, knit ribs, border stitch lines. band = in_cuff | in_hem v[band] = BAND_LUMA cuff_rib = in_cuff & (np.mod(arc, RIB_PERIOD_M) < RIB_PERIOD_M * 0.5) hem_rib = in_hem & (np.mod(X, RIB_PERIOD_M) < RIB_PERIOD_M * 0.5) v[cuff_rib | hem_rib] -= RIB_DELTA v[np.abs(ax - thr["cuff_x_abs"]) < lw] = BAND_LINE_LUMA v[(~on_sleeve) & (np.abs(Z - hem_top) < lw)] = BAND_LINE_LUMA # Front zip: placket band + edge stitching + dashed teeth, hem -> through # the collar (paint overrides bands/collar luma; regions stay untouched). zh = thr["zip_half_x"] v[front & (ax < zh)] = PLACKET_LUMA v[front & (np.abs(ax - zh) < lw * 0.7)] = ZIP_EDGE_LUMA teeth = front & (ax < TEETH_HALF_W) dash = np.mod(Z, TEETH_PERIOD) < TEETH_DUTY v[teeth & dash] = TEETH_LUMA v[teeth & ~dash] = TEETH_GAP_LUMA # --- albedo -------------------------------------------------------------- lum = np.clip(v + noise, 0.0, 1.0) alb = np.empty((n, 4), dtype=np.float32) for c in range(3): alb[:, c] = np.clip(lum * FABRIC_HUE[c], 0.0, 1.0) alb[:, 3] = 1.0 return alb, mask def _raster_tri_paint(alb_buf, mask_buf, noise_buf, uvs, cos, thr, hem_plane, hem_top, W, H): """Barycentric texel fill of one UV triangle: interpolate 3D positions, evaluate the track feature 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], thr, hem_plane, hem_top) alb_buf[ysin, xsin] = alb mask_buf[ysin, xsin] = mask def paint_albedo_and_mask(shell, thr, hem_plane, hem_top, out_dir, 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] = np.clip( (BASE_LUMA + noise_buf) * FABRIC_HUE[c], 0.0, 1.0) alb_buf[:, :, 3] = 1.0 mask_buf = np.zeros((H, W, 4), dtype=np.float32) mask_buf[:, :, 1] = 1.0 # background = body 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]), thr, hem_plane, hem_top, W, H) tri_count += 1 bm.free() log(f"painted {tri_count} UV triangles -> albedo + mask ({W}x{H})") # Honest region tally over the whole mask (texel counts, background excl. # impossible — background is body green by design). tot = W * H counts = { "collar(R)": int((mask_buf[:, :, 0] > 0.5).sum()), "body(G)": int((mask_buf[:, :, 1] > 0.5).sum()), "stripes(B)": int((mask_buf[:, :, 2] > 0.5).sum()), "cuff/hem(A)": int((mask_buf[:, :, 3] > 0.5).sum()), } log("mask texels: " + " ".join( f"{k}={v} ({100.0 * v / tot:.1f}%)" for k, v in counts.items())) 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 _save(mask_buf, f"{GARMENT_ID}_mask_{body}", os.path.join(out_dir, f"{body}_mask.png")) albedo_img = _save(alb_buf, f"{GARMENT_ID}_albedo_{body}", os.path.join(out_dir, f"{body}_base_albedo.png")) log(f"saved mask -> {body}_mask.png") log(f"saved albedo -> {body}_base_albedo.png") # Repoint the image at the shared sidecar name before export: the glTF # exporter names the embedded image after the file basename, and Godot # extracts it as _.png -> _base_albedo.png (tshirt # convention). The shared file is re-pointed to average_m's at the end. albedo_img.filepath_raw = os.path.join(out_dir, "base_albedo.png") albedo_img.save() return albedo_img # -------------------------------------------------------------------------- # Per-body authoring # -------------------------------------------------------------------------- def author_track_jacket(body_dir, out_dir, body, offset, wrist_keep): base.clear_scene() base.COVERED_SEGMENTS = SEGMENTS shell, armature = base.build_covered_mesh(body_dir) thr = track_thresholds(armature, wrist_keep) weld_boundaries(shell) wrist_cut(shell, thr) hem_plane = flatten_open_rims(shell, thr) base.offset_outward(shell, offset) base.solidify(shell, CLOTH_THICKNESS_M) clamp_hem_residue(shell, hem_plane) hem_top = hem_plane + HEM_BAND_FRAC * (thr["collar_z_min"] - hem_plane) log(f"hem band: z {hem_plane:.3f}..{hem_top:.3f}") base.author_logo_uv(shell, thr) # UV2 before bake (bake reads UV0 directly) albedo_img = paint_albedo_and_mask(shell, thr, hem_plane, hem_top, out_dir, body) base.assign_fabric_material(shell, albedo_img) base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) def main(): argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] if len(argv) < 2: print("Usage: -- [--bodies a,b,c] " "[--offset M] [--wrist-keep F]") sys.exit(1) bodies_root = argv[0] out_dir = argv[1] offset = OFFSET_M wrist_keep = WRIST_KEEP_FRAC if "--offset" in argv: offset = float(argv[argv.index("--offset") + 1]) if "--wrist-keep" in argv: wrist_keep = float(argv[argv.index("--wrist-keep") + 1]) 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"track-jacket per-body mode: {len(bodies)} bodies, " f"offset {offset*1000:.0f} mm, wrist keep {wrist_keep:.2f}") 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_track_jacket(body_dir, out_dir, body, offset, wrist_keep) 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()