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

616 lines
26 KiB
Python

"""
blender_author_boots.py (T-1089 wave 2, boots_modern + ankle-boot family)
Authors ANKLE BOOTS as per-body offset shells: both feet PLUS the lower calf
(seg_leg_lower cut to a boot shaft just above the ankle), with a chunky sole
extension and painted eyelets/laces up the shaft. Reuses
blender_author_offset_shell.py (via blender_author_denim_pants.py, imported as
a module) for the shared machinery — scene build, join, offset, solidify,
albedo material, GLB export — plus the two denim REQUIRED PRACTICES for
bottoms/footwear:
* boundary WELD of coincident segment-seam rings (denim.weld_boundaries):
the ankle joins (foot<->leg_lower) duplicate a coincident vert band per
segment; offsetting un-welded rings along diverging normals opens cracks.
* open-rim FLATTENING: the shaft cut leaves a jagged "teeth" ring on each
calf; both rims are pulled down onto one clean shared plane (the deeper
valley of the two, so the boots match) before offsetting.
What this companion adds, as reusable parameters (not hacks):
* --shaft-frac: boot shaft height as a fraction of the ankle->knee calf
span (0.35 cut => rim lands on the lower calf after rim flattening;
larger values give combat/riding variants).
* TOE-BOX MERGE: the skin mesh has individual toes; a boot must not. The
foot region is Laplacian-smoothed pre-offset (mild on the whole foot,
aggressive forward of the ball joint) so the toes fuse into one rounded
leather toe box and anatomical detail (ankle knobs, heel tendon) reads
as boot, not foot. Weights/UVs ride along untouched.
* SKIN CONTAINMENT CLAMP: smoothing can pull the shell inside the skin it
no longer follows (melted toe box vs real toes). A BVH of the ORIGINAL
welded skin is kept, and after the offset every foot-region vert whose
signed distance to the skin is below a minimum clearance is pushed back
out along the skin normal — standoff guaranteed by construction, which
is what the chromakey QA gates on (the runtime does not hide segments).
* FOOT WEIGHT RE-BIND: smoothing + clamping RELOCATE shell verts but leave
them carrying their ORIGIN vertex's bone weights, so material that ends
up hovering over toe N flexes with the bone of toe M — under ball-joint
flexion (Walk push-off, crouch) the shell diverges from the skin beneath
it and the toes poke through (QA/preview evidence, wave 2 run 1/2).
After shaping, every foot-region vert re-copies its vertex-group weights
from the nearest vert of the original welded skin, so the boot flexes
exactly with the anatomy each patch of leather actually covers.
* SOLE geometry as an offset-shell param extension (--sole-drop): the
under-foot surface created by the outward offset is flattened onto a slab
plane sole_drop below the skin sole (solidify adds its thickness back,
so the shipped slab bottom sits exactly sole_drop under the skin), and a
feathered radial LIP (~5 mm) bulges the sole band outward for the chunky
work-boot silhouette.
* --shaft-flare: extra feathered radial stand-off toward the shaft rim.
Serves the same crouch-fold purpose as the denim waist flare AND buys
clearance so full-length pant hems (jeans/formal, 12 mm standoff) tuck
INSIDE the boot shaft instead of z-fighting with it.
* TEXEL-level feature painting (denim technique, boot field): one analytic
field evaluation drives BOTH the painted albedo and the region mask so
they always agree — lace bars + eyelet dots up the front of the shaft,
welt stitching above the sole, padded collar shading at the rim.
Regions (spec): sole -> R, upper + shaft -> G, laces + eyelets -> B.
* a parked logo_uv TEXCOORD_1 layer (boots are not logo-capable, but
toon_garment.gdshader samples UV2 unconditionally).
All cut/mask/paint parameters derive PER BODY from that body's own calf bone
landmarks and measured skin-sole plane, scaled by the calf-span ratio against
average_m (clamped so the child keeps believable, not clown, proportions) —
the same proportional-ratio philosophy as base.derive_thresholds. Per-body
mode only (offset shells author per body, Q-060).
Style pin (T-1089): modern only; identity carried by the texture. The albedo
is painted at mid luma so toon_garment.gdshader's luma-preserving recolor
stays faithful; the brown/black work-street default lives in the manifest
default_tints (sole near-black, upper work brown, laces dark).
Usage (boots_modern reference invocation):
tooling/blender --background --python \
tooling/garment-fit/blender_author_boots.py -- \
client/assets/characters/bodies \
client/assets/characters/clothing/boots_modern \
[--bodies average_m,child,...] [--offset 0.013] [--shaft-frac 0.30] \
[--shaft-flare 0.008] [--sole-drop 0.020] [--base-rgb r,g,b] [--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 denim companion as a library (which itself imports the base
# offset-shell module). Boot reuse: base machinery + denim's weld/parked-UV2.
# --------------------------------------------------------------------------
_HERE = os.path.dirname(os.path.abspath(__file__))
_spec = importlib.util.spec_from_file_location(
"denim_pants_lib", os.path.join(_HERE, "blender_author_denim_pants.py"))
denim = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(denim)
base = denim.base
log = base.log
FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (verified in base)
# --------------------------------------------------------------------------
# Parameters
# --------------------------------------------------------------------------
COVERED_SEGMENTS = [
"seg_foot_l", "seg_foot_r",
"seg_leg_lower_l", "seg_leg_lower_r",
]
SHAFT_FRAC = 0.35 # boot shaft = this fraction of the ankle->knee span
BOOT_OFFSET_M = 0.013 # standoff along normals (leather sits off the skin)
SHAFT_FLARE_M = 0.008 # extra radial stand-off at the shaft rim (feathered)
SOLE_DROP_M = 0.020 # chunky sole slab depth below the skin sole (spec)
SOLE_LIP_M = 0.008 # radial sole bulge (chunky work-boot lip)
SOLE_LIP_TOP_M = 0.020 # lip feather reaches this far above the skin sole
TEX_SIZE = 1024 # albedo + mask resolution (painted laces need >512)
NOISE_SEED = 3089
# Convex toe box (shared base.convex_toe_box) — chunky work-boot cap.
TOE_EXT_M = 0.010 # nose extension past the longest toe (m)
TOE_WMARGIN_M = 0.006 # half-width padding (chunky)
TOE_HCLEAR_M = 0.009 # vertical headroom above the toes (flex room)
TOE_FEATHER_M = 0.020 # blend band behind the ball
# Painted-detail metrics (metres on average_m; scaled by the calf-span ratio).
LACE_PITCH_M = 0.017 # vertical distance between lace bars
LACE_BAR_HW_M = 0.0026 # half-height of a painted lace bar
LACE_PANEL_HW_M = 0.015 # half arc-width of the lace panel
EYELET_R_M = 0.0032 # eyelet dot radius (at the panel edges)
LACE_LO_FRAC = 0.0 # panel starts at the ankle joint (shaft only)
COLLAR_H_M = 0.014 # padded collar band at the shaft rim (albedo shade)
SOLE_TOP_M = 0.014 # R region reaches this far above the skin sole
WELT_OFF_M = 0.0035 # welt stitch line offset above the sole top
SEAM_HW_M = 0.0018 # painted stitch line half-width
TOECAP_STITCH = True # painted toe-cap stitch line at the ball joint
# Albedo tones (sRGB floats). Mid-luma leather so the toon_garment luma
# recolor keeps its dynamic range; hue defaults come from manifest tints.
LEATHER_RGB = (0.58, 0.44, 0.32) # light tan work leather
SOLE_RGB = (0.30, 0.29, 0.28) # darker rubber (low luma -> reads black)
LACE_RGB = (0.24, 0.21, 0.19) # dark laces
EYELET_RGB = (0.85, 0.78, 0.60) # bright metal eyelet glint
WELT_RGB = (0.78, 0.62, 0.40) # welt stitching thread
PANEL_SHADE = 0.90 # lace-panel background darkening
COLLAR_SHADE = 0.86 # padded collar darkening
ALBEDO_NOISE = 0.020 # +/- grain jitter
PLAIN = False # --plain: skip laces/eyelets/welt paint
# Reference proportions (average_m) the scale ratio is anchored to.
_REF_CALF_SPAN = 0.4559 # calf head z (0.5424) - calf tail z (0.0865)
# --------------------------------------------------------------------------
# Per-body landmarks
# --------------------------------------------------------------------------
class BootLandmarks:
"""Cut/mask/paint parameters from one body's calf bones + skin-sole plane."""
def __init__(self, armature, skin_min_z):
bones = armature.data.bones
calf_l = bones.get("calf_l")
calf_r = bones.get("calf_r")
if calf_l is None or calf_r is None:
raise RuntimeError("calf_l/calf_r missing — not the 65-bone rig?")
self.ankle_z = (calf_l.tail_local.z + calf_r.tail_local.z) / 2.0
self.knee_z = (calf_l.head_local.z + calf_r.head_local.z) / 2.0
self.skin_min_z = skin_min_z
self.calf_span = self.knee_z - self.ankle_z
# Detail scale: proportional to the calf span, clamped so small bodies
# keep believable (not clown, not doll) sole/lace metrics.
self.s = min(max(self.calf_span / _REF_CALF_SPAN, 0.55), 1.15)
# Per-z leg axis control points (ankle -> knee) for the +x leg;
# the right leg mirrors via the x sign. np.interp clamps outside.
self.leg_z_pts = np.array([calf_l.tail_local.z, calf_l.head_local.z])
self.leg_x_pts = np.array(
[abs(calf_l.tail_local.x), abs(calf_l.head_local.x)])
self.leg_y_pts = np.array([calf_l.tail_local.y, calf_l.head_local.y])
self.shaft_top_z = self.ankle_z + SHAFT_FRAC * self.calf_span
self.rim_z = self.shaft_top_z # finalized after rim flattening
# Ball joint (toe-box hinge): forward distance on the front axis.
ball = bones.get("ball_l")
self.ball_front = (ball.head_local.y * FRONT_Y_SIGN
if ball is not None else None)
def finalize(self, rim_z):
self.rim_z = rim_z
self.sole_top_z = self.skin_min_z + SOLE_TOP_M * self.s
self.lace_lo = self.ankle_z - LACE_LO_FRAC * (self.ankle_z - self.skin_min_z)
self.lace_hi = self.rim_z - 0.7 * COLLAR_H_M * self.s
log(f"landmarks: ankle={self.ankle_z:.3f} knee={self.knee_z:.3f} "
f"sole={self.skin_min_z:.3f} rim={self.rim_z:.3f} s={self.s:.2f} "
f"sole_top={self.sole_top_z:.3f} "
f"laces z=[{self.lace_lo:.3f},{self.lace_hi:.3f}]")
# --------------------------------------------------------------------------
# Geometry: shaft cut + rim flatten + flare + sole
# --------------------------------------------------------------------------
def shaft_cut(shell, lm):
"""Trim the calves above the boot-shaft plane (bone-derived)."""
bm = bmesh.new()
bm.from_mesh(shell.data)
doomed = [v for v in bm.verts if v.co.z > lm.shaft_top_z]
bmesh.ops.delete(bm, geom=doomed, context='VERTS')
bm.to_mesh(shell.data)
bm.free()
shell.data.update()
log(f"shaft cut at z={lm.shaft_top_z:.3f}: removed {len(doomed)} verts")
def flatten_shaft_rims(shell, lm):
"""Pull both shaft-rim teeth rings onto ONE shared clean plane (pre-offset).
The plane cut follows mesh topology, so each rim is a jagged ring of
teeth. Both rings flatten DOWN to the common valley (deepest notch of
either ring) — a clean straight rim, identical height on both boots, no
invented coverage. Any stray open-boundary verts below the shaft (weld
leftovers) are left alone and reported. Returns the rim plane z.
"""
me = shell.data
bm = bmesh.new()
bm.from_mesh(me)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
boundary = set()
for e in bm.edges:
if len(e.link_faces) == 1:
boundary.update(v.index for v in e.verts)
low_thresh = lm.ankle_z + 0.25 * (lm.shaft_top_z - lm.ankle_z)
rims = [i for i in boundary if bm.verts[i].co.z > low_thresh]
stray = len(boundary) - len(rims)
if not rims:
bm.free()
raise RuntimeError("no shaft rim boundary verts found")
valley = min(bm.verts[i].co.z for i in rims)
teeth = max(bm.verts[i].co.z for i in rims) - valley
for i in rims:
bm.verts[i].co.z = valley
bm.to_mesh(me)
bm.free()
me.update()
log(f"flattened shaft rims: {len(rims)} verts, teeth {teeth:.3f} m "
f"-> z={valley:.3f}"
+ (f" (WARNING: {stray} stray low boundary verts left)" if stray else ""))
return valley
def shaft_flare(shell, lm, rim_z, flare):
"""Extra RADIAL stand-off toward the shaft rim, feathered from the ankle
(post-offset, pre-solidify). Buys pant-hem clearance + chunky read."""
if flare <= 0.0:
return
span = max(rim_z - lm.ankle_z, 1e-6)
bm = bmesh.new()
bm.from_mesh(shell.data)
bm.normal_update()
n = 0
for v in bm.verts:
if v.co.z > lm.ankle_z:
t = min((v.co.z - lm.ankle_z) / span, 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"shaft flare: {n} verts, +{flare * 1000:.1f} mm radial at rim")
def sole_shape(shell, lm, offset, sole_drop):
"""Chunky sole (post-offset, pre-solidify): feathered radial LIP around
the sole band, then the under-foot offset surface flattened onto a slab
plane. Solidify(use_rim) then grows its thickness outward (downward
there), landing the shipped slab bottom exactly sole_drop below the skin
sole."""
lip_top = lm.skin_min_z + SOLE_LIP_TOP_M * lm.s
lip = SOLE_LIP_M * lm.s
slab_z = lm.skin_min_z - (sole_drop * lm.s - base.CLOTH_THICKNESS_M)
feather = max(lip_top - lm.skin_min_z, 1e-6)
bm = bmesh.new()
bm.from_mesh(shell.data)
bm.normal_update()
n_lip = 0
for v in bm.verts:
if v.co.z < lip_top:
t = min((lip_top - v.co.z) / feather, 1.0)
nx, ny = v.normal.x, v.normal.y
mag = (nx * nx + ny * ny) ** 0.5
if mag > 1e-6:
v.co.x += lip * t * nx / mag
v.co.y += lip * t * ny / mag
n_lip += 1
n_flat = 0
for v in bm.verts:
if v.co.z < lm.skin_min_z:
v.co.z = slab_z
n_flat += 1
bm.to_mesh(shell.data)
bm.free()
shell.data.update()
log(f"sole: lip +{lip * 1000:.1f} mm on {n_lip} verts, "
f"{n_flat} under-sole verts flattened -> z={slab_z:.3f} "
f"(shipped bottom {sole_drop * lm.s * 1000:.0f} mm below skin sole)")
def clamp_rim_residue(shell, rim_z):
"""Post-solidify safety clamp: verts still above the rim plane (solidify
displacement on multi-triangle teeth) get squashed onto it."""
me = shell.data
n = 0
for v in me.vertices:
if v.co.z > rim_z:
v.co.z = rim_z
n += 1
me.update()
if n:
log(f"clamped {n} residual rim verts -> {rim_z:.3f}")
# --------------------------------------------------------------------------
# Boot feature field (texel-level; drives albedo AND mask together)
# --------------------------------------------------------------------------
def _boot_field(px, py, pz, lm):
"""Evaluate boot features at texel 3D positions (numpy arrays)."""
s = lm.s
side = np.where(px >= 0.0, 1.0, -1.0)
cx = side * np.interp(pz, lm.leg_z_pts, lm.leg_x_pts)
cy = np.interp(pz, lm.leg_z_pts, lm.leg_y_pts)
dx = px - cx
dy = py - cy
r = np.hypot(dx, dy) + 1e-9
# Arc distance from the front meridian around the per-z leg axis —
# constant metric width at any girth, mirrors correctly on both boots.
arc_front = np.arccos(np.clip(dy * FRONT_Y_SIGN / r, -1.0, 1.0)) * r
in_sole = pz <= lm.sole_top_z
in_lace_z = (pz > lm.lace_lo) & (pz < lm.lace_hi) & ~in_sole
panel = in_lace_z & (arc_front < LACE_PANEL_HW_M * s)
pitch = LACE_PITCH_M * s
ph = np.mod(pz - lm.lace_lo, pitch)
laces = panel & (np.abs(ph - 0.5 * pitch) < LACE_BAR_HW_M * s)
eyelets = (
in_lace_z
& (np.abs(arc_front - LACE_PANEL_HW_M * s) < EYELET_R_M * s)
& (np.abs(ph - 0.5 * pitch) < EYELET_R_M * s)
)
welt = (~in_sole) & (
np.abs(pz - (lm.sole_top_z + WELT_OFF_M * s)) < SEAM_HW_M * s)
if TOECAP_STITCH and lm.ball_front is not None:
welt = welt | (
(~in_sole) & (pz < lm.lace_lo)
& (np.abs(py * FRONT_Y_SIGN - lm.ball_front) < SEAM_HW_M * s))
collar = (~in_sole) & (pz > lm.rim_z - COLLAR_H_M * s)
return in_sole, panel, laces, eyelets, welt, collar
def _paint_texels(px, py, pz, noise, lm):
"""Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions."""
n = px.shape[0]
in_sole, panel, laces, eyelets, welt, collar = _boot_field(px, py, pz, lm)
# --- albedo ------------------------------------------------------------
alb = np.empty((n, 4), dtype=np.float32)
for c in range(3):
alb[:, c] = LEATHER_RGB[c] + noise
alb[:, 3] = 1.0
alb[collar, :3] *= COLLAR_SHADE
if not PLAIN:
alb[panel, :3] *= PANEL_SHADE
for c in range(3):
alb[welt, c] = WELT_RGB[c]
for c in range(3):
alb[in_sole, c] = SOLE_RGB[c] + noise[in_sole]
if not PLAIN:
for c in range(3):
alb[laces, c] = LACE_RGB[c]
alb[eyelets, c] = EYELET_RGB[c]
# --- region mask: sole R / upper+shaft G / laces+eyelets B ---------------
mask = np.zeros((n, 4), dtype=np.float32)
is_b = (laces | eyelets) & ~in_sole if not PLAIN else np.zeros(n, dtype=bool)
is_g = ~(in_sole | is_b)
mask[in_sole, 0] = 1.0
mask[is_b, 2] = 1.0
mask[is_g, 1] = 1.0
return alb, mask
# --------------------------------------------------------------------------
# UV0 rasterization (denim technique: one pass paints albedo + 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 boot 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"boot_albedo_{body}", albedo_path)
_save(mask_buf, f"boot_mask_{body}", mask_path)
log(f"saved albedo -> {albedo_path}")
log(f"saved mask -> {mask_path}")
return albedo_img
# --------------------------------------------------------------------------
# Per-body authoring
# --------------------------------------------------------------------------
def author_boot_shell(body_dir, out_dir, body, offset, sole_drop):
base.clear_scene()
base.COVERED_SEGMENTS = COVERED_SEGMENTS
shell, armature = base.build_covered_mesh(body_dir)
denim.weld_boundaries(shell) # ankle seams: foot <-> leg_lower
skin_min_z = min(v.co.z for v in shell.data.vertices)
lm = BootLandmarks(armature, skin_min_z)
shaft_cut(shell, lm)
rim_z = flatten_shaft_rims(shell, lm)
# Smooth convex toe box (replaces the toe-merge + skin-conforming
# containment clamp + per-toe weight re-bind, which re-imprinted the
# individual toes and rippled under flex). The cap encloses the real skin
# toes and rebinds uniformly to the ball bone; offset then adds standoff.
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)
shaft_flare(shell, lm, rim_z, SHAFT_FLARE_M * lm.s)
sole_shape(shell, lm, offset, sole_drop)
base.solidify(shell, base.CLOTH_THICKNESS_M)
clamp_rim_residue(shell, rim_z)
lm.finalize(rim_z)
denim.author_parked_uv2(shell) # boots 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 SHAFT_FRAC, SHAFT_FLARE_M, LEATHER_RGB, PLAIN
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] [--shaft-frac F] [--shaft-flare M] "
"[--sole-drop M] [--base-rgb r,g,b] [--plain]")
sys.exit(1)
bodies_root = argv[0]
out_dir = argv[1]
offset = BOOT_OFFSET_M
sole_drop = SOLE_DROP_M
if "--offset" in argv:
offset = float(argv[argv.index("--offset") + 1])
if "--shaft-frac" in argv:
SHAFT_FRAC = float(argv[argv.index("--shaft-frac") + 1])
if "--shaft-flare" in argv:
SHAFT_FLARE_M = float(argv[argv.index("--shaft-flare") + 1])
if "--sole-drop" in argv:
sole_drop = float(argv[argv.index("--sole-drop") + 1])
if "--base-rgb" in argv:
LEATHER_RGB = tuple(
float(v) for v in argv[argv.index("--base-rgb") + 1].split(","))
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"boots per-body mode: {len(bodies)} bodies, offset "
f"{offset * 1000:.0f} mm, shaft-frac {SHAFT_FRAC}, "
f"sole-drop {sole_drop * 1000:.0f} mm, 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_boot_shell(body_dir, out_dir, body, offset, sole_drop)
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()