reorg(characters): ship-time folders — lena_base_v01 ships, lane moves to work/lena

REGISTRY rewritten around the central rule: a character folder is born only
when a body ships to ariki-game (<character>_base_v<NN> = ship ordinal).
lena_nude dissolves accordingly:
- characters/female/lena_base_v01/ — SHIPPED 2026-08-10: AccuRig GLB carrier,
  T-pose/rig FBX + JSON, previews, frozen README
- characters/work/lena/ — the live lane: recipes 01-47 (incl. new 36-47:
  refill/sheets/clay/despeckle/musculature/spin/AccuRig export/graft/pose QC),
  masters (athletic_v04 blend + textures, accurig blend), lane-history README
- hires_claude/hires_work intermediates (blends, logs, probes) pruned

Supporting docs: AGENTS.md, working-files rule, rig-graft plan addendum,
originals README, prune_lane.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 07:17:15 -07:00
parent 4bbd1a1732
commit 3d8825f5a9
263 changed files with 3262 additions and 26313 deletions
+158
View File
@@ -0,0 +1,158 @@
# Stage 2: isolate the garment on the welded hires sculpt, split it into functional zones, and
# save the masks + adjacency for the sculpt stage.
#
# blender --background --python 02_isolate.py -- <00_welded.blend> <masks.npz>
#
# Zones (per vertex):
# cups — bra front where the breasts live (gets wall + amplified mounds)
# shield — sternum strip the bra bridges (gets carved cleavage)
# toprest — rest of the top: band, straps, back panel (gets faired flat to skin)
# briefs — briefs incl. waistband/leg rims (gets faired featureless)
# plus 'hemband' — narrow ring around every garment/skin boundary (local crease fairing).
#
# The colour key was calibrated on THIS texture by 01_probe.py: garment sat~0.39/RB~1.65,
# skin sat~0.64/RB~2.80; thresholds are the midpoints. The z-guards keep the low-saturation
# face features (eyes, teeth, lips) out of the key's reach.
import bpy, sys, time
import numpy as np
argv = sys.argv[sys.argv.index("--") + 1:]
BLEND, OUT = argv[0], argv[1]
t0 = time.time()
SAT_THR, RB_THR = 0.518, 2.22
Z_GARMENT_LO, Z_GARMENT_HI = 0.40, 0.85 # below the head; briefs to over-shoulder straps
Z_TOP_SPLIT = 0.595 # briefs/top divide (waistband top 0.578 + margin)
GROW_RINGS = 6 # ~2 cm real at this density; must swallow the 1 cm hem lips
# cups zone (front bra): z and |x| bounds from the measured landmarks
CUP_Z_LO, CUP_Z_HI = 0.615, 0.745
CUP_X_MAX = 0.085
SHIELD_X = 0.014 # sternum strip half-width
def log(m):
print(f"[iso {time.time()-t0:6.1f}s] {m}", flush=True)
bpy.ops.wm.open_mainfile(filepath=BLEND)
ob = max([o for o in bpy.data.objects if o.type == 'MESH'],
key=lambda o: len(o.data.vertices))
me = ob.data
n_v = len(me.vertices)
log(f"loaded {n_v}v")
co = np.empty(n_v * 3, dtype=np.float64)
me.vertices.foreach_get("co", co)
co = co.reshape(-1, 3)
# --- per-vertex colour ---
img = next(i for i in bpy.data.images if "basecolor" in i.name.lower())
w, h = img.size
px = np.empty(w * h * 4, dtype=np.float32)
img.pixels.foreach_get(px)
rgb = px.reshape(h, w, 4)[:, :, :3]
loops_v = np.empty(len(me.loops), dtype=np.int32)
me.loops.foreach_get("vertex_index", loops_v)
uv = np.empty(len(me.loops) * 2, dtype=np.float64)
me.uv_layers.active.data.foreach_get("uv", uv)
uv = uv.reshape(-1, 2)
first_loop = np.full(n_v, len(loops_v), dtype=np.int64)
np.minimum.at(first_loop, loops_v, np.arange(len(loops_v), dtype=np.int64))
first_loop = np.minimum(first_loop, len(loops_v) - 1)
vx = (np.clip(uv[first_loop, 0], 0, 1) * (w - 1)).astype(int)
vy = (np.clip(uv[first_loop, 1], 0, 1) * (h - 1)).astype(int)
vcol = rgb[vy, vx]
mx = vcol.max(axis=1)
mn = vcol.min(axis=1)
sat = np.where(mx > 1e-5, (mx - mn) / np.maximum(mx, 1e-5), 0)
rb = vcol[:, 0] / np.maximum(vcol[:, 2], 1e-5)
# above z=0.80 only the shoulder straps qualify — keep the chin/lips (low-saturation paint)
# out by requiring lateral offset there
zone_guard = (co[:, 2] > Z_GARMENT_LO) & (co[:, 2] < Z_GARMENT_HI) & ((co[:, 2] < 0.80) | (np.abs(co[:, 0]) > 0.025))
key = (sat < SAT_THR) & (rb < RB_THR) & zone_guard
# The shoulder straps' paint is much closer to skin (median sat 0.57-0.61 vs the bra body's
# 0.39) — the main key catches only ~a third of each strap and the rest survived as raised
# geometry. In the strap corridor a relaxed threshold seeds them; the ring grow fills the rest.
strap_zone = (co[:, 2] > 0.72) & (co[:, 2] < 0.85) & (np.abs(co[:, 0]) > 0.03) & (np.abs(co[:, 0]) < 0.105)
key |= strap_zone & (sat < 0.55)
log(f"colour key (+strap corridor): {key.sum()} verts")
# --- adjacency (numpy CSR-ish over edges) ---
n_e = len(me.edges)
ev = np.empty(n_e * 2, dtype=np.int32)
me.edges.foreach_get("vertices", ev)
ev = ev.reshape(-1, 2)
order = np.concatenate([ev[:, 0], ev[:, 1]])
nbr = np.concatenate([ev[:, 1], ev[:, 0]])
srt = np.argsort(order, kind="stable")
order_s = order[srt]
nbr_s = nbr[srt]
ptr = np.searchsorted(order_s, np.arange(n_v + 1))
log("adjacency built")
def grow(mask, rings):
out = mask.copy()
for _ in range(rings):
sel = np.zeros(n_v, dtype=bool)
# mark all neighbours of current selection
active = np.nonzero(out)[0]
# gather neighbour slices
for a in active:
sel[nbr_s[ptr[a]:ptr[a + 1]]] = True
out |= sel
return out
# component filter: drop specks (<200 verts) via BFS on the keyed set
from collections import deque
comp_id = np.full(n_v, -1, dtype=np.int64)
cid = 0
keep = np.zeros(n_v, dtype=bool)
for s in np.nonzero(key)[0]:
if comp_id[s] >= 0:
continue
q = deque([s])
comp_id[s] = cid
members = [s]
while q:
c = q.popleft()
for nb in nbr_s[ptr[c]:ptr[c + 1]]:
if key[nb] and comp_id[nb] < 0:
comp_id[nb] = cid
q.append(nb)
members.append(nb)
if len(members) >= 200:
keep[members] = True
cid += 1
log(f"component filter: {keep.sum()} verts in {cid} raw components")
garment = grow(keep, GROW_RINGS)
log(f"grown +{GROW_RINGS}: {garment.sum()}")
top = garment & (co[:, 2] >= Z_TOP_SPLIT)
briefs = garment & (co[:, 2] < Z_TOP_SPLIT)
front = co[:, 1] < 0.0
cups = top & front & (co[:, 2] > CUP_Z_LO) & (co[:, 2] < CUP_Z_HI) \
& (np.abs(co[:, 0]) < CUP_X_MAX) & (np.abs(co[:, 0]) > SHIELD_X)
shield = top & front & (co[:, 2] > CUP_Z_LO) & (co[:, 2] < CUP_Z_HI) \
& (np.abs(co[:, 0]) <= SHIELD_X)
toprest = top & ~cups & ~shield
# hem band: garment boundary vs non-garment, +/-2 rings
edge_g = garment[ev]
bnd_edges = ev[edge_g[:, 0] != edge_g[:, 1]]
bmask = np.zeros(n_v, dtype=bool)
bmask[bnd_edges.ravel()] = True
hemband = grow(bmask, 8) # hem lips are ~3 rings wide; blend needs room beyond them
log(f"zones: cups={cups.sum()} shield={shield.sum()} toprest={toprest.sum()} "
f"briefs={briefs.sum()} hemband={hemband.sum()}")
np.savez_compressed(OUT, garment=garment, cups=cups, shield=shield, toprest=toprest,
briefs=briefs, hemband=hemband, key_raw=keep)
log(f"WROTE {OUT}")
print("ISOLATE_DONE")