Files
animation/characters/work/lena/README_lane_history.md
T

404 lines
24 KiB
Markdown
Raw Normal View History

# lena_nude — nude body variant (WIP, not released)
Lena's body with the underwear removed: no bra, no briefs, breasts with volume and **no
nipples**, smooth featureless crotch. Staged here until it is approved for the game.
| file | role |
|---|---|
| `lena_nude_quatskin_glb_v01.glb` | the body — 65-bone Quaternius game skeleton, texture **embedded** (self-contained) |
| `lena_nude_basecolor.png` | the baked albedo, loose, for further iteration |
Ship name at release is `Ariki_Female_QuatSkin_Nude.glb` in
`ariki-game/assets/quaternius/derived-bodies/`. Per `../../REGISTRY.md` rule 8 that rename happens
exactly once, on import — never create `Ariki_*` files in this repo.
## How it was made
Generators live in **this** repo (moved out of ariki-game 2026-08-06 — they author a character,
so `../../REGISTRY.md` governs them; see `.agents/wiki/ARCHITECTURE.md` "Tool provenance"). They
still *read* the canonical rigged bodies from the game repo (`Ariki_Female_QuatSkin.glb` and the
`_ChestV1` breast-sculpt donor), resolved via `$ARIKI_GAME` or a sibling `ariki-game/` checkout —
if neither resolves they abort with the path they tried, rather than reading the wrong file.
Run from `C:\Users\Jeremy\tinqs\animation`:
```
blender --background --python tools/_bake_nude_body_texture.py
# defaults to writing lena_nude_basecolor.png into THIS folder
blender --background --python tools/make_lena_nude_body.py -- \
--out characters/female/lena_nude/lena_nude_quatskin_glb_v01.glb \
--size 1.0 \
--texture characters/female/lena_nude/lena_nude_basecolor.png
python tools/verify_body_variant.py \
characters/female/lena_nude/lena_nude_quatskin_glb_v01.glb
```
`--size` is breast volume as a multiple of the `_ChestV1` sculpt (0.75 / 1.0 / 1.25 were
rendered for review; **v01 ships 1.0**). A new size is a **version bump**, not a new variant —
same recipe, different parameter. (This README and `../../REGISTRY.md` both called the flag
`--scale` until 2026-08-06; the script has always parsed `--size`.)
## Facts worth knowing before editing this
- **The underwear is the body surface.** A ray-crossing census finds exactly two surface crossings
at every height — thigh, belly, bra cup, briefs alike. There is no skin underneath, so the
garment can only be flattened into the skin, never deleted. Deleting it opens a hole, which is
precisely the defect in the game repo's `Ariki_Female_QuatSkin_Bare.glb` (black cavity across
the chest, visible even untextured). Do not use `_Bare` geometry for anything.
- **The base mesh is not watertight** (3,809 boundary edges). The painted underwear was hiding
open slits; with it gone they read as a dotted line of black triangles, so the builder welds
them shut in the torso band. That is where this file's lower vertex count comes from.
- **Use clay renders to judge geometry.** `tools/_render_body_closeup.py --clay` drops the
material; it is the only reliable way to tell a mesh defect from a painted one. Several dead
ends here came from mistaking painted bra shadow for a dent, and vice versa.
## Working files
This lane follows `.agents/rules/working-files.md`: **a version is a milestone,
not a step.** Fixing the bra produces one saved file at the end, not one per cut.
`hires_claude/.lanekeep` pins the only `.blend` files that survive here — the
masters named in the Derived-artifacts table of `../../REGISTRY.md`, plus the
live chain head. Everything else a step script wrote is scratch: the `NN_*.py`
recipe plus `masks.npz` regenerates it from the master above it.
```
python tools/prune_lane.py characters/female/lena_nude --recursive # dry run
```
Two things this lane learned the hard way:
- **Check for a running Blender before pruning.** A heal step takes ~7 minutes
and writes only at the end; the lane gained a new head (`17_healed.blend`)
during the audit that produced this section.
- `hires_work/` is the abandoned GLM-agent attempt (see its
`glm_run_attempt*.log`), superseded by `hires_claude/`. It has no `.lanekeep`,
so every `.blend` in it classifies as scratch.
The root-level `lena_sculpt_glb_v01-nude-Zephyr.blend` is hand-authored — no
step script rebuilds it — so the pruner reports it as UNKNOWN and never touches
it. Decide its fate by hand.
## Status
Verified against the canonical body: 1 primitive, identical attribute set (no tangents), material
`MI_Body_Lena`, 65 joints in identical order, height 1.777 m, inverse-bind drift 4.6e-6, weights
normalised, texture embedded. Posed sweep under a UAL clip is clean (no spikes or tearing).
Known cosmetic limits: a faint tonal seam where the fill meets untouched skin at extreme zoom, and
no skin-pore detail inside the filled area — that smoothness is also what guarantees no nipple or
crotch detail can appear.
Full history and the reasoning behind each choice: `~/.claude/plans/i-have-a-lena-velvet-ripple.md`.
## v04 — bust matched to the approved concept turnaround (2026-08-06)
`lena_nude_sculpt_glb_v04.glb`, master `hires_claude/34_v04.blend`. Recipes
`32_ref_measure.py` (measure) → `33_bust_variants.py` (size study) → `34_apply_bust.py` (k=0.5).
Guiding art: `exchange/INBOX/lena-base-turnaround/` — front/side/back T-pose renders, GPT Image 2,
approved by Can 2026-08-06.
**The reference is the same character, not a new one.** Measured against the front/side silhouettes:
| landmark | reference | v03 mesh |
|---|---|---|
| heads tall | 6.43 | 6.08 |
| waist / hip | 0.504 | 0.505 |
| waist width | 0.1260 H | 0.1318 H |
| hip width | 0.2500 H | 0.2608 H |
| **bust projection** | **+0.0156 H** | **+0.0210 H** |
Proportions already agree and the widths sit inside silhouette-threshold noise for generated art, so
only the bust was changed: the breast forms are rescaled to 50% of their projection off a
bi-harmonic chest wall, landing at +0.0155 H. Nothing outside the chest region moves (verified 0.0000 mm)
and height is unchanged, so every v03 heal survives.
Two traps for whoever tunes this next:
- **The projection metric saturates below k≈0.5** — k=0.35 measures +0.0154, indistinguishable from
k=0.5. Below that the "deepest slice" row migrates and stops tracking breast volume, so the number
cannot be pushed lower meaningfully. Treat 0.5 as a floor, not a midpoint. The reference reading may
also be inflated by the T-pose arm crossing the chest in a side view, which would mean the real
reference bust is smaller still.
- **Front renders cannot tell the sizes apart.** k=0.5, 0.65 and 0.8 look identical head-on because
the frontal key light flattens the chest. Judge from the side view only. Variants are in
`hires_claude/review_33/k{100,080,065,050,035}/`.
Re-running at another k takes seconds: `_bust_wall_cache.npy` holds the 8-minute chest-wall solve.
Not addressed, and the largest remaining gap to the concept art: the reference is a **smooth
mannequin** with no scan noise, while this mesh still carries speckles and dents on the hips and
thighs, plus the untouched original head.
## Decimated for the retexture / re-atlas lane (2026-08-06)
`lena_nude_decimated_glb_v01.glb` — v03 collapsed to the game-body budget (31,964 v / 64,957 tris,
ratio 0.037728) as input for `hires_claude/24_seams.py`. Recipe `30_decimate.py` + `30b_fix_winding.py`.
**Read `HANDOFF_decimated_v01.md` before working on it** — in particular, its carried UVs are scrap
(collapse shreds Tripo's 5,870-chart atlas) and it ships with 25 known backfacing triangles.
## RIGGED — `lena_nude_accurig_glb_v01.glb` (2026-08-07)
First rigged nude Lena. v10's mesh, byte-identical, plus a 101-bone AccuRig skeleton.
`45_accurig_export.py` (carrier out) → AccuRig (manual) → `46_graft.py` (skeleton in) →
`47_pose_qc.py`. Master `rig/lena_nude_accurig_glb_v01.blend`.
**No rig bait.** `tools/rigbait_decimate.py` exists because the source GLBs are 1.9M tris and
AccuRig refuses them; v10 is 62,929 tris, under the lane's own 78k bait budget, so it went in
whole. AccuRig returned the same 31,111 vertices **in the same order**, so weights copy
index-to-index — which deletes the decimated→full-res nearest-surface transfer where the July lane
mangled the fingers.
**Three traps, all of which produced a confidently wrong result before being caught:**
1. **AccuRig ships a `T-Pose` ACTION.** Blender's importer binds it as animation data, and an
action re-applies its pose on every evaluation *and every file load*. Clearing pose values
while it exists is futile: the in-session assertion passes, the saved file comes back posed,
and the body deforms **1.25 units into the air with every edge length unchanged**. Rigid
motion — which reads as "weights are fine, the model is gone". `animation_data_clear()` first,
then clear the pose, then assert.
2. **Verify the DEFORMED result, not the rest mesh.** The first graft asserted vertices hadn't
moved and passed — while the armature modifier displaced the whole body. The rest mesh proves
nothing; the modifier is what the engine runs.
3. **Never pose bones by substring.** `"Upperarm"` also matches `UpperarmTwist01/02`; rotating a
twist helper by its parent's angle compounds it and reports 14× edge stretch that looks like
broken weights and is entirely self-inflicted.
Scale (0.576058) and the carrier's 11.77 mm offset are recovered by least-squares fit, not
hardcoded, so a re-rig at a different height still lands.
**Still to come:** the AccuRig→Quaternius conversion. This carries 101 `CC_Base_*` bones; the game
standard is the 65-bone Quaternius contract.
## Athletic build — v10 (2026-08-07), textures/geometry base
`lena_nude_decimated_glb_v10.glb`, master `hires_claude/athletic/athletic_v04.blend`.
**v09's look, with a second geometry despeckle underneath it.** Recipe:
`39_despeckle.py --apply` (`SHARP_DEG=55 CLUSTER_MAX=20`) on `refill_v04/final_v06.blend`
`42_musculature.py` (strength 1.0, `SPINE=0.34`).
Ready to look at without any scripting: **open `athletic/lena_v10_turntable.blend` and press
spacebar** — the 50-second turntable is baked into the file.
Two things this pass settled:
- **Pick the threshold by sweep, not by feel.** At 55° the speckle caught rises 181 → 441 verts
while the anatomy classification is *unchanged* (8 long folds, exactly as at 70°). At 45° it
falls to 6 folds — real creases have started merging into the speckle set. 55° is the last
threshold that is still free.
- **Dissolve the stubborn clusters, do not pointmerge them.** Merging a cluster to a single point
leaves a fan vertex in the middle of where the flap was, and on a ring that is not planar that
vertex is a new spike: measured, it made folds >70° *worse* (361 → 444). Dissolving the
vertices and letting the ring close over improved every band — >30° 2138→1763, >50° 741→524,
>70° 361→304, >90° 268→196.
Converged: a third iteration is a wash (>70° 304 → 312). Mesh 31,504 → 31,111 v, height
unchanged. What is left at >70° is mostly the real creases the isolation test is protecting.
## Athletic build — v09 (2026-08-07), superseded by v10
`lena_nude_decimated_glb_v09.glb`, master `hires_claude/athletic/athletic_v03.blend`.
**v07's anatomy** (v08's additions rejected, left behind the `V08_EXTRAS` flag) with the back seam
fixed and a subtle spinal furrow. `42_musculature.py` at strength 1.0, `SPINE=0.34`.
**The seam down her back was self-inflicted, and the cause is worth remembering.** The spinal
groove sits at x = 0 — which is exactly where `24_seams.py` cuts the back midline. So the two
sides of the spine live on *opposite edges of the torso chart*, and the atlas-space gradient was
differentiating into padding rather than across her body. It drew a hard ragged line down her
whole back.
Fixed by taking the height field's gradient in **3D** and projecting it into per-texel tangent
frames. That never asks the atlas who a texel's neighbour is, so it is seam-correct by
construction — for the spine and equally for the inner-leg and arm seams, which would have hit
the same wall. Tangent maps are smoothed before use: rasterised per face they are
piecewise-constant, and a faceted tangent basis facets the normal map.
Two checks worth repeating on any future field:
- **Prove which line is yours.** Rendering with `SPINE=0.0` gives a back identical to v06, which
is what established the remaining soft line is the furrow and not a leftover artifact.
- **A falling max tilt can be good news.** It went 53.8° → 10.9°. The 53.8° *was* the seam
discontinuity, not anatomy; the real relief was never that steep.
Furrow trimmed to u 0.5200.795 so it fades at the sacrum. Run it to 0.470 and it reaches the
glute cleft, meets the crease already in the mesh, and reads ragged.
## Athletic build — v08 (2026-08-07), REJECTED — second definition pass
`lena_nude_decimated_glb_v08.glb`, master `hires_claude/athletic/athletic_v02.blend`,
`42_musculature.py` at strength 1.25. Adds clavicles, lower rib arch, the inguinal furrow ("V"),
glutes with cleft and fold, hamstrings, patella, tibialis, achilles and forearm mass.
**Two shape-function fixes mattered more than any of the new anatomy**, and both are worth
remembering whenever a field is turned into a normal map — because a normal map *is* the
derivative of the field, so anything that kinks the field draws a line:
- `band()` used a **linear clip ramp**, whose derivative jumps at both ends of the window. Every
window boundary was drawing a crease. It is a **smoothstep** now, which reaches each end with
zero slope.
- The front/back selector was the boolean `y < 0`. That terminates every front- or back-only
field abruptly at the silhouette — which is what drew the sickle lines down her shins. It is a
smooth blend over ~4 cm now.
- The per-leg `np.sign(sn) == sg` selector was dropped: it put a hard boundary at x=0 *and* was
redundant, since every leg field is already a function of `|x| - legc`.
## Athletic build — v07 (2026-08-07), first pass, superseded by v08
`lena_nude_decimated_glb_v07.glb`, master `hires_claude/athletic/athletic_v01.blend`.
Recipe `42_musculature.py <in> <out> [glb] [strength]`. **Maps only — geometry untouched, so this
is fully reversible and v06 remains the smooth build.**
Relief goes into the **normal map**, not painted shading. Darkening the albedo to fake a muscle
bakes one lighting direction into the skin: it reads as dirt the moment the key light moves and is
wrong in every pose. The albedo takes only a 5.5% cavity term, which is what subsurface actually
does and which survives relighting.
This is where the 14-chart atlas pays off a second time. A tangent-space normal *is* the surface
gradient in (u,v), so taking the gradient of a height field in atlas space gives one directly —
but only if texel density is uniform, or the same slope means different things in different
charts. This atlas measures 1.00x spread; on Tripo's 1.8x soup it would have needed per-chart
correction.
Two traps, both of which produced visible garbage before they were fixed:
- **Evaluate the anatomy per TEXEL, not per vertex.** Building it per vertex and interpolating
gave crumpled-paper noise: with ~10 mm edges an ab block is 2-3 vertices across, so the field's
gradient is piecewise-constant per triangle and the normal map follows the *triangulation*.
Rasterise position, then evaluate the anatomy at texel resolution.
- **Measure the landmarks.** The navel was assumed at u 0.523 and is actually at ~0.560 (the
front-midline profile peaks at 0.56 and the waist pulls in from 0.57), which put the whole ab
stack a band low. The quad centre line was assumed to drift inward going down; her stance has
the feet apart so it drifts *outward* (x 0.037 at the hip → 0.106 at the ankle) — `24_seams.py`
had already measured it, and using the guess laid the muscle bellies along the edges of the
thigh and left contour lines down the leg.
Breast and crotch are excluded by mask, so the featureless brief survives. Strength 0.55 / 1.0 /
1.45 rendered for review (`beauty_str_055`, `beauty_athletic_v01`, `beauty_str_145`); v07 ships
1.0. **This departs from the approved turnaround**, which is a smooth mannequin.
## Smooth body — v06 (2026-08-07)
`lena_nude_decimated_glb_v06.glb`, master `hires_claude/refill_v04/final_v06.blend`.
A second `40_texspeckle.py` pass on v05; geometry and UVs unchanged.
**Texture despeckling is done — do not run a third pass.** The passes converge fast and then stop
mattering:
| pass | texels touched | effect on the render |
|---|---|---|
| 1 | 111,212 (0.663%) | flecks visibly gone from chest and belly |
| 2 | 12,521 (0.075%) | 3,738 pixels over five views, max Δ 0.059 |
| 3 (measured, not run) | 4,426 (0.026%) | would be invisible |
Every mark still visible is **geometric** — the 361 edges still folding over 70°, mostly in tight
concave creases where collapsing them would cost real shape. More texture passes cannot reach
those, and past this point each pass trades impurity for plasticity by eroding the grain
`36_refill.py` deliberately transplanted.
## Body — v05 (2026-08-07), superseded by v06
`lena_nude_decimated_glb_v05.glb`, master `hires_claude/refill_v04/final_v05.blend`.
Recipes `39_despeckle.py --apply``40_texspeckle.py`.
**The speckle was two different defects wearing the same face**, and a clay render
(`38_clay.py`) is what separated them — as this README has said all along.
- **Geometry.** 892 edges folding over 70° in 220 isolated clusters. Detection needs *sharpness
AND isolation together*: deviation-from-smooth alone flags 33% of the body at this density —
her hips score like a defect — and sharpness alone would cut the real crease under the buttock.
Smoothing cleared 891 → 335 verts, then stalled: the survivors are **folded** flaps, whose
Laplacian target sits inside the fold, so relaxation cannot escape it. Those 126 clusters were
collapsed by pointmerge instead. Result 892 → 361 edges >70°, mesh 31,964 → 31,504 v.
- **Paint.** The rest was Tripo scan noise baked into the albedo and resampled faithfully by the
re-atlas. 31,457 texels of small local-contrast outliers inpainted, with blobs over 400 px and
the entire head excluded so lips, brows and eyes are never candidates.
## Body — v04 (2026-08-06), superseded by v05
`lena_nude_decimated_glb_v04.glb`**v03 with the breast and underwear region re-authored.**
Texture-only; geometry and UVs are v03's. Recipe `36_refill.py`.
The fill's low frequencies are a **harmonic solve on the mesh with Dirichlet boundaries**, not an
average of nearby skin. That distinction is the whole point: an IDW average (what `05_texture.py`
did) does not agree with the skin it abuts, so it lands as a decal with a tone step at the old
bra/briefs outline — the ghost. A harmonic fill *equals* her real skin at the mask edge by
construction, so a step is impossible rather than merely small. Solving on the mesh rather than in
the atlas also keeps the fill continuous across chart seams; the crotch alone straddles the
torso and leg charts.
Grain is transplanted back from clean skin tiles so it is not plastic, and the normal map is
flattened with `rm` set to the surrounding-skin median over the same texels — that flatness is what
guarantees no nipple or crotch detail can reappear.
Kills the rusty crotch patch. Against v03: mean pixel |Δ| 0.0028, **nothing over 0.15**, the change
concentrated in hip and chest. Her original texture is untouched outside the region.
## Re-atlased body — v03 (2026-08-06), superseded by v04
`lena_nude_decimated_glb_v03.glb` — **the v04 bust-matched sculpt, decimated and re-atlased in one
milestone.** Supersedes both files in the two sections below. 14 anatomical charts, material
exported `doubleSided=true`. Recipe `30_decimate.py``24_seams.py``31_reatlas.py` (~4 min).
**The grey tears across her face and underbust are fixed, and they were never holes** — they were
backface culling on inward-wound patches. Those patches are wound consistently *within themselves*,
so `normals_make_consistent` cannot see them (which is why `30b_fix_winding.py` oscillated and
settled at 25 stray triangles, far too few to explain a 15 cm band). Read
`HANDOFF_reatlas_v03.md` before re-treading any of it.
## Re-atlased (2026-08-06) — superseded by v03
`lena_nude_decimated_glb_v02.glb`**v01's geometry with a new UV atlas and resampled maps.**
Same 31,964 v / 64,957 tris; Tripo's 5,870 charts replaced by 14 anatomical ones (coverage
62.0 → 68.6%, texel-density spread 1.8× → 1.00×). Recipe `hires_claude/24_seams.py`
`hires_claude/31_reatlas.py`. It deliberately **looks identical** to v01 (mean pixel |Δ| 0.0012
across five views) — the win is a paintable, uniform-density layout, not a new look.
**Read `HANDOFF_reatlas_v02.md`**, especially the colorspace and unwrap-solver traps, and note that
the grey tears on the face/underbust are v01's dangling-flap defect and are still there.
## Hi-res sculpt v03 (2026-08-06) — Jeremy's three notes on v02
`lena_nude_sculpt_glb_v03.glb`, masters `hires_claude/27_clean.blend` (geometry) and
`29b_final.blend` (textured). Recipes: `17_line_heal.py``26_finish.py`
`25_speckle.py --apply``29_tone3d.py``08_export.py`.
| note | measured outcome |
|---|---|
| cut lines | shading kinks >12° 9704 → ~6900; flipped faces (the black dashes) 1511 → 10. Ribcage and thigh lines gone frontally; waistband + underbust still legible under raking light |
| discoloured bra/crotch | on-body tone gap, patch interior vs surrounding skin: 0.0102 → 0.0068 luma. The pale panels in `beauty_v02_healed/` are gone in `beauty_v03b/` |
| cleavage | sternum fillet radius 9.5 mm → no sharp sample at any height; notch depth 44 → 31 mm |
### Four dead ends this version ruled out — do not repeat them
1. **The lines are not cracks; do not weld.** Stage 15 assumed the panel seams were disconnected
vertex runs. They are not: the mesh has only 830 boundary edges in 689 loops of 35 edges, and
for kink verts the nearest vertex outside the 3-ring sits at 1.58× the local edge length
(median) against 1.73× for control skin. Welding at eps 1.2 mm — about the 1.85 mm mean edge
length — merged ordinary neighbours and tore the mesh, 830 → 24k boundary edges.
2. **They are not in the custom normals either.** Corner-vs-vertex deviation is 0.008° mean, 15
loops above 5°; clearing custom split normals changes nothing.
3. **A membrane is the wrong operator, and widening it is worse.** The relief is ONE VERTEX WIDE
(0.23 mm median at the centre, back to the 0.074 mm background by ring 2), so a collar-fixed
membrane pins itself to the defect's own shoulders. GROW 4 and 8 were no better than GROW 2
while moving material up to 38 mm. A **ring median** is correct: it deletes a 1-vertex outlier
and returns anything broader unchanged, so the underbust fold, clavicles and navel survive by
construction instead of by a tuned threshold.
4. **Never level tone from atlas-adjacent texels.** A Poisson solve whose boundary condition was
the mismatch against UV neighbours turned the patches into pale grey panels — UV adjacency is
not body adjacency, so the border mismatch came from other body parts and gutter. This is the
warning already in `05_texture.py`'s header. Sample on the body (KD over skin verts in 3D),
smooth the correction over the mesh graph, then rasterise.
Two process traps worth keeping: **restore the real material before saving**`18/20/25`
originally rendered clay last and then saved, so the file on disk kept the clay material and lost
its texture wiring, which is why the first tone attempt aborted with "could not find the wired
basecolor". And **the pristine textures are not inside working blends**: Blender purges the orphan
`.002/.003` copies on save, so read them from `00_welded.blend`.
Still open: 1649 non-manifold and 830 boundary edges — `bmesh.ops.holes_fill`, the edit-mode
`mesh.fill_holes` operator, and a per-loop pass all refused every one, which is why
`normals_make_consistent` could only reach 1198 of 1511 flipped faces before explicit per-face
reversal finished the job. None of this is propagated to the game body
(`lena_nude_quatskin_glb_v01.glb`), which is built independently on the 65-bone Quaternius rig.