Files
jpmschweitzerandClaude Opus 5 201dabd19b refactor(tooling): T-1273 — the Blender carve-out, and a guard that keeps it carved
35 payloads move to tooling/scripts/blender/ and stay outside package scope.
They run under Blender's bundled Python, which cannot see the repo venv, so
they physically cannot import tooling.core — holding them to the D-263 contract
would either fail the gate forever or force the contract to be weakened for
everyone, and the second is how a gate stops meaning anything.

Count verified by import rather than filename: 33 import bpy/bmesh directly,
and the two that do not are still payloads per their own usage lines.
garment-fit/make_logo.py is the one genuine non-payload and stays for T-1290.

The bash wrapper is retired rather than kept. Keeping it would have put the
install-resolution logic in two places, which is the duplication T-1286 had
just finished collapsing three copies of. domains/blender/service.py owns the
decisions — resolve_blender (native beats flatpak, ordering preserved),
resolve_payload, absolutise — and only run_payload performs. test_blender.py
pins all of them without launching Blender, which matters here more than
usual: the thing being launched is a 200 MB GUI application that writes GLBs.

`reach blender run` takes a registered payload name OR a path to any script,
because the wrapper served both — the spikes and the glb-gen skill hand it
one-off scripts of their own. An unknown name enumerates all 35 and exits 2.

The exclusion now defends itself. check_carve_out_stays_carved fails if
`scripts` is added to PACKAGE_ROOTS, if the payload directory empties (an empty
exclusion proves nothing), or if an __init__.py appears there (which would make
the payloads importable — the coupling the carve-out exists to prevent). All
three arms mutation-proved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 20:55:52 +02:00

754 lines
33 KiB
Python

"""
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):
tooling/blender --background --python \
tooling/garment-fit/blender_author_sneakers.py -- \
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: <out_dir>/<body>.glb (skinned, albedo embedded)
<out_dir>/<body>_mask.png (RGBA region mask, UV0)
<out_dir>/<body>_base_albedo.png
Plus: <out_dir>/base_albedo.png (average_m's, shared sidecar)
<out_dir>/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_root> <out_dir> [--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()