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:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user