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>
754 lines
33 KiB
Python
754 lines
33 KiB
Python
"""
|
|
blender_author_sneakers.py (T-1089 wave 2, sneakers_modern + trainer family)
|
|
|
|
Authors low-top TRAINERS as per-body offset shells over BOTH feet (seg_foot_l +
|
|
seg_foot_r joined into ONE garment — the peasant_shoes both-feet convention),
|
|
reusing blender_author_offset_shell.py as a library (scene build, join, offset,
|
|
solidify, GLB export) and blender_author_denim_pants.py's boundary weld.
|
|
|
|
What footwear needs beyond the bottoms family, as reusable parameters:
|
|
|
|
* boundary WELD first (denim practice) — the raw foot segment is NOT one
|
|
surface: the sole is a separate coincident-vert patch and the ankle cut
|
|
leaves floating shards (probe: average_m = 4 islands, muscular_m/child = 8).
|
|
remove_doubles at 0.5 mm fuses everything into one watertight-except-ankle
|
|
shell per foot; weights identical by origin, so skinning is unaffected.
|
|
* COLLAR cut + rim flatten — the segment splitter's ankle boundary is jagged
|
|
weight-threshold teeth (3.3-8.3 cm across bodies, back-biased). Verts above
|
|
the collar plane (a fraction of the ankle-joint height) are deleted, then
|
|
every remaining boundary vert is pulled ONTO the collar plane — a clean
|
|
horizontal low-top opening. Re-flattened after the outward offset because
|
|
rim-vert normals have a +z bias that would lift the rim.
|
|
* SOLE slab (this family's new offset-shell param) — the rim-flatten practice
|
|
applied to the ground plane, built as cut + flatten + extrude + fill: the
|
|
shell's underside band is cut away (with the toe-knuckle lobes that survive
|
|
smoothing), the open rim is flattened onto the cut plane and its outline
|
|
relaxed, then extruded straight down to a plane `--sole-drop` (scaled per
|
|
body) below the body's own foot-bottom and closed with a flat bottom — a
|
|
clean prism slab; Solidify thickens it and a final clamp guarantees the
|
|
outer sole is planar. (Snapping the band in place instead collapses mesh
|
|
rows into crumpled slivers — melted-wax scallops on the probe renders.)
|
|
* TOE-BOX SMOOTHING + ROUNDING — the body feet have INDIVIDUAL TOES; a raw
|
|
offset shell reads as a foot-shaped slipper (verified on average_m). Heavy
|
|
iterative vertex smoothing over the toe region (feathered, boundary rim
|
|
pinned) melts the toe creases into one volume, a light global pass
|
|
de-lumps the ankle anatomy, then an extra normal-along inflation feathered
|
|
toward the toe tip restores the lost volume as a rounded sneaker toe box.
|
|
* COLLAR FLARE — small feathered radial stand-off at the opening (the jeans
|
|
waist-flare practice) for ankle-flex clearance under Walk/Crouch.
|
|
* TEXEL-level feature painting (denim practice): one analytic field drives
|
|
BOTH the albedo and the region mask, so they always agree:
|
|
- albedo: painted lace cross-straps over a darker tongue panel, toe cap
|
|
+ border line, foxing stripe at the sole top, heel tab, collar band,
|
|
vamp + heel-counter panel lines (swoosh-free — no brand marks).
|
|
Everyday default: white/grey, flat toon-friendly tones.
|
|
- mask: sole -> R, upper -> G, laces + trim (collar band, toe cap,
|
|
heel tab) -> B (spec: sole=R, upper=G, laces+trim=B).
|
|
* a parked logo_uv TEXCOORD_1 layer (all UVs at (2,2)) — not logo-capable,
|
|
but toon_garment.gdshader samples UV2 unconditionally.
|
|
|
|
All parameters derive PER BODY from that body's own bone landmarks (foot_l /
|
|
ball_l) and measured mesh extents (foot length, half-width, ground plane),
|
|
scaled by foot length against the hand-calibrated average_m reference — the
|
|
same proportional-ratio philosophy as base.derive_thresholds. Per-body mode
|
|
only (offset shells author per body, Q-060). Left/right feet share body-atlas
|
|
UV space whose islands may overlap; every painted feature is |x|-mirror
|
|
symmetric, so overlapping texels agree by construction.
|
|
|
|
Usage (sneakers_modern reference invocation):
|
|
tooling/blender --background --python \
|
|
tooling/garment-fit/blender_author_sneakers.py -- \
|
|
client/assets/characters/bodies \
|
|
client/assets/characters/clothing/sneakers_modern \
|
|
[--bodies average_m,child,...] [--offset 0.009] [--sole-drop 0.015] \
|
|
[--sole-snap-frac 0.55] [--collar-frac 0.95] [--collar-flare 0.002] \
|
|
[--toe-round 0.004] [--toe-smooth 12] [--laces 4] [--plain]
|
|
|
|
Writes per body: <out_dir>/<body>.glb (skinned, albedo embedded)
|
|
<out_dir>/<body>_mask.png (RGBA region mask, UV0)
|
|
<out_dir>/<body>_base_albedo.png
|
|
Plus: <out_dir>/base_albedo.png (average_m's, shared sidecar)
|
|
<out_dir>/reference_mask.png (average_m's, runtime fallback)
|
|
|
|
Decisions: D-162 (clothing pre-fitted per body type), D-251 (in-house
|
|
wardrobe), Q-060 (per-body offset shells).
|
|
"""
|
|
|
|
import importlib.util
|
|
import os
|
|
import shutil
|
|
import sys
|
|
|
|
import bmesh
|
|
import bpy
|
|
import numpy as np
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Import the base offset-shell module + the denim module (weld utility)
|
|
# --------------------------------------------------------------------------
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def _load(mod_name, file_name):
|
|
spec = importlib.util.spec_from_file_location(
|
|
mod_name, os.path.join(_HERE, file_name))
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
base = _load("offset_shell_base", "blender_author_offset_shell.py")
|
|
denim = _load("denim_pants_lib", "blender_author_denim_pants.py")
|
|
|
|
log = base.log
|
|
FRONT_Y_SIGN = base.FRONT_Y_SIGN # bodies face -Y (toes point -Y)
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Parameters (CLI-overridable ones are module globals)
|
|
# --------------------------------------------------------------------------
|
|
COVERED_SEGMENTS = ["seg_foot_l", "seg_foot_r"]
|
|
|
|
OFFSET_M = 0.009 # shoe standoff — snugger than cloth (12 mm)
|
|
SOLE_DROP_M = 0.015 # sole slab depth below the body's own foot bottom
|
|
# (scaled by foot length per body)
|
|
SOLE_SNAP_FRAC = 0.55 # sole CUT height as a fraction of the sole rise
|
|
# (ground -> foxing top): everything below is cut
|
|
# away and rebuilt as an extruded prism slab (see
|
|
# build_sole_slab) — kills the toe-knuckle underside
|
|
# lobes that survive smoothing (probe, average_m)
|
|
COLLAR_FRAC = 0.95 # collar plane as fraction of ankle-joint height
|
|
COLLAR_FLARE_M = 0.002 # radial stand-off at the opening (ankle-flex room)
|
|
SMOOTH_GLOBAL_ITERS = 2 # light instep/ankle de-lumping passes (behind ball;
|
|
# the toe box is built analytically, not smoothed)
|
|
SMOOTH_FACTOR = 0.5
|
|
|
|
# Convex toe box (shared base.convex_toe_box) — rounded, roomy trainer cap.
|
|
TOE_EXT_M = 0.012 # nose extension past the longest toe (m)
|
|
TOE_WMARGIN_M = 0.006 # half-width padding around the widest toe (roomy)
|
|
TOE_HCLEAR_M = 0.010 # vertical headroom above the toes (flex room)
|
|
TOE_FEATHER_M = 0.022 # blend band behind the ball
|
|
N_LACES = 4 # painted cross-straps
|
|
PLAIN = False # --plain: flat upper, no painted features
|
|
TEX_SIZE = 1024 # painted lace/panel lines need > 512
|
|
NOISE_SEED = 3089
|
|
|
|
# Vertical proportions — fractions of the collar height above the ground.
|
|
SOLE_TOP_FRAC = 0.30 # sole sidewall (foxing) top -> R region below this
|
|
COLLAR_BAND_FRAC = 0.15 # collar trim band height (B region)
|
|
VAMP_LINE_FRAC = 0.42 # side panel line height between sole top and collar
|
|
|
|
# Foot-axis proportions — fractions of foot length / half-width.
|
|
TOE_CAP_FRAC = 0.18 # toe cap depth from the toe tip (B region)
|
|
HEEL_LINE_FRAC = 0.22 # heel-counter panel line from the heel tip
|
|
LACE_T0, LACE_T1 = 0.18, 0.80 # lace panel span along the foot bone
|
|
LACE_HALFW_FRAC = 0.40 # lace panel half-width, of foot half-width
|
|
LACE_STRIPE_DUTY = 0.44 # stripe thickness as a fraction of stripe spacing
|
|
LINE_W_M = 0.0035 # painted panel/border line half-width (scaled)
|
|
HEEL_TAB_HALFW_M = 0.012 # heel tab half-width (scaled)
|
|
|
|
# Everyday default: white/grey, flat toon-friendly tones (sRGB floats).
|
|
UPPER_RGB = (0.880, 0.880, 0.890)
|
|
SOLE_RGB = (0.780, 0.790, 0.800)
|
|
TREAD_RGB = (0.450, 0.460, 0.480) # below-ground outsole
|
|
TOE_RGB = (0.920, 0.920, 0.930) # toe bumper
|
|
LACE_RGB = (0.960, 0.960, 0.965)
|
|
TONGUE_SHADE = 0.90 # lace-zone panel behind the straps
|
|
COLLAR_SHADE = 0.88 # collar band darkening
|
|
HEEL_TAB_SHADE = 0.72
|
|
LINE_SHADE = 0.74 # painted panel/border lines
|
|
ALBEDO_NOISE = 0.015
|
|
|
|
# Reference proportions (average_m) the fractions were calibrated against.
|
|
_REF_FOOT_LEN = 0.2704 # heel y (0.1374) - toe tip y (-0.1330)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Per-body landmarks
|
|
# --------------------------------------------------------------------------
|
|
|
|
class FootLandmarks:
|
|
"""Cut/mask/paint parameters from one body's foot bones + measured mesh."""
|
|
|
|
def __init__(self, armature, shell):
|
|
bones = armature.data.bones
|
|
foot = bones.get("foot_l")
|
|
ball = bones.get("ball_l")
|
|
if foot is None or ball is None:
|
|
raise RuntimeError("foot_l/ball_l missing — not the 65-bone rig?")
|
|
# Bone landmarks (left foot; the right mirrors via |x|).
|
|
self.ankle_y = foot.head_local.y
|
|
self.ankle_z = foot.head_local.z
|
|
self.ball_y = foot.tail_local.y
|
|
self.ball_z = foot.tail_local.z
|
|
self.toe_y = ball.tail_local.y
|
|
# Measured mesh extents (left-foot verts; feet are x-mirror symmetric).
|
|
lx = [v.co.x for v in shell.data.vertices if v.co.x > 0.0]
|
|
ly = [v.co.y for v in shell.data.vertices if v.co.x > 0.0]
|
|
zs = [v.co.z for v in shell.data.vertices]
|
|
self.foot_cx = (min(lx) + max(lx)) / 2.0
|
|
self.half_w = (max(lx) - min(lx)) / 2.0
|
|
self.heel_y = max(ly)
|
|
self.toe_tip_y = min(ly)
|
|
self.ground_z = min(zs)
|
|
self.foot_len = self.heel_y - self.toe_tip_y
|
|
self.s = self.foot_len / _REF_FOOT_LEN
|
|
|
|
self.collar_z = self.ground_z + COLLAR_FRAC * (self.ankle_z - self.ground_z)
|
|
self.sole_drop = SOLE_DROP_M * self.s
|
|
self.sole_bottom = self.ground_z - self.sole_drop
|
|
rise = self.collar_z - self.ground_z
|
|
self.sole_top = self.ground_z + SOLE_TOP_FRAC * rise
|
|
self.band_h = COLLAR_BAND_FRAC * rise
|
|
self.vamp_z = self.sole_top + VAMP_LINE_FRAC * (self.collar_z - self.sole_top)
|
|
self.cap_y = self.toe_tip_y + TOE_CAP_FRAC * self.foot_len
|
|
self.heel_line_y = self.heel_y - HEEL_LINE_FRAC * self.foot_len
|
|
self.lace_halfw = LACE_HALFW_FRAC * self.half_w
|
|
self.line_w = LINE_W_M * self.s
|
|
log(f"landmarks: ankle_z={self.ankle_z:.4f} collar_z={self.collar_z:.4f} "
|
|
f"ground={self.ground_z:.4f} sole_bottom={self.sole_bottom:.4f} "
|
|
f"sole_top={self.sole_top:.4f} foot_len={self.foot_len:.4f} "
|
|
f"half_w={self.half_w:.4f} cx={self.foot_cx:.4f} s={self.s:.3f}")
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Geometry: collar cut + flatten, sole slab, toe round, collar flare
|
|
# --------------------------------------------------------------------------
|
|
|
|
def make_normals_consistent(shell):
|
|
"""Outward-consistent normals BEFORE the offset — the raw foot's sole patch
|
|
winds independently of the upper, so post-weld normals need one recalc or
|
|
the offset would pull the sole inward."""
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
shell.select_set(True)
|
|
bpy.context.view_layer.objects.active = shell
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|
bpy.ops.mesh.normals_make_consistent(inside=False)
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
log("recalculated outward-consistent normals")
|
|
|
|
|
|
def collar_cut(shell, collar_z):
|
|
"""Delete verts above the collar plane (bone-plane-cut practice)."""
|
|
bm = bmesh.new()
|
|
bm.from_mesh(shell.data)
|
|
doomed = [v for v in bm.verts if v.co.z > collar_z]
|
|
bmesh.ops.delete(bm, geom=doomed, context='VERTS')
|
|
bm.to_mesh(shell.data)
|
|
bm.free()
|
|
shell.data.update()
|
|
log(f"collar cut at z={collar_z:.4f}: removed {len(doomed)} verts")
|
|
|
|
|
|
def flatten_collar_rims(shell, collar_z, label):
|
|
"""Pull every open-boundary vert ONTO the collar plane (rim-flatten
|
|
practice). After the weld + collar cut the only open boundary is the two
|
|
collar rims, so a single pass flattens both feet."""
|
|
me = shell.data
|
|
bm = bmesh.new()
|
|
bm.from_mesh(me)
|
|
bm.verts.ensure_lookup_table()
|
|
boundary = set()
|
|
for e in bm.edges:
|
|
if len(e.link_faces) == 1:
|
|
boundary.update(v.index for v in e.verts)
|
|
if not boundary:
|
|
bm.free()
|
|
raise RuntimeError("no open collar boundary found after cut")
|
|
zs = [bm.verts[i].co.z for i in boundary]
|
|
for i in boundary:
|
|
bm.verts[i].co.z = collar_z
|
|
bm.to_mesh(me)
|
|
bm.free()
|
|
me.update()
|
|
log(f"flattened collar rims ({label}): {len(boundary)} verts, "
|
|
f"z {min(zs):.4f}..{max(zs):.4f} -> {collar_z:.4f}")
|
|
|
|
|
|
def smooth_shell(shell, lm):
|
|
"""De-lump the instep/ankle anatomy only (BEHIND the ball joint): light
|
|
iterative vertex smoothing with the open collar boundary pinned. The toe
|
|
box itself is built analytically by base.convex_toe_box, so the toe zone
|
|
(forward of the ball) is deliberately excluded here — smoothing it would
|
|
shrink the toes the toe box must still enclose."""
|
|
me = shell.data
|
|
bm = bmesh.new()
|
|
bm.from_mesh(me)
|
|
bm.verts.ensure_lookup_table()
|
|
boundary = set()
|
|
for e in bm.edges:
|
|
if len(e.link_faces) == 1:
|
|
boundary.update(v.index for v in e.verts)
|
|
ball_u = lm.ball_y * FRONT_Y_SIGN
|
|
# interior verts BEHIND the ball joint (u < ball_u): instep, arch, heel.
|
|
heel = [v for v in bm.verts if v.index not in boundary
|
|
and (v.co.y * FRONT_Y_SIGN) < ball_u]
|
|
for _ in range(SMOOTH_GLOBAL_ITERS):
|
|
bmesh.ops.smooth_vert(bm, verts=heel, factor=SMOOTH_FACTOR,
|
|
use_axis_x=True, use_axis_y=True, use_axis_z=True)
|
|
bm.to_mesh(me)
|
|
bm.free()
|
|
me.update()
|
|
log(f"smoothed instep/heel: {SMOOTH_GLOBAL_ITERS} passes "
|
|
f"({len(heel)} verts behind ball u<{ball_u:.4f})")
|
|
|
|
|
|
RIM_RELAX_ITERS = 3 # along-ring XY relaxation of the cut rim outline
|
|
|
|
|
|
def build_sole_slab(shell, lm):
|
|
"""Rim-flatten practice applied to the GROUND plane, done PROPERLY as a
|
|
cut + flatten + extrude (snapping a whole z-band onto the plane collapses
|
|
multiple mesh rows into crumpled slivers that read as melted-wax scallops
|
|
— probe renders v3-v5):
|
|
|
|
1. DELETE everything below the cut height (SOLE_SNAP_FRAC of the sole
|
|
rise) — removes the toe-knuckle underside lobes outright.
|
|
2. RIM-FLATTEN the resulting open bottom boundary onto the cut plane
|
|
(exactly the denim ankle practice), then relax the ring outline in
|
|
XY along the ring only — a smooth footprint curve, rounded toe.
|
|
3. EXTRUDE the ring straight down to the sole plane — a clean vertical
|
|
prism wall (extruded verts inherit the rim verts' deform weights,
|
|
so the sole still flexes at the ball joint).
|
|
4. FILL the bottom ring with faces — a closed flat underside.
|
|
|
|
Solidify then grows the outer surface CLOTH_THICKNESS_M further down,
|
|
landing the visible outsole on lm.sole_bottom (guaranteed by
|
|
clamp_residue after solidify)."""
|
|
plane = lm.sole_bottom + base.CLOTH_THICKNESS_M
|
|
hi = lm.ground_z + SOLE_SNAP_FRAC * (lm.sole_top - lm.ground_z)
|
|
me = shell.data
|
|
bm = bmesh.new()
|
|
bm.from_mesh(me)
|
|
|
|
# 1. cut
|
|
doomed = [v for v in bm.verts if v.co.z < hi]
|
|
bmesh.ops.delete(bm, geom=doomed, context='VERTS')
|
|
|
|
# 2. rim-flatten (the collar rim is also open — split boundaries by z)
|
|
boundary = {v for e in bm.edges if len(e.link_faces) == 1
|
|
for v in e.verts}
|
|
z_mid = (lm.collar_z + hi) / 2.0
|
|
sole_rim = {v for v in boundary if v.co.z < z_mid}
|
|
if not sole_rim:
|
|
bm.free()
|
|
raise RuntimeError("no sole rim found after cut — check cut height")
|
|
for v in sole_rim:
|
|
v.co.z = hi
|
|
# along-ring XY relax: average each rim vert with its ring neighbours
|
|
# only (bmesh smooth_vert would pull toward the upper rows and shrink)
|
|
for _ in range(RIM_RELAX_ITERS):
|
|
new_pos = {}
|
|
for v in sole_rim:
|
|
ring_nbrs = [e.other_vert(v) for e in v.link_edges
|
|
if len(e.link_faces) == 1
|
|
and e.other_vert(v) in sole_rim]
|
|
if len(ring_nbrs) >= 2:
|
|
ax = sum(n.co.x for n in ring_nbrs) / len(ring_nbrs)
|
|
ay = sum(n.co.y for n in ring_nbrs) / len(ring_nbrs)
|
|
new_pos[v] = (v.co.x + 0.5 * (ax - v.co.x),
|
|
v.co.y + 0.5 * (ay - v.co.y))
|
|
for v, (x, y) in new_pos.items():
|
|
v.co.x = x
|
|
v.co.y = y
|
|
|
|
# 3. extrude the rim edges straight down to the sole plane
|
|
rim_edges = [e for e in bm.edges if len(e.link_faces) == 1
|
|
and e.verts[0] in sole_rim and e.verts[1] in sole_rim]
|
|
ret = bmesh.ops.extrude_edge_only(bm, edges=rim_edges)
|
|
new_verts = [g for g in ret["geom"]
|
|
if isinstance(g, bmesh.types.BMVert)]
|
|
for v in new_verts:
|
|
v.co.z = plane
|
|
|
|
# 4. close the bottom
|
|
bottom_edges = [e for e in bm.edges if len(e.link_faces) == 1
|
|
and all(abs(v.co.z - plane) < 1e-6 for v in e.verts)]
|
|
filled = bmesh.ops.holes_fill(bm, edges=bottom_edges, sides=0)
|
|
|
|
# 5. UV-park the new faces. The fill n-gon's default loop UVs span the
|
|
# whole enclosed UV region — its texel bake overwrites painted islands
|
|
# (probe v6: mottled patches, features erased); wall quads' copied UVs
|
|
# sit ON the island boundary where bilinear sampling picks up
|
|
# background. ONE park point per foot (per-face points sample texels of
|
|
# varying paint state and stripe the wall — probe v7): all new faces on
|
|
# a side park on the UV centre of that side's LARGEST-UV-area
|
|
# rim-adjacent face — big enough that the bake reliably rasterizes its
|
|
# interior, and its centre lies in the sole band (z <= sole_top), so
|
|
# the sampled texel is the flat sole tone with an R-region mask.
|
|
uv_layer = bm.loops.layers.uv[0] if len(bm.loops.layers.uv) else None
|
|
if uv_layer is not None:
|
|
new_faces = [g for g in ret["geom"]
|
|
if isinstance(g, bmesh.types.BMFace)]
|
|
new_faces += list(filled["faces"])
|
|
new_face_set = set(new_faces)
|
|
best = {} # x-sign side -> (uv_area, centre uv); feet never cross x=0
|
|
for v in sole_rim:
|
|
side = 1 if v.co.x >= 0.0 else -1
|
|
for loop in v.link_loops:
|
|
f = loop.face
|
|
if f in new_face_set:
|
|
continue
|
|
us = [lp[uv_layer].uv for lp in f.loops]
|
|
area = 0.0
|
|
for i in range(len(us)):
|
|
j = (i + 1) % len(us)
|
|
area += us[i].x * us[j].y - us[j].x * us[i].y
|
|
area = abs(area) * 0.5
|
|
if side not in best or area > best[side][0]:
|
|
best[side] = (area,
|
|
(sum(u.x for u in us) / len(us),
|
|
sum(u.y for u in us) / len(us)))
|
|
for f in new_faces:
|
|
side = 1 if sum(v.co.x for v in f.verts) >= 0.0 else -1
|
|
if side not in best:
|
|
side = -side
|
|
uv = best[side][1]
|
|
for loop in f.loops:
|
|
loop[uv_layer].uv = uv
|
|
|
|
bm.to_mesh(me)
|
|
bm.free()
|
|
me.update()
|
|
log(f"sole slab: cut {len(doomed)} verts (z < {hi:.4f}), rim "
|
|
f"{len(sole_rim)} verts -> z={hi:.4f}, extruded {len(new_verts)} "
|
|
f"verts -> z={plane:.4f}, filled {len(filled['faces'])} bottom faces")
|
|
|
|
|
|
def collar_flare(shell, lm, flare):
|
|
"""Feathered radial stand-off at the opening (waist-flare practice) —
|
|
ankle-flex clearance under Walk/Crouch."""
|
|
if flare <= 0.0:
|
|
return
|
|
z0 = lm.collar_z - 2.0 * lm.band_h
|
|
bm = bmesh.new()
|
|
bm.from_mesh(shell.data)
|
|
bm.normal_update()
|
|
n = 0
|
|
for v in bm.verts:
|
|
if v.co.z > z0:
|
|
t = min((v.co.z - z0) / (2.0 * lm.band_h), 1.0)
|
|
nx, ny = v.normal.x, v.normal.y
|
|
mag = (nx * nx + ny * ny) ** 0.5
|
|
if mag > 1e-6:
|
|
v.co.x += flare * t * nx / mag
|
|
v.co.y += flare * t * ny / mag
|
|
n += 1
|
|
bm.to_mesh(shell.data)
|
|
bm.free()
|
|
shell.data.update()
|
|
log(f"collar flare: {n} verts, +{flare * 1000:.1f} mm radial at rim")
|
|
|
|
|
|
def clamp_residue(shell, lm):
|
|
"""Post-solidify safety clamps: the rim cap can push verts above the
|
|
collar plane, and blended edge normals leave the outsole slightly uneven —
|
|
squash both back onto their planes."""
|
|
me = shell.data
|
|
n_top = n_bot = 0
|
|
for v in me.vertices:
|
|
if v.co.z > lm.collar_z:
|
|
v.co.z = lm.collar_z
|
|
n_top += 1
|
|
elif v.co.z < lm.sole_bottom:
|
|
v.co.z = lm.sole_bottom
|
|
n_bot += 1
|
|
me.update()
|
|
log(f"clamped residue: {n_top} collar verts -> {lm.collar_z:.4f}, "
|
|
f"{n_bot} sole verts -> {lm.sole_bottom:.4f}")
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Sneaker feature field (texel-level; drives albedo AND mask together).
|
|
# Every feature is |x|-mirror symmetric so the (possibly overlapping)
|
|
# left/right UV islands paint identical values.
|
|
# --------------------------------------------------------------------------
|
|
|
|
def _paint_texels(px, py, pz, noise, lm):
|
|
"""Return (albedo (N,4), mask (N,4)) float32 arrays for texel positions."""
|
|
n = px.shape[0]
|
|
side = np.where(px >= 0.0, 1.0, -1.0)
|
|
dx = px - side * lm.foot_cx # signed offset from the foot centre
|
|
adx = np.abs(dx)
|
|
|
|
in_sole = pz <= lm.sole_top
|
|
# Tread = the flat underside plane only (never visible from the side).
|
|
# The sole SIDE WALL is snapped geometry whose UV triangles are stretched
|
|
# slivers — any tonal variation there (noise, a dark tread band) smears
|
|
# into vertical streaks under bilinear magnification, so the whole band
|
|
# above the underside is painted ONE flat tone.
|
|
in_tread = pz <= lm.sole_bottom + 0.0015
|
|
in_band = (pz >= lm.collar_z - lm.band_h) & ~in_sole
|
|
|
|
# Lace panel: param t along the foot bone (ankle head -> ball tail) in
|
|
# the (y,z) plane; texels above the bone line, near the centreline.
|
|
dy_ax = lm.ball_y - lm.ankle_y
|
|
dz_ax = lm.ball_z - lm.ankle_z
|
|
l2 = dy_ax * dy_ax + dz_ax * dz_ax
|
|
t = ((py - lm.ankle_y) * dy_ax + (pz - lm.ankle_z) * dz_ax) / l2
|
|
above_bone = pz > (lm.ankle_z + t * dz_ax + 0.002 * lm.s)
|
|
in_tongue = (~in_sole & above_bone & (adx < lm.lace_halfw)
|
|
& (t >= LACE_T0) & (t <= LACE_T1))
|
|
frac = (t - LACE_T0) / (LACE_T1 - LACE_T0)
|
|
stripe_pos = frac * N_LACES
|
|
stripe_d = np.abs(stripe_pos - (np.floor(stripe_pos) + 0.5))
|
|
laces = in_tongue & (stripe_d < 0.5 * LACE_STRIPE_DUTY)
|
|
|
|
toe_cap = (py < lm.cap_y) & ~in_sole
|
|
heel_tab = ((adx < HEEL_TAB_HALFW_M * lm.s) & ~in_sole
|
|
& (py > lm.ankle_y + 0.55 * (lm.heel_y - lm.ankle_y)))
|
|
|
|
# --- albedo -------------------------------------------------------------
|
|
alb = np.empty((n, 4), dtype=np.float32)
|
|
for c in range(3):
|
|
alb[:, c] = UPPER_RGB[c] + noise
|
|
alb[:, 3] = 1.0
|
|
for c in range(3):
|
|
alb[in_sole, c] = SOLE_RGB[c] # flat, noise-free (sliver UVs)
|
|
alb[in_tread, c] = TREAD_RGB[c] # underside plane only
|
|
|
|
if not PLAIN:
|
|
for c in range(3):
|
|
alb[toe_cap & ~in_sole, c] = TOE_RGB[c] + noise[toe_cap & ~in_sole]
|
|
alb[in_tongue, :3] *= TONGUE_SHADE
|
|
for c in range(3):
|
|
alb[laces, c] = LACE_RGB[c] + noise[laces]
|
|
alb[in_band & ~laces, :3] *= COLLAR_SHADE
|
|
alb[heel_tab & ~in_band, :3] *= HEEL_TAB_SHADE
|
|
|
|
# Painted panel lines (albedo only — swoosh-free).
|
|
foxing = np.abs(pz - lm.sole_top) < lm.line_w
|
|
cap_border = (np.abs(py - lm.cap_y) < lm.line_w) & ~in_sole
|
|
vamp = ((np.abs(pz - lm.vamp_z) < lm.line_w) & ~in_sole
|
|
& (adx > lm.lace_halfw * 0.8)
|
|
& (py > lm.cap_y) & (py < lm.heel_line_y))
|
|
heel_ctr = ((np.abs(py - lm.heel_line_y) < lm.line_w) & ~in_sole
|
|
& (pz < lm.collar_z - lm.band_h))
|
|
lines = (foxing | cap_border | vamp | heel_ctr) & ~laces
|
|
alb[lines, :3] *= LINE_SHADE
|
|
|
|
# --- region mask: sole R / upper G / laces+trim B -------------------------
|
|
mask = np.zeros((n, 4), dtype=np.float32)
|
|
is_b = (laces | toe_cap | heel_tab | in_band) & ~in_sole
|
|
mask[in_sole, 0] = 1.0
|
|
mask[is_b, 2] = 1.0
|
|
mask[~(in_sole | is_b), 1] = 1.0
|
|
return alb, mask
|
|
|
|
|
|
def _raster_tri_paint(alb_buf, mask_buf, noise_buf, uvs, cos, lm, W, H):
|
|
"""Barycentric texel fill of one UV triangle: interpolate 3D positions,
|
|
evaluate the sneaker field, write albedo + mask together."""
|
|
a, b, c = uvs
|
|
A, B, C = cos
|
|
ax, ay = a.x * (W - 1), a.y * (H - 1)
|
|
bx, by = b.x * (W - 1), b.y * (H - 1)
|
|
cx, cy = c.x * (W - 1), c.y * (H - 1)
|
|
minx = max(int(np.floor(min(ax, bx, cx))), 0)
|
|
maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1)
|
|
miny = max(int(np.floor(min(ay, by, cy))), 0)
|
|
maxy = min(int(np.ceil(max(ay, by, cy))), H - 1)
|
|
if minx > maxx or miny > maxy:
|
|
return
|
|
denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
|
|
if abs(denom) < 1e-9:
|
|
return
|
|
ys, xs = np.mgrid[miny:maxy + 1, minx:maxx + 1]
|
|
pxg = xs + 0.5
|
|
pyg = ys + 0.5
|
|
w0 = ((by - cy) * (pxg - cx) + (cx - bx) * (pyg - cy)) / denom
|
|
w1 = ((cy - ay) * (pxg - cx) + (ax - cx) * (pyg - cy)) / denom
|
|
w2 = 1.0 - w0 - w1
|
|
inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4)
|
|
if not inside.any():
|
|
return
|
|
w0i, w1i, w2i = w0[inside], w1[inside], w2[inside]
|
|
px3 = w0i * A.x + w1i * B.x + w2i * C.x
|
|
py3 = w0i * A.y + w1i * B.y + w2i * C.y
|
|
pz3 = w0i * A.z + w1i * B.z + w2i * C.z
|
|
ysin = ys[inside]
|
|
xsin = xs[inside]
|
|
alb, mask = _paint_texels(px3, py3, pz3, noise_buf[ysin, xsin], lm)
|
|
alb_buf[ysin, xsin] = alb
|
|
mask_buf[ysin, xsin] = mask
|
|
|
|
|
|
def paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body):
|
|
"""Rasterize all UV0 triangles once, producing the painted albedo and the
|
|
region mask from one shared feature-field evaluation per texel."""
|
|
W = H = TEX_SIZE
|
|
rng = np.random.default_rng(NOISE_SEED)
|
|
noise_buf = ((rng.random((H, W), dtype=np.float32) - 0.5)
|
|
* 2.0 * ALBEDO_NOISE)
|
|
|
|
alb_buf = np.empty((H, W, 4), dtype=np.float32)
|
|
for c in range(3):
|
|
alb_buf[:, :, c] = UPPER_RGB[c] + noise_buf
|
|
alb_buf[:, :, 3] = 1.0
|
|
mask_buf = np.zeros((H, W, 4), dtype=np.float32)
|
|
mask_buf[:, :, 1] = 1.0 # background = upper green (bleed-safe)
|
|
|
|
me = shell.data
|
|
bm = bmesh.new()
|
|
bm.from_mesh(me)
|
|
bm.faces.ensure_lookup_table()
|
|
if not len(bm.loops.layers.uv):
|
|
raise RuntimeError("no UV layer for albedo/mask paint")
|
|
uv_layer = bm.loops.layers.uv[0]
|
|
|
|
tri_count = 0
|
|
for face in bm.faces:
|
|
loops = face.loops[:]
|
|
uvs = [loop[uv_layer].uv.copy() for loop in loops]
|
|
cos = [loop.vert.co.copy() for loop in loops]
|
|
for i in range(1, len(uvs) - 1):
|
|
_raster_tri_paint(
|
|
alb_buf, mask_buf, noise_buf,
|
|
(uvs[0], uvs[i], uvs[i + 1]),
|
|
(cos[0], cos[i], cos[i + 1]),
|
|
lm, W, H)
|
|
tri_count += 1
|
|
bm.free()
|
|
counts = [float(mask_buf[:, :, c].sum()) for c in range(3)]
|
|
total = max(sum(counts), 1.0)
|
|
log(f"painted {tri_count} UV triangles ({W}x{H}); mask texels "
|
|
f"R={100 * counts[0] / total:.1f}% G={100 * counts[1] / total:.1f}% "
|
|
f"B={100 * counts[2] / total:.1f}%")
|
|
|
|
def _save(buf, name, path):
|
|
img = bpy.data.images.new(name, W, H, alpha=True)
|
|
img.pixels.foreach_set(buf.reshape(-1))
|
|
img.update()
|
|
img.filepath_raw = path
|
|
img.file_format = 'PNG'
|
|
img.save()
|
|
return img
|
|
|
|
albedo_img = _save(alb_buf, f"sneaker_albedo_{body}", albedo_path)
|
|
_save(mask_buf, f"sneaker_mask_{body}", mask_path)
|
|
log(f"saved albedo -> {albedo_path}")
|
|
log(f"saved mask -> {mask_path}")
|
|
return albedo_img
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Per-body authoring
|
|
# --------------------------------------------------------------------------
|
|
|
|
def author_sneaker_shell(body_dir, out_dir, body, offset):
|
|
base.clear_scene()
|
|
base.COVERED_SEGMENTS = COVERED_SEGMENTS
|
|
shell, armature = base.build_covered_mesh(body_dir)
|
|
denim.weld_boundaries(shell) # fuse sole patch + ankle shards
|
|
make_normals_consistent(shell)
|
|
|
|
lm = FootLandmarks(armature, shell)
|
|
|
|
collar_cut(shell, lm.collar_z)
|
|
# Convex toe box FIRST, on the raw skin foot (so it encloses the real
|
|
# toes), then de-lump only behind the ball.
|
|
base.convex_toe_box(
|
|
shell, armature,
|
|
extension=TOE_EXT_M * lm.s, width_margin=TOE_WMARGIN_M * lm.s,
|
|
height_clear=TOE_HCLEAR_M * lm.s, feather_m=TOE_FEATHER_M * lm.s)
|
|
smooth_shell(shell, lm)
|
|
flatten_collar_rims(shell, lm.collar_z, "pre-offset")
|
|
base.offset_outward(shell, offset)
|
|
flatten_collar_rims(shell, lm.collar_z, "post-offset") # rim normals lift it
|
|
build_sole_slab(shell, lm)
|
|
collar_flare(shell, lm, COLLAR_FLARE_M * lm.s)
|
|
base.solidify(shell, base.CLOTH_THICKNESS_M)
|
|
clamp_residue(shell, lm)
|
|
|
|
denim.author_parked_uv2(shell) # not logo-capable; shader needs UV2
|
|
|
|
albedo_path = os.path.join(out_dir, f"{body}_base_albedo.png")
|
|
mask_path = os.path.join(out_dir, f"{body}_mask.png")
|
|
albedo_img = paint_albedo_and_mask(shell, lm, albedo_path, mask_path, body)
|
|
base.assign_fabric_material(shell, albedo_img)
|
|
|
|
base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb"))
|
|
|
|
|
|
def main():
|
|
global OFFSET_M, SOLE_DROP_M, COLLAR_FRAC, COLLAR_FLARE_M
|
|
global N_LACES, PLAIN, SOLE_SNAP_FRAC
|
|
global TOE_EXT_M, TOE_WMARGIN_M, TOE_HCLEAR_M
|
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
|
if len(argv) < 2:
|
|
print("Usage: -- <bodies_root> <out_dir> [--bodies a,b,c] "
|
|
"[--offset M] [--sole-drop M] [--sole-snap-frac F] "
|
|
"[--collar-frac F] [--collar-flare M] [--toe-ext M] "
|
|
"[--toe-wmargin M] [--toe-hclear M] "
|
|
"[--laces N] [--plain]")
|
|
sys.exit(1)
|
|
bodies_root = argv[0]
|
|
out_dir = argv[1]
|
|
if "--offset" in argv:
|
|
OFFSET_M = float(argv[argv.index("--offset") + 1])
|
|
if "--sole-drop" in argv:
|
|
SOLE_DROP_M = float(argv[argv.index("--sole-drop") + 1])
|
|
if "--sole-snap-frac" in argv:
|
|
SOLE_SNAP_FRAC = float(argv[argv.index("--sole-snap-frac") + 1])
|
|
if "--collar-frac" in argv:
|
|
COLLAR_FRAC = float(argv[argv.index("--collar-frac") + 1])
|
|
if "--collar-flare" in argv:
|
|
COLLAR_FLARE_M = float(argv[argv.index("--collar-flare") + 1])
|
|
if "--toe-ext" in argv:
|
|
TOE_EXT_M = float(argv[argv.index("--toe-ext") + 1])
|
|
if "--toe-wmargin" in argv:
|
|
TOE_WMARGIN_M = float(argv[argv.index("--toe-wmargin") + 1])
|
|
if "--toe-hclear" in argv:
|
|
TOE_HCLEAR_M = float(argv[argv.index("--toe-hclear") + 1])
|
|
if "--laces" in argv:
|
|
N_LACES = int(argv[argv.index("--laces") + 1])
|
|
if "--plain" in argv:
|
|
PLAIN = True
|
|
bodies = base.BODY_TYPES
|
|
if "--bodies" in argv:
|
|
bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")]
|
|
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
log(f"sneaker per-body mode: {len(bodies)} bodies, offset "
|
|
f"{OFFSET_M * 1000:.0f} mm, sole-drop {SOLE_DROP_M * 1000:.0f} mm, "
|
|
f"collar-frac {COLLAR_FRAC}, laces {N_LACES}, plain={PLAIN}")
|
|
|
|
results = []
|
|
for body in bodies:
|
|
body_dir = os.path.join(bodies_root, body)
|
|
log(f"=== {body} ===")
|
|
if not os.path.isdir(body_dir):
|
|
results.append((body, "skipped: body dir missing"))
|
|
continue
|
|
try:
|
|
author_sneaker_shell(body_dir, out_dir, body, OFFSET_M)
|
|
results.append((body, "ok"))
|
|
except Exception as exc:
|
|
log(f"ERROR {body}: {exc}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
results.append((body, f"error: {exc}"))
|
|
|
|
ref = base.REFERENCE_BODY
|
|
ref_mask = os.path.join(out_dir, f"{ref}_mask.png")
|
|
if os.path.isfile(ref_mask):
|
|
shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png"))
|
|
log(f"copied {ref}_mask.png -> reference_mask.png (fallback)")
|
|
ref_alb = os.path.join(out_dir, f"{ref}_base_albedo.png")
|
|
if os.path.isfile(ref_alb):
|
|
shutil.copy2(ref_alb, os.path.join(out_dir, "base_albedo.png"))
|
|
log(f"copied {ref}_base_albedo.png -> base_albedo.png (shared sidecar)")
|
|
|
|
log("=" * 50)
|
|
for body, status in results:
|
|
log(f" {body:12s} {status}")
|
|
ok = sum(1 for _, s in results if s == "ok")
|
|
log(f"OK={ok}/{len(results)}")
|
|
if ok != len(results):
|
|
sys.exit(1)
|
|
log("DONE")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|