fix(assets): convex toe boxes on closed footwear — no more foot-shaped shoes (T-1089)

User review finding: sneakers/formal shoes/boots conformed to individual
toes (and toes poked the closed front). Root cause: the skin-conforming
clearance clamp ran AFTER toe smoothing and re-imprinted the original toe
bumps; boots also copied per-toe skin weights (ripple under flex). Fix:
shared base.convex_toe_box() — per-slice enclosing ellipse from the skin,
notch fill, projection onto the smooth cap (outside skin by construction),
extended rounded nose past the longest toe, uniform feathered ball-bone
binding so shoes flex rigidly at the ball joint. Style-parameterized
(sneakers roomy / formal sleek tapered / boots chunky). Re-authored x 11
bodies; QA all-green (worst 82px « 150 gate; residuals are collar/sole-edge
slivers, not toes). Lookbook shots re-rendered on the desktop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 12:59:00 +02:00
co-authored by Claude Fable 5
parent e562418d06
commit e709462a7b
131 changed files with 302 additions and 256 deletions
+14 -139
View File
@@ -127,23 +127,11 @@ 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
# Toe-box merge (pre-offset Laplacian smoothing).
FOOT_SMOOTH_ITERS = 4 # mild pass over the whole foot (below the ankle)
FOOT_SMOOTH_FACTOR = 0.5
TOE_SMOOTH_ITERS = 10 # aggressive pass forward of the ball joint
TOE_SMOOTH_FACTOR = 1.0
TOE_ZONE_BALL_FRAC = 0.80 # toe zone starts at this fraction of the ball-y
# Skin containment clamp (post-offset).
CLAMP_CLEAR_FRAC = 1.0 # minimum clearance as a fraction of the offset.
# At 1.0 the clamp restores the full standoff: the
# clamp moves VERTS, and a face spanning two clamped
# verts can still dip toward the high-frequency skin
# toes between them (QA/preview evidence: toe tips
# poked the melted toe box at 0.75). Higher values
# (1.15 tried) buy little once the foot weight
# re-bind aligns flexion, and read as a lumpy box.
CLAMP_ITERS = 3
# 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
@@ -221,117 +209,6 @@ class BootLandmarks:
# Geometry: shaft cut + rim flatten + flare + sole
# --------------------------------------------------------------------------
def build_skin_bvh(shell):
"""BVH of the welded skin BEFORE any shaping — the containment reference.
Returns (bvh, bm); the bmesh must stay alive as long as the BVH is used.
"""
from mathutils.bvhtree import BVHTree
bm = bmesh.new()
bm.from_mesh(shell.data)
bm.normal_update()
return BVHTree.FromBMesh(bm), bm
def containment_clamp(shell, skin_bvh, min_clear, z_max):
"""Push shell verts back outside the original skin (post-offset).
The toe-box merge can leave the smoothed shell INSIDE the real skin toes;
any vert (below z_max) whose signed distance to the skin surface is less
than min_clear is moved to min_clear along the skin normal. Iterated,
because in concave spots (toe crevices) the first push can land near
another skin face."""
me = shell.data
total = 0
for it in range(CLAMP_ITERS):
moved = 0
for v in me.vertices:
if v.co.z > z_max:
continue
loc, nrm, _idx, _dist = skin_bvh.find_nearest(v.co)
if loc is None:
continue
if (v.co - loc).dot(nrm) < min_clear:
v.co = loc + nrm * min_clear
moved += 1
total += moved
if moved == 0:
break
me.update()
log(f"containment clamp: {total} vert pushes "
f"(min clearance {min_clear * 1000:.1f} mm)")
def transfer_foot_weights(shell, skin_bm, skin_bvh, z_max):
"""Re-copy foot-region vertex weights from the nearest ORIGINAL skin vert.
Smoothing + clamping relocate shell verts while they keep their origin
vert's weights, so leather hovering over toe N can flex with toe M's bone
— under ball flexion the shell then diverges from the skin it covers and
the toes poke through. Copying each relocated vert's weights from the
nearest vert of the original welded skin re-aligns material to anatomy.
Group indices match by construction (same object, groups untouched).
"""
me = shell.data
bm = bmesh.new()
bm.from_mesh(me)
dl = bm.verts.layers.deform.verify()
sdl = skin_bm.verts.layers.deform.verify()
skin_bm.faces.ensure_lookup_table()
rebound = 0
for v in bm.verts:
if v.co.z > z_max:
continue
loc, _nrm, fidx, _dist = skin_bvh.find_nearest(v.co)
if loc is None:
continue
face = skin_bm.faces[fidx]
sv = min(face.verts, key=lambda fv: (fv.co - v.co).length_squared)
src = sv[sdl]
dst = v[dl]
dst.clear()
for g, w in src.items():
dst[g] = w
rebound += 1
bm.to_mesh(me)
bm.free()
me.update()
log(f"foot weight re-bind: {rebound} verts re-copied from nearest skin")
def toe_merge(shell, lm):
"""Fuse the skin toes into one rounded boot toe box (pre-offset).
Two Laplacian passes: a mild one over the whole foot (below the ankle)
rounds anatomical detail into leather, an aggressive one forward of the
ball joint melts the individual toes together. Vertex weights and UVs are
untouched, so skinning and texel painting are unaffected; the outward
offset afterwards restores the lost girth."""
bm = bmesh.new()
bm.from_mesh(shell.data)
foot = [v for v in bm.verts if v.co.z < lm.ankle_z]
for _ in range(FOOT_SMOOTH_ITERS):
bmesh.ops.smooth_vert(bm, verts=foot, factor=FOOT_SMOOTH_FACTOR,
use_axis_x=True, use_axis_y=True, use_axis_z=True)
n_toe = 0
if lm.ball_front is not None:
toe_y = lm.ball_front * TOE_ZONE_BALL_FRAC
toes = [v for v in foot if v.co.y * FRONT_Y_SIGN > toe_y]
n_toe = len(toes)
for _ in range(TOE_SMOOTH_ITERS):
bmesh.ops.smooth_vert(bm, verts=toes, factor=TOE_SMOOTH_FACTOR,
use_axis_x=True, use_axis_y=True,
use_axis_z=True)
else:
log("WARNING: ball_l bone missing — toe box keeps skin toes")
bm.to_mesh(shell.data)
bm.free()
shell.data.update()
log(f"toe merge: smoothed {len(foot)} foot verts "
f"({FOOT_SMOOTH_ITERS}x{FOOT_SMOOTH_FACTOR}), "
f"{n_toe} toe verts ({TOE_SMOOTH_ITERS}x{TOE_SMOOTH_FACTOR})")
def shaft_cut(shell, lm):
"""Trim the calves above the boot-shaft plane (bone-derived)."""
bm = bmesh.new()
@@ -635,21 +512,19 @@ def author_boot_shell(body_dir, out_dir, body, offset, sole_drop):
skin_min_z = min(v.co.z for v in shell.data.vertices)
lm = BootLandmarks(armature, skin_min_z)
# Containment reference: the ORIGINAL welded skin (real toes), captured
# before any shaping. The bmesh must outlive the BVH queries.
skin_bvh, skin_bm = build_skin_bvh(shell)
toe_merge(shell, lm)
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)
# Toe-box merge can leave the smoothed+offset shell inside the real skin
# toes; push foot-region verts back out to the minimum clearance, then
# re-align their bone weights to the anatomy they now cover (flex fix).
containment_clamp(shell, skin_bvh, CLAMP_CLEAR_FRAC * offset, lm.ankle_z)
transfer_foot_weights(shell, skin_bm, skin_bvh, lm.ankle_z)
skin_bm.free()
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)
@@ -336,6 +336,226 @@ def offset_outward(shell, offset):
log(f"offset surface outward by {offset*1000:.0f} mm along normals")
# --------------------------------------------------------------------------
# Convex toe box (T-1089 footwear fix — shared by sneakers/shoes/boots)
#
# Closed shoes have a smooth rigid TOE BOX: a convex rounded cap the toes sit
# INSIDE, not a shell that wraps each toe. The earlier per-script approach
# (Laplacian smooth the toes, then push verts back out to the ORIGINAL skin
# surface) re-imprinted the individual toes — the skin-conforming clamp
# followed each toe bump, so bumps/pokes survived. This routine instead
# forces every toe cross-section onto one analytic half-ellipse dome that
# CIRCUMSCRIBES the toes (guaranteed outside the skin, so no poke, and no
# per-toe detail survives), extends the nose forward past the longest toe,
# and rebinds the whole box UNIFORMLY to the ball bone so it flexes rigidly
# at the ball joint with no per-vertex toe-weight ripple under animation.
#
# Applied PRE-offset: the mold encloses the skin toes by construction, then
# offset_outward adds the standoff uniformly over a smooth surface. No skin
# clamp is needed (or wanted) in the toe zone afterward.
# --------------------------------------------------------------------------
def _smoothstep(t):
t = min(max(t, 0.0), 1.0)
return t * t * (3.0 - 2.0 * t)
def foot_ball_u(armature):
"""Forward coord (u = y*FRONT_Y_SIGN) of the ball joint (toe-box hinge).
Bodies face -Y so toes point -Y; u increases toward the toes. ball_l/ball_r
share the same forward head coord (feet are x-mirror symmetric)."""
ball = armature.data.bones.get("ball_l")
if ball is None:
return None
return ball.head_local.y * FRONT_Y_SIGN
def _interp(x, xp, fp):
"""Minimal linear interp with flat ends (np.interp semantics, no import)."""
if x <= xp[0]:
return fp[0]
if x >= xp[-1]:
return fp[-1]
for i in range(1, len(xp)):
if x < xp[i]:
t = (x - xp[i - 1]) / max(xp[i] - xp[i - 1], 1e-9)
return fp[i - 1] + t * (fp[i] - fp[i - 1])
return fp[-1]
def convex_toe_box(shell, armature, *, extension, width_margin, height_clear,
nbins=10, feather_m=0.020, bottom_band_m=0.0015,
nose_frac=0.40, smooth_iters=7, smooth_factor=0.6,
uniform_ball_weights=True):
"""Reshape the forefoot into a smooth convex toe box (per foot side).
Each cross-section forward of the ball joint is forced onto ONE smooth
ellipse that circumscribes that slice's toe verts — every individual-toe
bump/crevice is erased and the shell sits OUTSIDE the skin (the ellipse is
the slice's own enclosing ellipse + a margin, so projecting only pushes
verts outward). Sizes are measured per u-slice (never a single collapsing
quadric, which over-inflates), so the box follows the foot's natural taper
while reading as one rigid cap. The frontmost `nose_frac` of the toe length
is pushed forward up to `extension` past the longest toe. The whole cap is
rebound uniformly to the ball bone so it flexes rigidly at the ball joint
with no per-vertex toe-weight ripple.
extension forward nose extension past the longest toe (m).
width_margin half-width padding added around each slice (m).
height_clear vertical headroom added above the toes (m) — flex room.
nbins number of u-slices sized independently along the toe length.
feather_m blend band behind the ball over which effect + rebind ramp.
bottom_band_m underside band left for the sole routine (dome does top+sides).
nose_frac fraction of the toe length (from the tip back) that is pushed
forward to form the extended rounded nose.
"""
ball_u = foot_ball_u(armature)
if ball_u is None:
log("WARNING: ball_l missing — convex toe box skipped")
return
me = shell.data
bm = bmesh.new()
bm.from_mesh(me)
bm.verts.ensure_lookup_table()
bm.normal_update() # pre-reshape normals gate the underside (sole) verts
dl = bm.verts.layers.deform.verify()
gi = {g.name: g.index for g in shell.vertex_groups}
ball_gi = {1: gi.get("ball_l"), -1: gi.get("ball_r")}
verts = list(bm.verts)
feather_u0 = ball_u - feather_m
total_reshaped = 0
for side in (1, -1):
sverts = [v for v in verts if (v.co.x * side) > 0.0]
toe = [v for v in sverts if (v.co.y * FRONT_Y_SIGN) > ball_u]
if len(toe) < 6:
continue
cx = sum(v.co.x for v in toe) / len(toe)
base_z = min(v.co.z for v in sverts) # per-side sole level
u_tip = max(v.co.y * FRONT_Y_SIGN for v in toe)
span = max(u_tip - ball_u, 1e-6)
# --- per-slice circumscribing ellipse (top + side verts only) --------
centers = [ball_u + span * (i + 0.5) / nbins for i in range(nbins)]
Barr = [width_margin] * nbins
Harr = [height_clear] * nbins
bin_verts = [[] for _ in range(nbins)]
for v in toe:
if (v.co.z - base_z) <= bottom_band_m:
continue # underside -> sole routine
i = int((v.co.y * FRONT_Y_SIGN - ball_u) / span * nbins)
i = min(max(i, 0), nbins - 1)
bin_verts[i].append(v)
for i in range(nbins):
bv = bin_verts[i]
if not bv:
continue
b0 = max(abs(v.co.x - cx) for v in bv) + width_margin
h0 = max(v.co.z - base_z for v in bv) + height_clear
# circumscribe: scale the (b0,h0) ellipse until it holds every vert
kmax = 1.0
for v in bv:
rr = (((v.co.x - cx) / b0) ** 2
+ ((v.co.z - base_z) / h0) ** 2) ** 0.5
kmax = max(kmax, rr)
Barr[i] = b0 * kmax
Harr[i] = h0 * kmax
# fill empty bins by carrying the last known size forward/back
for i in range(1, nbins):
if bin_verts[i] == [] or Barr[i] == width_margin:
Barr[i], Harr[i] = Barr[i - 1], Harr[i - 1]
# one along-length smoothing pass (keeps the cap from stepping)
Bs = list(Barr)
Hs = list(Harr)
for i in range(1, nbins - 1):
Bs[i] = 0.25 * Barr[i - 1] + 0.5 * Barr[i] + 0.25 * Barr[i + 1]
Hs[i] = 0.25 * Harr[i - 1] + 0.5 * Harr[i] + 0.25 * Harr[i + 1]
nose_start = u_tip - nose_frac * span
work = [v for v in sverts if (v.co.y * FRONT_Y_SIGN) > feather_u0]
# Capture the underside gate from the SKIN (pre-smooth) normals so it
# matches how the per-style sole routine classifies its verts (roughly
# normal.z < -0.5). Verts the sole owns are excluded from the cap, so
# the cap never fights the sole flatten (which caused underside tears).
gate = {}
for v in work:
gate[v.index] = _smoothstep((v.normal.z + 0.5) / 0.25) # -0.5->0
# --- fill the between-toe notches (Laplacian) BEFORE projecting -------
# The individual-toe crevices are deep valleys; in-place ellipse
# projection alone leaves their walls. Smoothing melts the valleys into
# one volume (like the old pipeline) — but the ellipse SIZES above were
# measured from the ORIGINAL toe, so the projection below pushes the
# smoothed (shrunk) surface back OUT onto a cap that still encloses the
# real skin. The uniform ball rebind fixes the flex ripple that made
# the old pipeline keep its smoothing timid.
if smooth_iters > 0:
toe_all = [v for v in sverts if (v.co.y * FRONT_Y_SIGN) > ball_u]
for _ in range(smooth_iters):
bmesh.ops.smooth_vert(bm, verts=toe_all, factor=smooth_factor,
use_axis_x=True, use_axis_y=True,
use_axis_z=True)
n_side = 0
for v in work:
u = v.co.y * FRONT_Y_SIGN
f = _smoothstep((u - feather_u0) / max(ball_u - feather_u0, 1e-6))
# ellipse size at this u (from the pre-stretch position)
B = _interp(u, centers, Bs)
H = _interp(u, centers, Hs)
hpos = v.co.z - base_z
wn = gate[v.index]
wh = 1.0 if hpos > bottom_band_m else 0.0
w = f * wn * wh
dx = v.co.x - cx
hh = max(hpos, 0.0)
rr = ((dx / B) ** 2 + (hh / H) ** 2) ** 0.5
if w > 1e-6 and rr > 1e-6:
scale = min(max(1.0 / rr, 0.5), 2.5)
tx = cx + dx * scale
tz = base_z + hh * scale
v.co.x += (tx - v.co.x) * w
v.co.z += (tz - v.co.z) * w
n_side += 1
# forward nose push (feathered from nose_start to the tip)
if u > nose_start:
t = _smoothstep((u - nose_start) / max(u_tip - nose_start, 1e-6))
v.co.y += -extension * t * FRONT_Y_SIGN * f
# Uniform ball rebinding (feathered by the length feather f).
bi = ball_gi[side]
if uniform_ball_weights and bi is not None:
for v in work:
u = v.co.y * FRONT_Y_SIGN
f = _smoothstep((u - feather_u0) / max(ball_u - feather_u0, 1e-6))
if f <= 1e-6:
continue
dv = v[dl]
for gidx in list(dv.keys()):
dv[gidx] = dv[gidx] * (1.0 - f)
cur = dv[bi] if bi in dv else 0.0
dv[bi] = cur + f
tot = sum(dv[g] for g in dv.keys())
if tot > 1e-8:
for gidx in list(dv.keys()):
dv[gidx] = dv[gidx] / tot
total_reshaped += n_side
log(f"toe box side {'L' if side > 0 else 'R'}: {len(toe)} toe verts, "
f"B={min(Bs) * 1000:.0f}-{max(Bs) * 1000:.0f}mm "
f"H={min(Hs) * 1000:.0f}-{max(Hs) * 1000:.0f}mm "
f"cap +{extension * 1000:.0f}mm, reshaped {n_side}")
bm.normal_update()
bm.to_mesh(me)
bm.free()
me.update()
log(f"convex toe box: reshaped {total_reshaped} verts "
f"(headroom {height_clear * 1000:.0f}mm, nose +{extension * 1000:.0f}mm)")
def solidify(shell, thickness):
"""Solidify with use_rim to give cloth thickness and cap the cut rims."""
bpy.ops.object.select_all(action='DESELECT')
@@ -133,11 +133,13 @@ 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
TOE_SMOOTH_REPS = (2, 3, 4) # feathered smoothing bands, metatarsal -> toes
# (moderate: heavy reps migrate verts off their
# weight-source skin and animated toes poke out)
TOE_EXTRA_OFFSET_M = 0.005 # extra forefoot standoff after smoothing
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
@@ -326,60 +328,6 @@ def relax_rim(shell, lm, passes):
log(f"relaxed topline rim: {len(adj)} verts, {passes} XY passes")
def smooth_toe_box(shell, lm):
"""Feathered Laplacian smoothing over the forefoot so the individual toe
bumps merge into one formal toe box. Bands (mild -> strong toward the
toes) avoid a crease at the smoothing frontier."""
me = shell.data
bm = bmesh.new()
bm.from_mesh(me)
margin = 0.012 * lm.s
bands = [
(lm.ball_head_y + margin, TOE_SMOOTH_REPS[0]),
(lm.ball_head_y, TOE_SMOOTH_REPS[1]),
((lm.ball_head_y + lm.ball_tail_y) * 0.5, TOE_SMOOTH_REPS[2]),
]
total = 0
for y_max, reps in bands:
verts = [v for v in bm.verts if v.co.y < y_max]
if not verts:
continue
for _ in range(reps):
bmesh.ops.smooth_vert(bm, verts=verts, factor=0.5,
use_axis_x=True, use_axis_y=True,
use_axis_z=True)
total += len(verts) * reps
bm.to_mesh(me)
bm.free()
me.update()
log(f"smoothed toe box: bands at y<{[f'{b:.3f}' for b, _ in bands]} "
f"({total} vert-passes)")
def toe_extra_offset(shell, lm, extra):
"""Extra forefoot standoff (feathered along y) to buy back the clearance
the toe smoothing costs over the toe bumps."""
if extra <= 0.0:
return
me = shell.data
bm = bmesh.new()
bm.from_mesh(me)
bm.normal_update()
margin = 0.012 * lm.s
y0 = lm.ball_head_y + margin # feather start (0 extra)
y1 = lm.ball_tail_y # full extra from the ball line on
n = 0
for v in bm.verts:
if v.co.y < y0:
t = min((y0 - v.co.y) / max(y0 - y1, 1e-6), 1.0)
v.co += v.normal * (extra * t)
n += 1
bm.to_mesh(me)
bm.free()
me.update()
log(f"toe extra offset: {n} verts, +{extra * 1000:.1f} mm feathered")
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
@@ -393,16 +341,23 @@ def snapshot_skin_bvh(shell):
return bvh
def enforce_clearance(shell, skin_bvh, min_clearance):
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). This is what makes
the toe smoothing safe: Laplacian migration can leave the toe box inside
the toe bumps; here every vert gets its rest-pose clearance back by
construction."""
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
@@ -651,13 +606,21 @@ def author_shoes(body_dir, out_dir, body, offset, topline_lift):
lm = FootLandmarks(armature, shell.data)
ankle_cut_and_flatten(shell, lm, topline_lift)
relax_rim(shell, lm, RIM_RELAX_PASSES)
skin_bvh = snapshot_skin_bvh(shell) # BEFORE smoothing moves anything
smooth_toe_box(shell, lm)
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)
sole_idx = classify_sole_verts(shell)
base.offset_outward(shell, offset)
toe_extra_offset(shell, lm, TOE_EXTRA_OFFSET_M)
enforce_clearance(shell, skin_bvh, 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)
+37 -49
View File
@@ -118,11 +118,15 @@ SOLE_SNAP_FRAC = 0.55 # sole CUT height as a fraction of the sole rise
# 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)
TOE_ROUND_M = 0.004 # extra feathered inflation at the toe box
SMOOTH_GLOBAL_ITERS = 2 # light whole-shell de-lumping passes
SMOOTH_TOE_ITERS = 18 # heavy toe-crease melting passes (12 left visible
# knuckle grooves on the probe renders)
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
@@ -262,10 +266,11 @@ def flatten_collar_rims(shell, collar_z, label):
def smooth_shell(shell, lm):
"""Melt the individual toes into one toe box and de-lump the ankle
anatomy: iterative vertex smoothing, heavier toward the toe, with the open
collar boundary pinned so the flattened rim stays put. Runs pre-offset;
the 9 mm standoff + toe inflation restore the smoothing's volume loss."""
"""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)
@@ -274,44 +279,18 @@ def smooth_shell(shell, lm):
for e in bm.edges:
if len(e.link_faces) == 1:
boundary.update(v.index for v in e.verts)
interior = [v for v in bm.verts if v.index not in boundary]
toe_lim = lm.cap_y + 0.10 * lm.foot_len
toe_verts = [v for v in interior if v.co.y < toe_lim]
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=interior, factor=SMOOTH_FACTOR,
use_axis_x=True, use_axis_y=True, use_axis_z=True)
for _ in range(SMOOTH_TOE_ITERS):
bmesh.ops.smooth_vert(bm, verts=toe_verts, factor=SMOOTH_FACTOR,
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 shell: {SMOOTH_GLOBAL_ITERS} global passes "
f"({len(interior)} verts), {SMOOTH_TOE_ITERS} toe passes "
f"({len(toe_verts)} verts, y < {toe_lim:.4f})")
def toe_round(shell, lm, amount):
"""Extra normal-along inflation feathered toward the toe tip — swallows
the foot's toe detail into one rounded sneaker toe box."""
if amount <= 0.0:
return
span = lm.cap_y - lm.toe_tip_y
if span <= 1e-6:
return
bm = bmesh.new()
bm.from_mesh(shell.data)
bm.normal_update()
n = 0
for v in bm.verts:
if v.co.y < lm.cap_y:
t = min((lm.cap_y - v.co.y) / span, 1.0)
v.co += v.normal * (amount * t)
n += 1
bm.to_mesh(shell.data)
bm.free()
shell.data.update()
log(f"toe rounding: {n} verts, +{amount * 1000:.1f} mm feathered")
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
@@ -666,11 +645,16 @@ def author_sneaker_shell(body_dir, out_dir, body, offset):
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
toe_round(shell, lm, TOE_ROUND_M * lm.s)
build_sole_slab(shell, lm)
collar_flare(shell, lm, COLLAR_FLARE_M * lm.s)
base.solidify(shell, base.CLOTH_THICKNESS_M)
@@ -688,13 +672,15 @@ def author_sneaker_shell(body_dir, out_dir, body, offset):
def main():
global OFFSET_M, SOLE_DROP_M, COLLAR_FRAC, COLLAR_FLARE_M
global TOE_ROUND_M, N_LACES, PLAIN, SOLE_SNAP_FRAC, SMOOTH_TOE_ITERS
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-round M] "
"[--toe-smooth N] [--laces N] [--plain]")
"[--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]
@@ -704,14 +690,16 @@ def main():
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 "--toe-smooth" in argv:
SMOOTH_TOE_ITERS = int(argv[argv.index("--toe-smooth") + 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-round" in argv:
TOE_ROUND_M = float(argv[argv.index("--toe-round") + 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: