Files
animation/characters/work/lena/11_line_heal.py
T
jeremy 3d8825f5a9 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>
2026-08-12 07:17:15 -07:00

143 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Stage 11: heal the garment-line creases across the stomach/waist/hips.
# The lines are GEOMETRY (they show in clay): the briefs waistband ledge and leg-hem ridges
# (the briefs zone's 1x field kept hip anatomy AND the hem ridges), plus dotted pinch lines
# where the source mesh's open slits were welded. Strip = hemband mask slit-seam verts
# (mapped from the RAW pre-weld GLB's boundary edges) normal-kink verts in the torso band.
# Taubin (volume-preserving) on the strip: ridges round off, hips/butt keep their shape.
# blender --background --python 11_line_heal.py -- <in.blend> <masks.npz> <raw.glb> <out.blend>
import bpy, sys, time, math
import numpy as np
from mathutils import Vector
from mathutils.kdtree import KDTree
argv = sys.argv[sys.argv.index("--") + 1:]
BLEND, MASKS, RAW, OUT = argv[0], argv[1], argv[2], argv[3]
t0 = time.time()
ANG_THR = math.radians(12.0)
TAUBIN_PAIRS = 30
Z0, Z1 = 0.30, 0.87
def log(m):
print(f"[heal {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)
co = np.empty(n_v * 3)
me.vertices.foreach_get("co", co)
co = co.reshape(-1, 3)
M = np.load(MASKS)
hemband = M["hemband"]
# ---- slit-seam verts from the raw pre-weld GLB ----
before = set(bpy.data.objects)
bpy.ops.import_scene.gltf(filepath=RAW)
new = [o for o in bpy.data.objects if o not in before]
raw = max([o for o in new if o.type == 'MESH'], key=lambda o: len(o.data.vertices))
rme = raw.data
rn = len(rme.vertices)
rco = np.empty(rn * 3)
rme.vertices.foreach_get("co", rco)
rco = rco.reshape(-1, 3)
l_tot = np.empty(len(rme.polygons), dtype=np.int32)
rme.polygons.foreach_get("loop_total", l_tot)
l_start = np.empty(len(rme.polygons), dtype=np.int32)
rme.polygons.foreach_get("loop_start", l_start)
l_v = np.empty(len(rme.loops), dtype=np.int32)
rme.loops.foreach_get("vertex_index", l_v)
ecount = {}
for fs, ft in zip(l_start, l_tot):
idxs = l_v[fs:fs + ft]
for k in range(ft):
a, b = idxs[k], idxs[(k + 1) % ft]
key = (a, b) if a < b else (b, a)
ecount[key] = ecount.get(key, 0) + 1
rbnd = np.zeros(rn, dtype=bool)
for (a, b), c in ecount.items():
if c == 1:
rbnd[a] = rbnd[b] = True
log(f"raw boundary verts: {rbnd.sum()}")
for o in new:
bpy.data.objects.remove(o, do_unlink=True)
kd = KDTree(n_v)
for i in range(n_v):
kd.insert(Vector(co[i]), i)
kd.balance()
seam = np.zeros(n_v, dtype=bool)
# NB: positions have been sculpted since the weld — match generously but only in the torso
# band, and only trust matches within 6 mm (sculpted garment areas moved far more; their
# seams are already handled by the fills/melts there)
for p in rco[rbnd]:
if not (Z0 < p[2] < Z1):
continue
hit = kd.find(Vector(p))
if hit[0] is not None and hit[2] < 0.006:
seam[hit[1]] = True
log(f"seam verts mapped: {seam.sum()}")
# ---- normal-kink verts (ledges/ridges) ----
nrm = np.empty(n_v * 3)
me.vertices.foreach_get("normal", nrm)
nrm = nrm.reshape(-1, 3)
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")
o_s = order[srt]
n_s = nbr[srt]
ptr = np.searchsorted(o_s, np.arange(n_v + 1))
cnt = np.maximum(ptr[1:] - ptr[:-1], 1)
N = nrm.copy()
for _ in range(5):
acc = np.zeros_like(N)
np.add.at(acc, o_s, N[n_s])
N = acc / cnt[:, None]
N /= np.maximum(np.linalg.norm(N, axis=1, keepdims=True), 1e-12)
ang = np.arccos(np.clip((nrm * N).sum(axis=1), -1, 1))
navel = (np.abs(co[:, 0]) < 0.022) & (co[:, 2] > 0.495) & (co[:, 2] < 0.555) & (co[:, 1] < 0)
band = (co[:, 2] > 0.42) & (co[:, 2] < Z1) & ~navel
kink = band & (ang > ANG_THR)
log(f"kink verts: {kink.sum()}")
strip = (hemband | seam | kink) & (co[:, 2] > Z0) & (co[:, 2] < Z1) & ~navel
for _ in range(2):
hit = strip[ev[:, 0]] | strip[ev[:, 1]]
s2 = strip.copy()
s2[ev[:, 0]] |= hit
s2[ev[:, 1]] |= hit
strip = s2
strip &= ~navel
sidx = np.nonzero(strip)[0]
log(f"strip: {len(sidx)} verts")
Q = co.copy()
lam, mu = 0.5, -0.53
for _ in range(TAUBIN_PAIRS):
for f in (lam, mu):
acc = np.zeros_like(Q)
np.add.at(acc, o_s, Q[n_s])
mean = acc / cnt[:, None]
Q[sidx] += f * (mean[sidx] - Q[sidx])
d = np.linalg.norm(Q - co, axis=1)
log(f"Taubin x{TAUBIN_PAIRS}: max move {d.max():.4f}")
me.vertices.foreach_set("co", Q.reshape(-1))
me.update()
if me.has_custom_normals:
vn = np.empty(n_v * 3, dtype=np.float32)
me.vertices.foreach_get("normal", vn)
me.normals_split_custom_set_from_vertices(vn.reshape(-1, 3))
bpy.context.preferences.filepaths.save_version = 0 # no .blend1 autosave
bpy.ops.wm.save_as_mainfile(filepath=OUT)
log(f"WROTE {OUT}")
print("HEAL_DONE")