Files
animation/characters/work/lena_leafbikini/README.md
T

207 lines
14 KiB
Markdown
Raw Normal View History

# work/lena_leafbikini — the leaf-bikini lena lane
Grammar-exempt (REGISTRY rule 11). This lane has already produced one ship —
`characters/female/lena_leafbikini_base_v01`, the game's female default since 2026-08-13,
which is **frozen** (REGISTRY rule 2). Everything still here is lane material governed by
`.agents/rules/working-files.md`; when the next body ships it graduates into
`characters/female/lena_leafbikini_base_v02/` and gets its own Ships row.
Parent original: `characters/originals/female/female_lena_leafbikini_tripo.glb`
(sha256 `977104ba…66cf29`, 1,029,360 v / 1,992,503 f, 0.9792 units tall → 1 unit = 1.815
of a 1.777 m body). Read-only, and re-hashed by every stage that opens it.
## Layout
| | |
|---|---|
| `NN_*.py` | the recipes. **These are the history** — kilobytes each, and they regenerate any state below from the original |
| `leaf_mask.npz` | the leaf mask (stage 04): per welded point, plus the vertex→welded map and the hue/sat/val the key was cut from. Regenerable, kept because stage 05 consumes it and re-deriving it costs a full import |
| `v01/` | stage output. **gitignored** (`characters/work/*/v0[0-9]/`) — registered artifacts inside it are `git add -f`'d by hand |
| `v01/review/` | QA renders. gitignored, regenerable, reused rather than re-minted |
## The stages
| stage | what it does |
|---|---|
| `01_graft.py` | grafts the AccuRig skeleton onto the pristine GLB (nearest-surface, not index-exact — the bait was decimated 20:1). This is what produced `lena_leafbikini_base_v01`. Independent of 0205 |
| `02_probe_leaves.py` | what are the leaves? Reports the albedo key candidates and per-vertex proudness |
| `03_render_leaves.py` | textured **and clay** turnaround of any mesh in the lane. The clay pass is what settled the question |
| `04_leaf_mask.py` | builds and *proves* the leaf mask: hue key → component filter → close → hole fill → grow, baked to vertex colour and rendered |
| `05_cut_leaves.py` | deletes the masked faces. Holes left open, on purpose |
| `06_open_in_blender.py` | opens a lane mesh in the Blender GUI, framed on her front, Material Preview |
| `07_fill_crotch.py` | **v02**: Barbie-fills the crotch — melts the briefs leaves into a smooth featureless surface, bust holes stay open. See below |
## What the leaves turned out to be
The clay render (stage 03) shows every leaf, curled tip and hip vine with all materials
stripped, so:
1. **They are real geometry, not paint.** This is the opposite of Lena's game-body
underwear, where a ray-crossing census found the bra *was* the skin and deleting it
opened a hole — the whole reason `tools/make_lena_nude_body.py` re-fairs a rim band
instead of cutting. Here there is a solid shell to remove.
2. **They are welded into the body.** One Tripo mesh, one material, one UV map: the
leaves are a bulge in the body surface, not a separable object, material slot or UV
island. Nothing can be selected "by object"; the seam has to be found.
3. **The seam is both a colour edge and a crease** — where a leaf meets skin the surface
folds back on itself.
## Why the key is hue
Stage 02 first keyed on green dominance, `G - max(R, B)`, and topped out at 0.20: Tripo
painted these leaves a dark desaturated olive (sRGB ~0.27/0.35/0.20), so the channel gap
is only ~0.050.09 and any threshold catching the leaf also catches shadow noise on skin.
Hue separates them outright — skin 2040°, leaf 6080° — and is invariant to exactly the
thing that ruins the channel gap, which is how dark the pixel is.
Everything after the key is repaired **on the mesh, over position-welded adjacency**, not
in texture space:
| step | why |
|---|---|
| min-comp 200 | drops JPEG speckle |
| close 6 rings | fills specular blowouts inside a leaf, where the highlight clips to near-white and its hue becomes noise. At `--val-max 0.62` the hip vine leaves came back half-grey |
| enclosed-hole fill | a mask hole fully surrounded by leaf is a mask defect, not a skin island |
| grow 2 rings | lands the cut just outside the rim. A hole one ring too big is invisible; a leftover leaf stub is not |
| z ≤ 0.85 of height | **her irises and eyebrows are green too.** Without this the key takes her eyes (929 verts) |
Result: 239,561 welded points in exactly **two** islands — bust (z 0.6310.755 of height)
and briefs (z 0.4550.594). Two islands is itself the check that nothing else keyed.
Welded adjacency is not optional anywhere here: the glTF importer splits every UV seam
into separate Blender vertices, so on the raw mesh a face on a texture-chart border is
indistinguishable from a face on a hole rim, and ring operations would leak along the
seams (33,202 of the 1,029,360 verts are seam duplicates).
## The cut rule
A face dies only if **all** of its vertices are masked. With the 2-ring grow that puts the
surviving rim about one ring outside the green, i.e. just onto skin. Both directions of
that asymmetry are deliberate: "any vertex masked" erodes a further ring into her skin and
leaves single triangles hanging off every zig-zag in the mask edge, while a cut one ring
short leaves leaf root standing proud — the one outcome that would make the file useless
for judging the seam.
Then two passes that can only remove things that are already not part of a smooth surface:
dangling-face erosion (a face with ≥2 welded-boundary edges is joined to the surface by at
most one edge; no such face exists on a closed surface, so it cannot bite into skin) and
detached-shell removal. On this mesh they found **2 faces and 0 shells** — the colour cut
was already clean, which is the strongest evidence the mask was right.
## Regenerating `v01/lena_leafbikini_leafcut_sculpt_glb_v01.glb`
```
B="/c/Program Files/Blender Foundation/Blender 5.1/blender.exe"
L=characters/work/lena_leafbikini
"$B" --background --factory-startup --python $L/04_leaf_mask.py -- \
characters/originals/female/female_lena_leafbikini_tripo.glb $L/v01/review
"$B" --background --factory-startup --python $L/05_cut_leaves.py -- \
characters/originals/female/female_lena_leafbikini_tripo.glb $L/leaf_mask.npz \
$L/v01/lena_leafbikini_leafcut_sculpt_glb_v01.glb
"$B" --background --factory-startup --python $L/03_render_leaves.py -- \
$L/v01/lena_leafbikini_leafcut_sculpt_glb_v01.glb $L/v01/review cut
```
Stage 05's gates, all reported and the first two fatal:
| gate | result |
|---|---|
| every surviving vertex position present in the input | 0 strays — this is a deletion and only a deletion |
| loose vertices | 0 |
| surviving shells | 1 |
| removed geometry's extent | z 0.4550.755 of height (bust + briefs bands only) |
## v02 — the Barbie crotch fill (stage 07)
Target: "Barbie doll anatomy" — a completely smooth, undifferentiated pelvic surface, no
cleft, no features, belly flowing into thighs. Jeremy's "take the distance and angle
between the two sides" is precisely a bi-harmonic membrane: the rim supplies POSITION, the
collar behind it supplies SLOPE through the second Laplacian application, and L²x = 0
cannot invent detail. Bust holes stay cut open (v01 behaviour); only the briefs band gets
the treatment.
Getting there burned through roughly ten runs; the recipe header carries the full autopsy,
the short version is:
| attempt | verdict |
|---|---|
| triangle_fill the v01 holes + membrane | the briefs rim is ONE ~2,500-vert loop snaking front → between the legs → back; beauty triangulation bridges the WRONG BANKS at the bends, and the rim itself still carried leaf-root crumple the membrane faithfully anchored to |
| + rim erosion + Delaunay flips + double solve | better rims, same disease — flips are local, the mis-bridging is global; sliver strands shot off the hips |
| **melt instead of fill** (the pivot) | the leaf shell IS the disk that spans the hole — the scan's own manifold surface. Free its verts + a skin collar, solve; wrong-bank bridging becomes impossible. This produced the first genuinely smooth pelvis |
| proudness detectors + deflation for the leftovers | fully-masked leaves are PENDANT BALLOONS — closed shells whose excess area the (no-maximum-principle) bi-harmonic parks as smooth raised caps. Smoothing-built references partially follow any bump wider than their radius, and **Taubin references are worse than useless here: shape-preserving by design, a 1.5 cm cap sits inside the passband and reads ~0** |
| density excision + scar refill | catches the FLAT wads (the melted shell lies ~4 layers deep, and piles would z-fight) but not inflated caps, which sit BELOW the piled median. Scar refill needs: per-sweep densify scope with a floored target (a sliver scar once re-densified everything to 3.3M faces / 17 h), a slit weld first (scars meeting Tripo slits are ribbons, not loops — fill ops refuse them, and degenerate output crashed Blender once), and a >900-edge refusal |
| occlusion / fin / diffusion detectors for the last four "caps" | all near-zero. A pixel-ray probe finally explained why: **the last bumps were not leaf debris at all — they are the melt web itself bridging taut over the inguinal hollow.** There was never skin under the vine, and every rim-anchored method spans its anchors by construction |
| **+ the flow finish** (the guarantee) | damped pure-Laplacian flow over the whole changed region, no detector at all: the maximum principle sinks any pendant cap while the 5-10× wider pubic web barely moves. Ramp weight 0→1 over **3** rings — at 8 rings the hip-vine channel (1020 rings wide) sat entirely in the damped zone |
| **+ the solid-welt pass** | some leaf roots are SOLID skin-painted ridges, not shells — free them (0.5 mm proudness against a whole-band 400-sweep reference; honest skin reads p95 +0.13 mm) and re-solve locally; converges in 2 passes (5,375 verts, then 1) |
Pipeline as shipped in `07_fill_crotch.py`: weld → cut bust open (+despike) → free briefs
leaves + collar → remnant sweep (relaxed hue/blown-white key near the melt, no speckle
filter) → bi-harmonic melt (matrix-free Jacobi-PCG, harmonic warm start) → solid-welt pass
×2 → density excision of flat wads + occlusion pass → slit weld + degenerate dissolve →
three scar-fill sweeps + patch solve → flow finish. Melted faces get one donor texel
(thigh-band vertex nearest the band's median tone with the most neutral normal texel) —
the atlas is Tripo chart soup, so interpolating UVs across the fill would sample garbage.
```
"$B" --background --factory-startup --python $L/07_fill_crotch.py -- \
characters/originals/female/female_lena_leafbikini_tripo.glb $L/leaf_mask.npz \
$L/v02/lena_leafbikini_crotchfill_sculpt_glb_v02.glb --free-rings 3
```
## Open
- **The hip-side shelf bulges are a known consequence, not a bug to chase further.** Where
the vine crossed the inguinal crease there was never skin underneath, and "take the
distance and angle between the two sides" — the membrane — spans its anchors by
construction. Any method anchored at the rims produces the same taut bridge (harmonic,
bi-harmonic, and flow all agree; the maximum principle *protects* a two-sided span).
Killing them means SCULPTING the crease through the strip — invented anatomy, its own
decision, its own stage. Ten runs of detector archaeology confirmed there is nothing
foreign left there to remove.
- **The bust holes are not filled, by request** (v01 deliverable; v02 fills the crotch
only). Whatever fills the bust has to rebuild breast anatomy, not just span it — see
`tools/make_lena_nude_body.py` for how the nude lane sculpted hers procedurally.
- The base-colour map still carries the leaves and their baked contact shadows. Around the
v01 holes and the v02 fill alike, the surviving skin reads darker than her surrounding
tone — an albedo re-author job for a later stage (the nude lane's harmonic refill is the
template).
- **Both artifacts are unrigged**, because they were authored on the pristine original.
They transfer to the shipped body's frame for free: `01_graft.py` moved the mesh
0.000000 mm, so `lena_leafbikini_base_v01` carries the *same* 1,029,360 vertices in the
same order and `leaf_mask.npz` indexes either mesh. Re-running 05/07 against the shipped
GLB is a first-argument change — but the result is a new ship folder, never an edit to
the frozen one.
## Finger-weight re-solve (`08_finger_weights.py`) — how to run it
**The input is `v02/lena_leafbikini_quatskin_fingers_glb_exp03.glb`, not exp01.** This is
the one thing to get right; guessing it cost three wasted bakes on 2026-08-18.
```
"C:/Program Files/Blender Foundation/Blender 5.1/blender.exe" --background \
--factory-startup --python characters/work/lena_leafbikini/08_finger_weights.py -- \
characters/work/lena_leafbikini/v02/lena_leafbikini_quatskin_fingers_glb_exp03.glb \
characters/work/lena_leafbikini/v02/lena_leafbikini_quatskin_fingers_glb_<tag>.glb
```
exp01 and exp03 carry the SAME weight groups (identical vertex sets — `index_l` is 6,985
verts in both) but **different meshes**: 91,445 verts differ, by up to 12.5 cm. exp01 is
the pre-hand-fit body, so on it those groups land on the wrist/palm, overlapping the real
index finger by only 1.6 cm. Solving from exp01 therefore produces a body where the
arc-length param `s` never clears the phalanx-1 threshold and **every `_02`/`_03` finger
bone gets exactly zero weight** — rigid stick fingers hinging at the knuckle, and a torn
flat pose. It fails silently: weight sums are 1.0, the asserts pass, the export succeeds.
Confirm a good run by the label counts — from exp03, side l is index 4,000 / middle 4,718
(these match exp05's groups) and `chain-continuity repaired` is ~400 per side, not 0.
Verify a bake with `tools/handpose_bake_preview.py` + `tools/handpose_skin_to_obj.py` +
edge-stretch; judge on **real** tears (>=1mm rest length) and **visible** needles (>=1cm
posed), not raw ratios. Flat must stay at 0-1 real tears per hand — that pose ships.
Measurement tools for this lane: `tools/edge_stretch_cmp.py` (stretch with the real-tear /
visible-needle split, several builds side by side), `tools/skin_bone_territory.py` (how many
verts each finger bone actually owns — catches a starved chain), `tools/fin_bones.py`
(classifies torn edges by bone pair, which is what tells you *which* defect you are looking
at), `tools/handpose_trim_hand_obj.py` (trim an arm-sized skin dump to the hand, or a
bbox-framing renderer puts the hand in a corner).