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>
616 lines
26 KiB
Python
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()
|