feat(lena): v02 lane recipes + the archival full-res master
The v02 chain, which exists because bisecting the shard-eye defect proved it lives in
the hires MASTER rather than in any v02 step: the early heal chain ran whole-mesh welds
and reset custom split normals, and the eye/lash shells only read as an eye through
Tripo's authored normals. Every descendant inherits it, including the shipped body.
48_decimate_headsafe body/hands only, head bit-identical (feathered ramp)
49_seams_v02 24_seams with a density-aware hip landmark — the v01 rule fired
at u=0.610, mid-belly, on the head-protected vert count
50_seams_headuv body-only unwrap; the head KEEPS its original Tripo charts.
SLIM collapsed the undecimated lash/brow slivers to points and
ANGLE_BASED packed at half v01's texel density; the face was
the best-mapped region of the source atlas, so it is reused
51_reatlas_v02 31_reatlas + centroid splats for sub-texel triangles — the
skipped set IS the lashes, which rendered as grey glass
52_head_transplant the PRISTINE ORIGINAL head onto the decimated nude body
53_rig_transfer 54_crotch_refill
55_fullres_v02.blend is pinned in .lanekeep as THE archival master: full-res nude body
+ pristine original head, crotch refill and texture despeckle applied, 883,404 v /
1,761,640 f. It supersedes 34_v04 as the lane root (34_v04's head has the shard eyes).
Also: tools/graft_hands.py, the Marvelous Designer hunter-skirt configs v1-v8, the
hunter cloth texture generator, and Mako's measurement card.
Per .agents/rules/working-files.md the per-attempt .blend files under work/lena/v02
are SCRATCH ("never committed") — the .py recipes here are the history and regenerate
any of them from the pinned master. See the ignore rule landing next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
# Stage 48 (v02 candidate): decimate the body, KEEP THE HEAD — Jeremy 2026-08-11.
|
||||
#
|
||||
# blender --background --python 48_decimate_headsafe.py -- <in.blend> <out.blend> [body_target_v]
|
||||
#
|
||||
# The v01 decimation was one global ratio (30_decimate.py, 0.037728): it kept 4% of her head
|
||||
# triangles, and the face is where that shows — Tripo modelled her brows, lash lines and lips as
|
||||
# raised GEOMETRY, so the 197,306-tri head collapsed to 7,836 and the brows went faceted.
|
||||
#
|
||||
# Here the head is excluded outright and the reduction is spent on the body alone:
|
||||
# - vertex group 'decim': weight 1.0 below u=0.80 (shoulders), feathered to 0.0 at u=0.86
|
||||
# (chin). The feather ramps triangle density through the neck instead of snapping at a line.
|
||||
# - Decimate COLLAPSE with that group; the ratio is BISECTED on the evaluated depsgraph until
|
||||
# the BODY-region vertex count lands on target (the same measure-don't-guess pattern as
|
||||
# 30_decimate — the ratio is faces-global, so the body count is what must converge).
|
||||
# - The head is then ASSERTED untouched: exact vertex count and positional checksum over
|
||||
# u > 0.87, not hoped for. If the modifier nibbled it, this aborts rather than ships.
|
||||
#
|
||||
# Budget consequence, stated up front: head 101,136 v rides along whole, so the result is
|
||||
# ~128k v / ~250k tris — 4x the v01 game budget. That is the point: quality over budget,
|
||||
# Jeremy's call. The head budget can be dialled later; the body work is not redone.
|
||||
import bpy, sys, os, time
|
||||
import numpy as np
|
||||
|
||||
argv = sys.argv[sys.argv.index("--") + 1:]
|
||||
BLEND, OUT = argv[0], argv[1]
|
||||
BODY_TARGET = int(argv[2]) if len(argv) > 2 else 27500 # v01's body-region density
|
||||
# Feather narrowed 0.80-0.86 -> 0.84-0.87 after the first run: the wide band held 26,405
|
||||
# partially-protected verts, and because the bisect counted everything below u=0.87 as "body",
|
||||
# the band soaked up nearly the whole 27,500 budget — the true body came out at ~4k verts.
|
||||
# The bisect now counts ONLY u <= FEATHER_LO, so the budget lands where it was meant to.
|
||||
FEATHER_LO, FEATHER_HI = 0.84, 0.87
|
||||
HEAD_CHECK = 0.875
|
||||
t0 = time.time()
|
||||
|
||||
|
||||
def log(m):
|
||||
print(f"[dec48 {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))
|
||||
bpy.context.view_layer.objects.active = ob
|
||||
for o in bpy.data.objects:
|
||||
o.select_set(o is ob)
|
||||
me = ob.data
|
||||
n0 = len(me.vertices)
|
||||
co = np.empty(n0 * 3); me.vertices.foreach_get("co", co); co = co.reshape(-1, 3)
|
||||
zlo, zhi = co[:, 2].min(), co[:, 2].max()
|
||||
u = (co[:, 2] - zlo) / (zhi - zlo)
|
||||
head0 = u > HEAD_CHECK
|
||||
head_n0 = int(head0.sum())
|
||||
head_sum0 = co[head0].sum(axis=0) # positional checksum of the protected region
|
||||
log(f"in: {n0} v / {len(me.polygons)} f head(u>{HEAD_CHECK}): {head_n0} v")
|
||||
|
||||
# feathered protection weights
|
||||
w = np.clip((FEATHER_HI - u) / (FEATHER_HI - FEATHER_LO), 0.0, 1.0)
|
||||
vg = ob.vertex_groups.get("decim") or ob.vertex_groups.new(name="decim")
|
||||
for band in (0.0, 0.25, 0.5, 0.75, 1.0):
|
||||
idx = np.nonzero(np.isclose(np.round(w * 4) / 4, band))[0]
|
||||
if len(idx):
|
||||
vg.add(idx.tolist(), float(band), 'REPLACE')
|
||||
log(f"vertex group: {int((w >= 0.999).sum())} full-weight, "
|
||||
f"{int(((w > 0) & (w < 1)).sum())} feathered, {int((w <= 0).sum())} protected")
|
||||
|
||||
# Applying Decimate discards CUSTOM SPLIT NORMALS for the entire mesh — measured consequence:
|
||||
# the head's lash and eye shells (dense sliver geometry that depends on Tripo's authored normals)
|
||||
# rendered as faceted glass. Keep an untouched duplicate as a normal donor; after the decimate is
|
||||
# applied, the head's normals are transferred back. Positions up there are identical by
|
||||
# construction, so POLYINTERP_NEAREST is an exact restore, feathered off through the neck.
|
||||
donor = ob.copy()
|
||||
donor.data = ob.data.copy()
|
||||
donor.name = "nrm_donor"
|
||||
bpy.context.scene.collection.objects.link(donor)
|
||||
|
||||
mod = ob.modifiers.new("dec", 'DECIMATE')
|
||||
mod.decimate_type = 'COLLAPSE'
|
||||
mod.vertex_group = "decim"
|
||||
mod.vertex_group_factor = 10.0
|
||||
|
||||
|
||||
def body_count(ratio):
|
||||
mod.ratio = ratio
|
||||
dg = bpy.context.evaluated_depsgraph_get()
|
||||
ev = ob.evaluated_get(dg)
|
||||
m = ev.to_mesh()
|
||||
n = len(m.vertices)
|
||||
a = np.empty(n * 3); m.vertices.foreach_get("co", a); a = a.reshape(-1, 3)
|
||||
uu = (a[:, 2] - zlo) / (zhi - zlo)
|
||||
body = int((uu <= FEATHER_LO).sum()) # TRUE body only — the feather band is excluded
|
||||
head = int((uu > HEAD_CHECK).sum())
|
||||
ev.to_mesh_clear()
|
||||
return body, head, n
|
||||
|
||||
|
||||
# analytic first guess: protected faces survive, so the global ratio must budget for them
|
||||
face_head = 0
|
||||
lv = np.empty(len(me.loops), dtype=np.int32); me.loops.foreach_get("vertex_index", lv)
|
||||
ls = np.empty(len(me.polygons), dtype=np.int32); me.polygons.foreach_get("loop_start", ls)
|
||||
prot = u[lv[ls]] > FEATHER_HI # cheap: classify face by first corner
|
||||
face_head = int(prot.sum())
|
||||
guess = (face_head + BODY_TARGET * 2.05) / len(me.polygons)
|
||||
lo_r, hi_r = guess * 0.4, min(1.0, guess * 2.5)
|
||||
log(f"protected faces ~{face_head}; first guess ratio {guess:.5f}")
|
||||
best = None
|
||||
for it in range(9):
|
||||
r = 0.5 * (lo_r + hi_r)
|
||||
b, h, n = body_count(r)
|
||||
log(f" it{it:02d} ratio {r:.5f} -> body {b} v, head {h} v, total {n}")
|
||||
if abs(b - BODY_TARGET) / BODY_TARGET < 0.02:
|
||||
best = r
|
||||
break
|
||||
if b < BODY_TARGET:
|
||||
lo_r = r
|
||||
else:
|
||||
hi_r = r
|
||||
best = r
|
||||
mod.ratio = best
|
||||
bpy.ops.object.modifier_apply(modifier=mod.name)
|
||||
log(f"applied ratio {best:.5f}")
|
||||
|
||||
# ---- assert the head did not move ----
|
||||
me = ob.data
|
||||
n1 = len(me.vertices)
|
||||
co1 = np.empty(n1 * 3); me.vertices.foreach_get("co", co1); co1 = co1.reshape(-1, 3)
|
||||
u1 = (co1[:, 2] - zlo) / (zhi - zlo)
|
||||
head1 = u1 > HEAD_CHECK
|
||||
head_n1 = int(head1.sum())
|
||||
head_sum1 = co1[head1].sum(axis=0)
|
||||
drift = np.abs(head_sum1 - head_sum0).max()
|
||||
print(f"\nHEAD CHECK: {head_n0} -> {head_n1} verts, positional checksum drift {drift:.9f}")
|
||||
assert head_n1 == head_n0 and drift < 1e-4, \
|
||||
"the decimation touched the protected head — do not ship this"
|
||||
print(f"RESULT: {n0} -> {n1} v ({len(me.polygons)} f); body {n1 - head_n1} v, head {head_n1} v")
|
||||
print(f"height unchanged: {co1[:,2].max()-co1[:,2].min():.5f} vs {zhi-zlo:.5f}")
|
||||
|
||||
# restore the head's authored normals from the donor
|
||||
vgn = ob.vertex_groups.new(name="nrm_keep")
|
||||
co2 = np.empty(len(me.vertices) * 3); me.vertices.foreach_get("co", co2); co2 = co2.reshape(-1, 3)
|
||||
u2 = (co2[:, 2] - zlo) / (zhi - zlo)
|
||||
wn = np.clip((u2 - 0.82) / (0.86 - 0.82), 0.0, 1.0)
|
||||
for band in (0.25, 0.5, 0.75, 1.0):
|
||||
idx = np.nonzero(np.isclose(np.round(wn * 4) / 4, band))[0]
|
||||
if len(idx):
|
||||
vgn.add(idx.tolist(), float(band), 'REPLACE')
|
||||
dt = ob.modifiers.new("nrm", 'DATA_TRANSFER')
|
||||
dt.object = donor
|
||||
dt.use_loop_data = True
|
||||
dt.data_types_loops = {'CUSTOM_NORMAL'}
|
||||
dt.loop_mapping = 'POLYINTERP_NEAREST'
|
||||
dt.vertex_group = "nrm_keep"
|
||||
bpy.context.view_layer.objects.active = ob
|
||||
bpy.ops.object.modifier_apply(modifier=dt.name)
|
||||
log("head custom normals restored from donor (feathered 0.82-0.86)")
|
||||
bpy.data.objects.remove(donor, do_unlink=True)
|
||||
ob.vertex_groups.remove(ob.vertex_groups["nrm_keep"])
|
||||
|
||||
ob.vertex_groups.remove(ob.vertex_groups["decim"])
|
||||
bpy.ops.wm.save_as_mainfile(filepath=OUT)
|
||||
log(f"WROTE {OUT}")
|
||||
print("DEC48_DONE")
|
||||
Reference in New Issue
Block a user