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