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

718 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):
reach blender run blender_author_shoes_formal \
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()