Files
jeremy cf336f3b5b feat(lena-hands): exp09 graded cross-digit finger weights + honest tear metrics
The exp05 verdict ("weights alone cannot clear the bar on this mesh") was
measured against a hard partition that was itself causing much of the tearing.
exp01-exp05 gave every vert to exactly ONE digit -- 08_finger_weights.py had an
explicit `elif lo in FING: continue  # other digit: hard wall` -- which
guarantees the fused inter-digit bridges tear by the full finger separation,
because a single edge ring absorbs the whole gap.

Replace that with graded blending (WEB_BLEND_R0): a vert's fraction toward its
nearest other digit ramps 0 -> 0.5 as r = d_own/(d_own+d_other) goes 0.35 -> 0.5,
so both sides of the equidistance valley reach 50/50 and the field is continuous
across the boundary. Applied after the smoother (whose hard wall would erode beta
exactly where it must survive) and evaluated in the neighbour digit's own
arc-length frame. WEB_BLEND_SKIP excludes the thumb: its transform is opposition,
not curl, so its frame does not correspond to a finger's.

Measured against an identical baseline (same input, blend the only variable),
real tears (>=1mm rest length) drop 23-60% with NO regression on flat, the pose
that ships: fist_r 1178 -> 530, grip_r 561 -> 222, fist_l 979 -> 633,
grip_l 420 -> 322; total >5x 2487 -> 1307; p99.9 better on every pose;
flat unchanged at 0/1. fin_bones confirms the mechanism rather than just the
count -- the middle<->ring and pinky<->ring families leave the top classes while
the thumb/palm ones are untouched to the edge (182 -> 182, 148 -> 148).

This does NOT make fist/grip shippable: 222-633 real tears still reads as a
destroyed hand in clay renders, and the residual is now ~53% thumb-pad-fused-to
-palm, which is topology and needs mesh surgery or the v02 rebake. Flat and
relaxed are the shippable poses; fist/grip belong to the morph lane for now.

Also here:
- README: the solver's input is v02/..._exp03.glb, NOT exp01. exp01 is pre-hand
  -fit (converter steps 2b/2c); its finger groups sit on the wrist and overlap
  the real finger by 1.6cm, so a solve from it silently zeroes every _02/_03 bone
  -- rigid stick fingers and a torn flat -- while weight sums stay 1.0 and every
  assert passes. Cost three wasted bakes and one false "the solver regressed".
- Seed assert demanded >=100 seeds while the radius loop caps at
  SEED_AXIS_R_MAX, which left pinky (88 seeds at 16mm) can never satisfy; the two
  constants were mutually unsatisfiable. Now >=60, and it is documented as a
  sanity gate rather than a quality bar.
- Detwist poses tested at last: real but marginal (fist_r 38.7x -> 28.7x,
  grip_r 30.0x -> 17.3x, left hand flat). A knob, not a fix.
- edge_stretch_cmp.py / skin_bone_territory.py / handpose_trim_hand_obj.py:
  judge tears by rest length and absolute posed growth, not raw ratio; audit
  whether a bone owns any verts at all (thumb_01 owns ZERO in exp05); and trim
  an arm-sized skin dump to the hand before rendering.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:33:15 -07:00

207 lines
14 KiB
Markdown
Raw Permalink 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.
# 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).