Files
settled-reach/tooling/scripts/blender/blender_author_sneakers.py
T
jpmschweitzerandClaude Opus 5.5 c597ec9131 docs(tooling): T-1253 — sweep the live references to retired tool paths
A script scanned every tracked doc, rule, skill, agent, hook and source file
for tooling/ paths that no longer exist, skipping historical records (sprints,
discussions, workshops, governance, generated wiki pages). It found 62. The
ones that tell a reader what to RUN now name the reach verb:

- The atlas skill still sent agents to tooling/atlas, atlas-verify,
  atlas-update-field and atlas-commit-and-sync — about forty lines, all
  retired in T-1285. They now name the `reach atlas` verbs, and the skill
  records that commit-and-sync STAGES by default (--commit to commit) and
  takes --corridor as an option.
- The clerk agent named tooling/clerk-review (now `reach dev clerk`). The Si
  and clerk briefings sent those agents to the retired tooling/db/decision
  and sqlite-query CLIs and to decisions/*.md paths that moved to
  governance/ in the pql migration. They now name pql.
- The ticket-cli rule documented `pql decisions read`, which does not exist;
  `show` already includes the body.
- The culture authoring guide and the RON sources name
  `reach validate ron`, with the same arguments as before.
- The 41 Blender payloads' usage lines ran the retired tooling/blender
  wrapper, and the docstrings still cited pre-carve-out paths. They now read
  `reach blender run <payload>`.
- Doc comments in server/, client/, wiki TOMLs and the domain modules.

What is left is deliberate: "Formerly …" provenance, dated plans and findings
docs, the retired-pipeline doc, and a build-artefact path.

project.yaml 0.4.14 (mirrored to the client). Comment-only, but four touched
files are in the canvas-version registry (trait_catalog_reader.rs, since
T-1289, canvas_sources.py itself, and two client files). The gate is
path-based and has no override. The previous push was rejected on exactly
this.

Three of the edits are stamped ledger sources, so systems.db is regenerated
and the stamp is fresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:13:58 +02:00

753 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):
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: <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()