Files
settled-reach/tooling/scripts/blender/blender_author_shoes_formal.py
T
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

719 lines
29 KiB
Python

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