MOTION (§41.1). The clip library goes 8 -> 46: ten idles, eight browse, eight sit/lean, eight social, six locomotion, six venue, in SIX grouped GLBs (one fetch each), 3.35 MB — LESS than the 4.29 MB the old eight cost, via lossless dedup + int16 rotations (worst error 0.0034 deg). All six verified skeleton-only (tris 0, meshes 0, nodes 66): ZERO DRAW, which is what makes this round affordable. No retarget was run and none was wanted — the bank and the peds are the same mixamorig skeleton, so retargeting would add error AND bake the ped mesh into the clip, ending zero-draw. MIRPAMO 'make smoke' green to prove the tool, then deliberately unused. Three seeds in the brief were duds and got substituted: the whole *_Degree_Turn set is RIFLE-AIMING, and two 'examine' clips are 262 KB static poses, not motion. R16 flat-body trap re-checked on all 46 (spine tilt 24 samples/clip, 0 frames >75 deg); turn_in_place auto-demoted from loopable on a 36.7 deg seam. RULING 1 — the Bandai hazard, closed non-destructively and better than specified. 3,077 CC BY-NC clips sat unzoned in a neutrally-named path. Renamed PER FILE (3,077/3,077) not just the directory, because mirpamo names output <rig>@<clip>.glb — the _NC-research marker now PROPAGATES into any retargeted GLB automatically. Nothing deleted; ultra's red/bandai re-verified as canonical. Ledger corrected: the manifest claimed CC-BY-NC-ND, the bundled licence text says CC BY-NC, no ND. PROPS (§41.2). 110 assets from three libraries, published, sha1-verified, 0 validator errors both modes. THE FINDING: every handover number was a TRIANGLE count, and triangles were never the binding constraint — DRAW CALLS were. As handed over this cargo cost 14,140 draws against a 162-draw margin; it ships at 117. '3dstore passes as-is' was true on tris/metres/ Draco/textures and FALSE on draws — 30 of 46 files carried up to 16 materials on one mesh (one per record sleeve), so a 1,020-tri tub cost 16 draws; baked to COLOR_0, 288 -> 40, zero tri drift, A/B identical. dj-gear's own manifest claimed median 6,288 tris / 18 under budget; measured 51,528 and 12, with draws to 2,145. Pub props were NOT metre-correct (every source unit-normalised to max dim 1.00 m) and 40x decimation was impossible as specified (loungeChair is 94% non-manifold). Ruling 5 vs the draw budget was a real conflict (one mixer = 4.6x a whole room); resolved by joining SCENERY selectively while every control node keeps its object/name/pivot — verified by parsing the SHIPPED GLB, not the tool that wrote it: PASS 4 / STATIC 10 / N/A 17 / FAIL 0. deck_1200_rigged = 5 draws with Platter_SPIN, Arm_YAW, Fader_PITCH, Btn_STARTSTOP intact. The R40 four-surface transmission gate FIRED ON LIVE CARGO: 5 genuinely transmissive materials (cartridge dust-covers, a mixer meter window) that would each have doubled every opaque draw in the room. 9 assets rejected on eyeball after decimation rather than shipping mush. Two existing-tooling bugs fixed: normalize.py's yaw/up were silent no-ops (the jukebox exported 0.40 m instead of 0.97 m), and footprints now measure the shipped GLB. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1011 lines
72 KiB
Markdown
1011 lines
72 KiB
Markdown
# LANE E — ASSETS · progress (PROCITY-E)
|
||
|
||
*Owner: PROCITY-E. Scope: `pipeline/*`, `web/assets/*`. Nothing in other lanes' files was touched.*
|
||
|
||
> ⚠ **THIS FILE STOPS AT ROUND 12 AND HAS SINCE R13. IT IS NOT THE LIVE RECORD.** Everything from
|
||
> Round 13 on lives in **[`docs/LANES/LANE_E_NOTES.md`](docs/LANES/LANE_E_NOTES.md)** (what other
|
||
> lanes need from this one, newest first) and **[`pipeline/AUDIT.md`](pipeline/AUDIT.md)**
|
||
> (provenance, licence zone, measurements and method, per round). Recorded in R39 rather than
|
||
> quietly resurrected: a progress file whose last entry is 27 rounds old is a trap for the next
|
||
> reader, and the honest fix is a signpost, not a backfill. *(Two exceptions below: the R40 tasking
|
||
> explicitly asked for its control-arm transcripts in THIS file, and the R41 §41.2 tasking asked for
|
||
> its three import tables here, so §40 and §41 live here AND are cross-referenced from the notes.)*
|
||
|
||
## Round 41 (§41.2 — THE FITTINGS) · 2026-08-07
|
||
|
||
*Entry here by explicit R41 tasking (as R40 was). Cross-lane hand-off for C in
|
||
`docs/LANES/LANE_E_NOTES.md` §41; provenance in `pipeline/AUDIT.md`.*
|
||
|
||
**110 assets landed** from three libraries, all interior (ruling 4), all published + sha1-verified,
|
||
validator green. The round's one recurring lesson: **every handover number was a triangle count,
|
||
and triangles were never the binding constraint — draw calls were.**
|
||
|
||
| | source | shipped | rejected | tris | draws |
|
||
|---|---|---|---|---|---|
|
||
| `~/Documents/3dstore` | 46 | **40** | 6 | 18,420 | 288 → **40** |
|
||
| `not-tonight/art_incoming` | 48 | **39** | 3 (+6 `staff*` skipped) | 9,508,361 → 194,837 | 39 |
|
||
| ultra `3D=models/dj-gear` | 60 | **31** | 29 not selected | 2,284,094 → 143,340 | 13,813 → **38** |
|
||
| **total** | **154** | **110** | — | 11,810,875 → **356,597** | 14,140 → **117** |
|
||
|
||
### The finding that shaped the round: draws, not tris
|
||
|
||
The tasking said 3dstore "passes the house law AS-IS". On tris, metres, Draco and textures that is
|
||
exactly true and I re-verified all four. It is false on the number Lane C spends: **30 of the 46
|
||
files carry 3–16 materials on a single mesh** (one flat colour per record sleeve,
|
||
`Mat_Sleeve_<hex>`), and glTF gives each material its own primitive, which `GLTFLoader` turns into
|
||
its own `Mesh`. A 1,020-tri "Black Tub" cost **16 draw calls**. The interior margin is 162 — ten
|
||
tubs would have been the whole room.
|
||
|
||
Every one of those materials is a flat `baseColorFactor` with no texture and no UVs, so
|
||
`pipeline/merge_prims.py` bakes each primitive's colour into a `COLOR_0` attribute and emits one
|
||
white-factor material. glTF multiplies `COLOR_0` by `baseColorFactor` and both are linear, so the
|
||
result is **numerically the original colour** — verified by A/B render
|
||
(`docs/shots/laneE/r41_3dstore_merge_ab.png`) and by zero triangle drift on all 40.
|
||
|
||
The same defect, unmeasured, was in the DJ gear: joined bodies still carried 5–31 materials, so
|
||
"one mesh" was still 5–31 draws. `merge_prims.py` over that set took it **530 → 38**.
|
||
|
||
### §41.2a — `~/Documents/3dstore` · 40 shipped, 6 rejected
|
||
|
||
Re-measured with `glb_stat.py`, not quoted: 45 of 46 are ≤5k tris (median 384), metre-correct,
|
||
+Y up, no Draco, no textures. **44 of 45 are base-origin** — `vinylsleeve_v5` is centred
|
||
(minY −0.160) and was re-seated. `3dmrp.glb` confirmed at **499,948 tris** + a 4096px texture.
|
||
|
||
No Blender pass: the geometry is already correct and every extra tool is a chance to move it.
|
||
Spec: `pipeline/r41_3dstore.json` (by name, ruling 3) · stager: `pipeline/r41_stage_3dstore.py`.
|
||
|
||
| id | kind | tris | draws | footprint (m) | height (m) |
|
||
|---|---|---:|---|---|---:|
|
||
| `beam_roof` | arch | 12 | 1 → 1 | 6.5 × 0.32 | 0.36 |
|
||
| `column_square` | arch | 12 | 1 → 1 | 0.22 × 0.22 | 2.6 |
|
||
| `column_wide` | arch | 12 | 1 → 1 | 0.36 × 0.46 | 2.6 |
|
||
| `door_shop` | arch | 144 | 1 → 1 | 1.7 × 0.19 | 2.3 |
|
||
| `bin_endoffunk` | bin | 972 | 16 → 1 | 0.36 × 0.34 | 0.34 |
|
||
| `chiu_bin` | bin | 996 | 16 → 1 | 0.37 × 0.43 | 0.35 |
|
||
| `chiu_bin_half` | bin | 600 | 16 → 1 | 0.24 × 0.43 | 0.35 |
|
||
| `chiu_bin_low` | bin | 912 | 16 → 1 | 0.34 × 0.43 | 0.35 |
|
||
| `crate_blue` | bin | 936 | 16 → 1 | 0.34 × 0.3 | 0.322 |
|
||
| `mos_bin_bottom` | bin | 912 | 16 → 1 | 0.34 × 0.4 | 0.34 |
|
||
| `mos_bin_lower_mid` | bin | 912 | 16 → 1 | 0.34 × 0.36 | 0.34 |
|
||
| `mos_bin_top` | bin | 912 | 16 → 1 | 0.34 × 0.2 | 0.34 |
|
||
| `mos_bin_upper_mid` | bin | 912 | 16 → 1 | 0.34 × 0.26 | 0.34 |
|
||
| `rack_bin_wooden` | bin | 912 | 16 → 1 | 0.34 × 0.4 | 0.338 |
|
||
| `tub_black` | bin | 1020 | 16 → 1 | 0.36 × 0.53 | 0.229 |
|
||
| `tub_black_long` | bin | 1536 | 16 → 1 | 0.53 × 0.36 | 0.229 |
|
||
| `tub_white` | bin | 972 | 16 → 1 | 0.34 × 0.53 | 0.229 |
|
||
| `tub_white_long` | bin | 1536 | 16 → 1 | 0.53 × 0.36 | 0.229 |
|
||
| `counter_station` | counter | 192 | 1 → 1 | 0.88 × 0.5 | 0.87 |
|
||
| `counter_station_side` | counter | 192 | 1 → 1 | 0.5 × 0.88 | 0.87 |
|
||
| `vinyl_sleeve` | prop | 180 | 4 → 1 | 0.32 × 0.01 | 0.32 |
|
||
| `window_booth` | prop | 144 | 1 → 1 | 1.85 × 0.09 | 0.72 |
|
||
| `bin_big_4x4` | rack | 288 | 4 → 1 | 0.59 × 1.5 | 1.255 |
|
||
| `chiu_rack_long` | rack | 300 | 3 → 1 | 1.88 × 0.43 | 0.795 |
|
||
| `chiu_rack_short` | rack | 276 | 3 → 1 | 1.38 × 0.43 | 0.795 |
|
||
| `mos_stack` | rack | 204 | 5 → 1 | 0.37 × 0.4 | 1.275 |
|
||
| `tubs_3x3` | rack | 168 | 3 → 1 | 0.53 × 1.3 | 0.555 |
|
||
| `tubs_3x3_long` | rack | 168 | 3 → 1 | 1.3 × 0.53 | 0.51 |
|
||
| `tubs_4x2_vertical` | rack | 240 | 3 → 1 | 0.53 × 1.56 | 0.475 |
|
||
| `tubs_4x2_wide` | rack | 240 | 3 → 1 | 1.56 × 0.53 | 0.475 |
|
||
| `tubs_shelf_long` | rack | 228 | 3 → 1 | 1.56 × 0.55 | 0.565 |
|
||
| `tubs_shelf_wide` | rack | 228 | 3 → 1 | 0.55 × 1.56 | 0.575 |
|
||
| `wooden_rack_long` | rack | 384 | 3 → 1 | 0.78 × 1.8 | 0.81 |
|
||
| `wooden_rack_wide` | rack | 384 | 3 → 1 | 1.8 × 0.78 | 0.93 |
|
||
| `library_trolley` | trolley | 264 | 4 → 1 | 0.41 × 0.88 | 0.74 |
|
||
| `aircon_unit` | wall_mount | 12 | 1 → 1 | 1.15 × 0.25 | 0.35 |
|
||
| `shelf_board_long` | wall_mount | 12 | 1 → 1 | 4.6 × 0.48 | 0.03 |
|
||
| `shelf_board_short` | wall_mount | 12 | 1 → 1 | 0.48 × 1.95 | 0.03 |
|
||
| `wall_shelves_big` | wall_mount | 36 | 3 → 1 | 4.65 × 0.05 | 0.76 |
|
||
| `wall_shelves_long` | wall_mount | 48 | 4 → 1 | 2.6 × 0.05 | 1.1 |
|
||
|
||
**Rejected (6):**
|
||
|
||
| source | why |
|
||
|---|---|
|
||
| `3dmrp.glb` | 499,948 tris (100x the 5k prop budget) + a 4096px texture. Named in the tasking as the one reject; measured and confirmed. |
|
||
| `DOOR.glb` | 0.010 m thick — a 1 cm plane, 12 tris. The solid door is DOOR-front.glb (0.19 m), which is accepted as door_shop. |
|
||
| `GREY COLUMN.glb` | 0.010 m thick plane, 12 tris. The solid post is GREY COLUMN-FRONT.glb (0.22 x 0.22 m), accepted as column_square. |
|
||
| `Shelf Level.glb` | 4.50 x 0.05 x 0.005 m — a 5 mm trim rail from the booth_room build script, not a placeable fitting. |
|
||
| `Shelf Level long.glb` | 2.60 x 0.05 x 0.005 m — same 5 mm trim rail, short version. |
|
||
| `floor crate.glb` | 0.34 x 0.34 x 0.002 m — a 2 mm floor decal. Would z-fight Lane C's interior floor; a real crate is crate_blue. |
|
||
|
||
### §41.2b — DJ gear · 31 shipped, node contract 4/4 PASS
|
||
|
||
**Ruling 5 and the draw budget were in direct conflict, and nobody had measured the second half.**
|
||
`gltf-transform optimize` is banned because it deleted `Fader_CROSS` — correct. But the reason it
|
||
was reached for is also real: measured on ultra's exports, these models cost **TR-1000 2,145 ·
|
||
MPC-XL 1,319 · TR-808 1,092 · PMC-08Pro 751 · TTM54i 659 draws**. Against a 162-draw margin, one
|
||
PMC-08Pro is 4.6× an entire room. Both "join everything" and "join nothing" ship something
|
||
unusable.
|
||
|
||
*(Also re-measured: the handover's "median 6,288 tris, 18 already ≤5k" is the stale
|
||
`dj-gear/manifest.json`. True median is **51,528 tris**; only **12** of 60 are ≤5k.)*
|
||
|
||
`pipeline/rig_join.py` joins **selectively**: scenery merges to one body, each contract node keeps
|
||
its own object, name and pivot. Two variants ship — `static` (dressing, 1 draw) and `rigged`
|
||
(drivable hero, 2–5 draws). Contract verified by `pipeline/check_contract.py`, which parses the
|
||
**shipped GLB's** node list rather than trusting the tool that wrote it.
|
||
|
||
| id | mode | tris | draws | source contract | shipped contract | verdict |
|
||
|---|---|---|---|---|---|---|
|
||
| `deck_1200_rigged` | rigged | 254,048 → 3,821 | 62 → 5 | Platter_SPIN,Arm_YAW,Fader_PITCH,Btn_STARTSTOP | Platter_SPIN,Arm_YAW,Fader_PITCH,Btn_STARTSTOP | PASS |
|
||
| `mixer_pmc05pro_rigged` | rigged | 61,872 → 5,000 | 525 → 2 | Fader_CROSS | Fader_CROSS | PASS |
|
||
| `mixer_shdj1200_rigged` | rigged | 86,577 → 4,999 | 776 → 2 | Fader_CROSS | Fader_CROSS | PASS |
|
||
| `mixer_ttm54i_rigged` | rigged | 75,788 → 5,000 | 659 → 2 | Fader_CROSS | Fader_CROSS | PASS |
|
||
| `cart_case` | static | 6,912 → 5,000 | 79 → 1 | — | — | N/A |
|
||
| `cart_concorde` | static | 2,853 → 2,853 | 34 → 1 | — | — | N/A |
|
||
| `cart_m447` | static | 1,387 → 1,387 | 25 → 1 | — | — | N/A |
|
||
| `cart_stanton` | static | 3,290 → 3,290 | 15 → 1 | — | — | N/A |
|
||
| `deck_1200` | static | 254,048 → 5,000 | 62 → 1 | Platter_SPIN,Arm_YAW,Fader_PITCH,Btn_STARTSTOP | — | STATIC (contract intentionally dropped) |
|
||
| `drum_808` | static | 118,569 → 5,000 | 1092 → 1 | — | — | N/A |
|
||
| `drum_tr1000` | static | 119,959 → 4,897 | 2145 → 1 | — | — | N/A |
|
||
| `headphones_hd25` | static | 8,495 → 4,999 | 169 → 1 | — | — | N/A |
|
||
| `headshell_technics` | static | 2,097 → 2,097 | 30 → 1 | — | — | N/A |
|
||
| `mixer_mx2200` | static | 69,127 → 5,000 | 411 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
|
||
| `mixer_pmc05pro` | static | 61,872 → 5,000 | 525 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
|
||
| `mixer_pmc06pro` | static | 59,633 → 5,000 | 506 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
|
||
| `mixer_pmc07pro` | static | 64,752 → 4,999 | 420 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
|
||
| `mixer_pmc08pro` | static | 137,885 → 5,000 | 751 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
|
||
| `mixer_pmc10` | static | 72,950 → 4,999 | 522 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
|
||
| `mixer_pmc20` | static | 91,127 → 5,000 | 775 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
|
||
| `mixer_pmx9000` | static | 130,053 → 5,000 | 807 → 1 | — | — | N/A |
|
||
| `mixer_shdj1200` | static | 86,577 → 5,000 | 776 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
|
||
| `mixer_ttm54i` | static | 75,788 → 4,999 | 659 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) |
|
||
| `sampler_mpc3000` | static | 102,536 → 5,000 | 480 → 1 | — | — | N/A |
|
||
| `sampler_s900` | static | 86,536 → 5,000 | 443 → 1 | — | — | N/A |
|
||
| `sampler_sp1200` | static | 158,218 → 5,000 | 481 → 1 | — | — | N/A |
|
||
| `sl_deck_1` | static | 19,139 → 5,000 | 113 → 1 | — | — | N/A |
|
||
| `sl_deck_2` | static | 19,241 → 5,000 | 97 → 1 | — | — | N/A |
|
||
| `sl_deck_3` | static | 23,347 → 5,000 | 141 → 1 | — | — | N/A |
|
||
| `sl_deck_4` | static | 19,875 → 5,000 | 161 → 1 | — | — | N/A |
|
||
| `zip_drive` | static | 9,543 → 5,000 | 72 → 1 | — | — | N/A |
|
||
|
||
**`STATIC` = the contract is dropped by design** (a dressing copy is one joined mesh and is not
|
||
claimed to be drivable); **`N/A`** = the source carried no contract node (a cartridge has no
|
||
crossfader). **FAIL: 0.** Excluded as out-of-period: DIGITAKT-II, DIGITONE-II, POLYEND-DRUMS,
|
||
MPC-XL (2020s gear); the rest of the 60 are near-duplicate colourways.
|
||
|
||
### §41.2c — pub/venue props · 39 shipped, 3 rejected, 6 `staff*` skipped
|
||
|
||
Two handover claims failed re-measurement. **These are not metre-correct**: every source is
|
||
unit-normalised to a max dimension of exactly 1.00 m, so a picnic table and an ashtray are the same
|
||
size. Aspect *is* preserved, so a real-world height (read off each `*_shot.png` reference render)
|
||
fixes all three axes — the height table is `pipeline/r41_pub_batch.json`. And **textures are not
|
||
"all fine"**: 37 of 39 have no texture at all (they carry `COLOR_0` vertex colour), while `keg` and
|
||
`urinal_trough` carried 2048px maps, resized to 1024.
|
||
|
||
**40× decimation was impossible as specified.** COLLAPSE decimation cannot merge across overlapping
|
||
shells: `loungeChair` is **94% non-manifold** (376,076 of 399,542 verts) and a ratio-0.01 decimate
|
||
moved it 1,319,836 → 1,274,119, i.e. **3%**. `topiary` behaved identically at 93%; `pokie`, at 29%,
|
||
decimated normally. So `normalize.py` gained a `remesh` path (voxel remesh → decimate → vertex-colour
|
||
transfer), which needed two fixes found by measuring rather than assuming:
|
||
|
||
- **colour came back empty** until `bpy.ops.object.datalayout_transfer()` created the destination
|
||
layers — a Data Transfer modifier applied without it silently transfers nothing;
|
||
- **a voxel remesh needs a volume**, so thin-walled sources (`keg`) returned a wire cage at every
|
||
voxel size until a Solidify pass preceded the remesh
|
||
(`docs/shots/laneE/r41_shellsoup_solidify.png`).
|
||
|
||
| id | src tris | out tris | ratio | non-manifold | footprint (m) | height (m) |
|
||
|---|---:|---:|---:|---:|---|---:|
|
||
| `ash_urn` | 198,782 | 5,000 | 40× | 43% | 0.32 × 0.33 | 0.75 |
|
||
| `ashtray_post` | 198,710 | 5,000 | 40× | 42% | 0.41 × 0.41 | 0.95 |
|
||
| `bain_marie` | 184,992 | 4,958 | 37× | 83% | 0.89 × 0.76 | 0.95 |
|
||
| `banquette` | 190,350 | 4,999 | 38× | 68% | 1.55 × 1.02 | 0.85 |
|
||
| `beer_umbrella` | 192,752 | 4,995 | 39× | 60% | 2.68 × 2.65 | 2.3 |
|
||
| `bin_round` | 197,576 | 4,999 | 40× | 40% | 0.53 × 0.51 | 0.75 |
|
||
| `bistro_table` | 196,995 | 4,999 | 39× | 38% | 1.18 × 1.18 | 0.92 |
|
||
| `broken_basin` | 197,978 | 5,000 | 40× | 53% | 0.59 × 0.49 | 0.6 |
|
||
| `cable_snake` | 193,623 | 5,000 | 39× | 64% | 0.45 × 0.47 | 0.2 |
|
||
| `can_stack` | 196,005 | 5,000 | 39× | 46% | 0.38 × 0.32 | 0.45 |
|
||
| `cdj_deck` | 197,967 | 4,998 | 40× | 33% | 0.63 × 0.42 | 0.15 |
|
||
| `champ_bucket` | 183,943 | 4,999 | 37× | 62% | 0.24 × 0.2 | 0.35 |
|
||
| `concrete_plinth` | 190,528 | 5,000 | 38× | 53% | 0.76 × 0.74 | 1.2 |
|
||
| `crowd_barrier` | 189,524 | 5,000 | 38× | 60% | 1.97 × 0.3 | 1.1 |
|
||
| `distro_box` | 193,633 | 5,000 | 39× | 41% | 0.63 × 0.87 | 1.2 |
|
||
| `fire_ext` | 198,707 | 5,000 | 40× | 37% | 0.34 × 0.19 | 0.6 |
|
||
| `firepit` | 188,041 | 5,000 | 38× | 88% | 0.76 × 0.56 | 0.5 |
|
||
| `flight_case` | 195,452 | 5,000 | 39× | 37% | 0.6 × 0.36 | 0.75 |
|
||
| `jukebox` | 184,271 | 4,999 | 37× | 75% | 0.97 × 0.41 | 1.52 |
|
||
| `keg` | 441,861 | 5,000 | 88× | 23% | 0.4 × 0.4 | 0.6 |
|
||
| `lounge_chair` | 1,319,156 | 5,000 | 264× | 94% | 0.69 × 0.69 | 0.8 |
|
||
| `marble_sink` | 199,095 | 4,998 | 40× | 21% | 1.15 × 0.56 | 0.85 |
|
||
| `old_couch` | 196,277 | 5,000 | 39× | 46% | 1.8 × 0.95 | 0.85 |
|
||
| `pallet` | 198,086 | 5,000 | 40× | 30% | 0.79 × 0.73 | 0.2 |
|
||
| `picnic_table` | 197,370 | 4,999 | 39× | 42% | 1.38 × 1.41 | 0.75 |
|
||
| `pie_warmer` | 196,434 | 5,000 | 39× | 31% | 0.61 × 0.71 | 0.55 |
|
||
| `planter` | 239,862 | 4,897 | 49× | 87% | 0.98 × 0.96 | 0.55 |
|
||
| `plywood_bar` | 192,907 | 5,000 | 39× | 63% | 1.51 × 0.82 | 1.05 |
|
||
| `pokie` | 198,030 | 5,000 | 40× | 29% | 0.75 × 0.78 | 1.7 |
|
||
| `raffle_drum` | 193,830 | 5,000 | 39× | 68% | 0.37 × 0.37 | 0.9 |
|
||
| `rope_post` | 199,748 | 5,000 | 40× | 27% | 0.32 × 0.32 | 1.0 |
|
||
| `sandwich_board` | 198,359 | 5,000 | 40× | 25% | 0.58 × 0.56 | 1.0 |
|
||
| `smoke_machine` | 194,423 | 5,000 | 39× | 52% | 0.17 × 0.24 | 0.38 |
|
||
| `speaker_stack` | 186,357 | 5,000 | 37× | 59% | 0.52 × 0.52 | 1.8 |
|
||
| `strobe_light` | 193,392 | 5,000 | 39× | 57% | 0.22 × 0.24 | 0.4 |
|
||
| `sub_bass` | 193,581 | 4,998 | 39× | 53% | 0.87 × 0.63 | 0.7 |
|
||
| `topiary` | 415,872 | 4,999 | 83× | 93% | 0.61 × 0.61 | 1.2 |
|
||
| `urinal_trough` | 488,808 | 5,000 | 98× | 6% | 0.31 × 0.26 | 0.7 |
|
||
| `wheelie_bin` | 195,084 | 5,000 | 39× | 42% | 0.72 × 0.92 | 1.1 |
|
||
|
||
**Rejected (3) — eyeballed against each source's `*_shot.png`, per the house rule:**
|
||
|
||
| id | src tris | why |
|
||
|---|---:|---|
|
||
| `balustrade` | 198,229 | glass panel + handrail. The remesh lost the handrail entirely and left a lumpy slab with a detached lump — a transparent thin panel is the worst case for a signed-distance remesh. Eyeballed against balustrade_shot.png; unusable. |
|
||
| `cocktail_bar` | 194,298 | back-bar with a lit bottle display. At 5k the bottle shelving smears into a grey L-shaped mass; nothing about it reads as a bar. Rejected rather than shipping mush. |
|
||
| `dj_plinth` | 197,159 | the white plinth survives but the controller on top melts into a grey blob. Nothing is lost: cdj_deck normalized cleanly and Lane C can stand it on any counter or the plywood_bar. |
|
||
|
||
Plus the **6 `staff*.glb` skipped** as instructed (200k-tri statues, no skin/anim/material — and
|
||
PROCITY has a rigged ped fleet, so a photogrammetry human statue would read wrong beside it).
|
||
|
||
### Gate, publish, validate
|
||
|
||
**The R40 four-surface transmission gate earned its keep on live imports, not a fixture.** Sweeping
|
||
the 110 new assets caught **5 real `KHR_materials_transmission` materials** — cartridge dust-covers
|
||
(`cc_clear`, `n44_clear`, factor 0.85–0.9) and a mixer meter window (`meter_window`, 0.9). Each
|
||
would have made three.js issue *every opaque draw in that room twice*. `strip_transmission.py`
|
||
removed all 5 with **BIN chunk byte-identical** (sha1 printed per file). This is the control arm
|
||
R40 demonstrated with a synthetic fixture, firing on real cargo.
|
||
|
||
Full house-law sweep over all 110 — tris ≤5k, WebP ≤1024, no Draco, base-origin |minY| ≤5 mm, no
|
||
transmission, and `_normalized/` byte-identical to the `web/assets/models/` served mirror:
|
||
**0 violations.**
|
||
|
||
```
|
||
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/publish.py --files pipeline/_r41_publish_list.txt
|
||
published 110/110 this run → _published.json now 169 (was 59).
|
||
✓ _published.json matches the live depot (all 169 recorded assets live).
|
||
|
||
$ sha1 verify (fetch each back from the depot, compare bytes)
|
||
110/110 identical, 0 mismatched # → pipeline/_r41_sha1.json
|
||
32.4 MB on the depot
|
||
|
||
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/validate_manifest.py
|
||
manifest v1 — facades 49, skins 92 groups, GLBs 149 (+40 non-manifest GLBs transmission-swept), audio 34
|
||
OK — 0 errors, 0 warning(s) # rc=0
|
||
|
||
$ ... validate_manifest.py --depot # stricter: every GLB must be live on the depot
|
||
OK — 0 errors, 0 warning(s) # rc=0
|
||
```
|
||
|
||
`manifest.json` fittings **23 → 133**. Every new entry carries `footprint` [w,d] and `height` in
|
||
metres measured **from the shipped GLB** (not from the Blender scene that wrote it — `rig_join`'s
|
||
in-scene bounds mis-reported a 0.114 m mixer as 0.629 m, which is why the staging step re-measures
|
||
the file), plus `kind`, `tris` and `draws` so Lane C can spend the 162-draw margin deliberately.
|
||
DJ entries add `mode`, `control_nodes`, `source_controls` and `drivable`.
|
||
|
||
### Tools added / changed (all in `pipeline/`)
|
||
|
||
| file | what |
|
||
|---|---|
|
||
| `merge_prims.py` | **new** — bake `baseColorFactor` → `COLOR_0`, collapse N primitives to 1. The draw-cost fix. |
|
||
| `contact_sheet.py` | **new** — labelled multi-GLB sheet in one Blender run, the eyeball tool. |
|
||
| `thumbs.py` | **new** — EEVEE 256px thumbs; workbench `TEXTURE` renders vertex-coloured, material-less meshes flat grey, which is 2 of this round's 3 libraries. |
|
||
| `rig_join.py` | **new** — the selective join; static + contract-preserving rigged variants. |
|
||
| `check_contract.py` | **new** — contract assertion read from the shipped GLB. |
|
||
| `normalize.py` | `remesh` / `shell_thickness` / `vertex_colour` options + `nonmanifold_ratio()`; explicit `COLOR_0` export; **orientation bug fixed** — `yaw`/`up` were applied without re-selecting the object, so `transform_apply` silently no-opped (the jukebox came out 0.40 m wide instead of 0.97 m). Now it re-selects and *verifies the dimensions changed*, warning if not. |
|
||
| `publish.py` | `--files LIST.txt` — publish an explicit set; `_normalized/` is shared with other lanes and a blanket run would push their half-written staging. |
|
||
| `build_manifest.py` | reads the three R41 result files; new entries carry kind/tris/draws/control-nodes. |
|
||
| `r41_3dstore.json`, `r41_pub_batch.json`, `r41_djgear_batch.json` | the by-name specs (ruling 3). |
|
||
|
||
### Proof shots (`docs/shots/laneE/`)
|
||
|
||
`r41_3dstore_40.png` · `r41_3dstore_merge_ab.png` (16-material source vs 1-draw merge, identical) ·
|
||
`r41_pub_42.png` · `r41_pub_before_after.png` · `r41_pub_rejects.png` (the 3 rejects beside their
|
||
sources) · `r41_shellsoup_solidify.png` (the keg cage vs the solidify fix) · `r41_djgear_31.png` ·
|
||
`r41_djgear_rigged.png` · `r41_djgear_merge_ab.png`.
|
||
|
||
### For Lane C (§41.4)
|
||
|
||
Everything is in `manifest.json` under `fittings`, keyed by the ids in the tables above. Place on
|
||
`footprint`/`height` untouched. Budget notes: **every static asset is 1 draw**; the four `*_rigged`
|
||
DJ heroes are 2–5 draws each and are the only ones with drivable `control_nodes`. The `-long`/
|
||
`-wide` 3dstore pairs are the same mesh rotated 90° — pick one, they are not two props. `kind`
|
||
tells you what it stands on: `bin`/`rack`/`counter`/`trolley` are floor-standing, `wall_mount`
|
||
needs a wall and a height, `arch` is structure, `prop` is a surface dressing.
|
||
|
||
## Round 40 (§40.1+40.4) · 2026-08-04 — *(entry here by explicit R40 tasking; cross-lane story in `docs/LANES/LANE_E_NOTES.md` §40)*
|
||
|
||
### §40.1 — transmission gate blind spot CLOSED, both control arms demonstrated
|
||
|
||
`pipeline/validate_manifest.py` `check_transmission()` now covers every path the game can load a
|
||
GLB from: `pipeline/_normalized/` (as before), **`web/assets/models/`** (the `?localdepot=1` served
|
||
mirror — never scanned before), **the depot copy** for any manifest GLB with no local file in either
|
||
dir (ranged GET of the GLB JSON chunk; 206-aware, slices a 200 — the tailnet depot returns 200-full),
|
||
and a **sweep of every non-manifest `.glb`** in both dirs (ped rigs, dance clips — `rigs.js` loads
|
||
them by literal path). A depot fetch failure is a loud WARN (`depot copy UNSCANNED`), never a skip.
|
||
|
||
**Fixture:** `procity_fit_box_crate_01.glb` with `transmissionFactor: 1` injected into material[0]
|
||
(the exact defect `strip_transmission.py` removes; injector kept in the session scratchpad, BIN
|
||
untouched). Clean sha1 `6c327d8624bc90203275a341128abb546f5d69e0` (staging = served, verified after
|
||
cleanup). **Depot arm used a LOCAL MOCK depot (`python3 -m http.server` serving `a/<file>`) — no
|
||
fixture ever touched the real depot namespace.**
|
||
|
||
**RED/GREEN arm 1 — the `web/assets/models/` path** (transmissive copy planted at the served path
|
||
ONLY; staging copy clean — the R39 gate is structurally blind here):
|
||
|
||
```
|
||
$ python3 $SCRATCH/make_glass.py pipeline/_normalized/procity_fit_box_crate_01.glb \
|
||
web/assets/models/procity_fit_box_crate_01.glb
|
||
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/.validate_r39_ab.py # R39 copy of the gate
|
||
OK — 0 errors, 0 warning(s) # old rc=0 ← the blind spot, live
|
||
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/validate_manifest.py # R40 gate, same tree
|
||
ERR procity_fit_box_crate_01.glb (assets/models copy): material[0] 'Material.001' has
|
||
transmissionFactor 1 — three.js will run a transmission PRE-PASS ... # new rc=1 ← RED
|
||
$ cp $SCRATCH/hold/procity_fit_box_crate_01.glb web/assets/models/ # remove fixture
|
||
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/validate_manifest.py
|
||
OK — 0 errors, 0 warning(s) # rc=0 ← GREEN
|
||
```
|
||
|
||
**RED/GREEN arm 2 — the depot path** (BOTH local copies moved aside = the R39 live-demo shape:
|
||
manifest GLB, no local file anywhere; mock depot on 127.0.0.1:8991 serves the transmissive copy):
|
||
|
||
```
|
||
$ mv pipeline/_normalized/procity_fit_box_crate_01.glb $SCRATCH/hold/norm_...
|
||
$ mv web/assets/models/procity_fit_box_crate_01.glb $SCRATCH/hold/models_...
|
||
$ GOD3_DEPOT=http://127.0.0.1:8991 python3 pipeline/validate_manifest.py
|
||
ERR procity_fit_box_crate_01.glb (depot copy): material[0] 'Material.001' has
|
||
transmissionFactor 1 — ... # rc=1 ← RED
|
||
# mock now serves the CLEAN bytes (local still absent):
|
||
$ GOD3_DEPOT=http://127.0.0.1:8991 python3 pipeline/validate_manifest.py
|
||
OK — 0 errors, 38 warning(s) # rc=0 ← GREEN (mock)
|
||
# and the REAL tailnet depot, local still absent — the production fetch path, scanning the R39-fixed copy:
|
||
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/validate_manifest.py
|
||
OK — 0 errors, 0 warning(s) # rc=0 ← GREEN (real depot)
|
||
```
|
||
|
||
**Cleanup + final:** both local copies restored (sha1 `6c327d86…` both), mock killed, temp R39
|
||
script deleted, fixtures gone. Full validator on the real tree:
|
||
`manifest v1 — facades 49, skins 92 groups, GLBs 39 (+40 non-manifest GLBs transmission-swept),
|
||
audio 34 · OK — 0 errors, 0 warning(s) · rc=0`.
|
||
|
||
### §40.4 — the arcade's posts: re-measured, and the verdict is a PLACEMENT RULE, not an asset
|
||
|
||
**Re-measured on today's tree** (`node pipeline/arcade_measure.mjs` + `buildings.js` read at the
|
||
cited lines — all R39 numbers stand): one edge `kind:'arcade'` **42.00 m × 5 m**, lot faces ±2.50 m
|
||
⇒ **5.00 m lane**; `AWNING_DEPTH 2.2` (`buildings.js:21`) at `AWNING_Y 2.95` (`:22`) ⇒ slabs reach
|
||
±0.30 m ⇒ **0.60 m open slot** at the centreline; posts (`:641-646`) at `d/2 + 2.2 − 0.2` = 2.00 m
|
||
from each lot face = **±0.50 m from the centreline — two rows 1.00 m apart**, 0.12 × 0.12 × 2.95 m,
|
||
**34 posts** at seed 20261990 (2 × 17 shop lots), colliderless (`:649`).
|
||
|
||
**The kit (posts + roof designed together): the roof SPANS (depth 2.5 = half-lane, slabs meet flush
|
||
at the centreline), and a spanned roof has NO posts.** That is a change in `web/js/world/buildings.js`
|
||
+ one line of district plumbing in `web/js/world/chunks.js` — **Lane B's files, so E edited nothing**
|
||
and handed the exact rule (with the mandatory explicit `?classic=1` gate — the synthetic IS the
|
||
classic boot) to B in `LANE_E_NOTES.md` §40 with file:line.
|
||
|
||
**Proof shots without touching B's tree:** the rule was applied **in-flight** by a rewriting
|
||
no-store dev server (session scratchpad `patch_server.py`, exact-match rewrite that fails loudly if
|
||
the source moved) and shot headless (Playwright, `DBG.teleport(21.5, 66.67, −π/2)` — inside the lane
|
||
at seed 20261990, looking down it):
|
||
`docs/shots/laneE/r40_arcade_posts_before.png` (post rows + open slot) ·
|
||
`r40_arcade_posts_after.png` (clear lane, continuous roof) · `r40_arcade_posts_pair.png`.
|
||
**HUD draws 124 → 124 (+0), tris 44k → 44k, chunks 25 → 25.** Nothing generated, published, or
|
||
re-published this round; disk copies of `buildings.js`/`chunks.js` byte-identical throughout.
|
||
|
||
## Round 12 (v3.0-alpha gig layer, `?gigs=1`) · 2026-07-15
|
||
|
||
Three asset streams for one-pub-end-to-end, all free/on-device. C works the pub interior in parallel
|
||
(I touched none of `web/js/interiors/*`).
|
||
|
||
- **Instruments (5 GLBs)** — `flux_local` concept → `trellis_mac` → `normalize.py`, via
|
||
`gen_props.py --batch gig_instruments.json`. **All 5 kept** (electric_guitar, bass_guitar, drum_kit,
|
||
guitar_amp, mic_stand) — every one reconstructed cleanly (thin mic-boom + cymbal hardware survived,
|
||
unlike the round-3 wire rack). Heavy tris (30–96k, TRELLIS shell-soup — amp 96k the outlier),
|
||
accepted for focal/LOD-able stage props; manifest fittings + thumbnails + staged to local-depot.
|
||
D's rigs hold them; primitive fallback if absent.
|
||
- **Posters (4 skins)** — `gen_skins.py --posters`: poster-xerox/screenprint/grunge/retro, portrait
|
||
flyers with a **blank name-band** (no baked text); `skins.poster[].nameZone` [x,y,w,h] for B/C to
|
||
overprint the seeded band name — same discipline as blank-signboard facades. Eyeballed, zero rejects.
|
||
- **Gig audio (3)** — extended `gen_audio.py` procedurally: `music-pubrock-live` (original 90s pub-rock
|
||
riff), `ambience-crowd-walla` (crowd murmur), `sfx-applause` (cheer sting). All `gig:true` in the
|
||
manifest for F. Pack now 26 assets, 9.1 MB of 25. Verified (spectrogram + seams/peaks).
|
||
- **Manifest**: `skins.poster` + audio gig assets + 5 instrument fittings; `build_manifest.py` reads
|
||
`_gig_results.json`; `validate_manifest.py` covers audio. Provenance in `AUDIT.md` (§ Round 12).
|
||
- **Open — depot publish gated**: the 5 instrument GLBs are staged locally (usable via `?localdepot=1`)
|
||
but the tailnet depot push was **blocked by the harness classifier** (outward-facing) → **needs
|
||
John's OK**. Until then `validate --depot` / qa Gate 3 flag provenance-drift on the 5. F owns the
|
||
v3.0-alpha tag; I did not commit yet pending the publish call.
|
||
|
||
## Round 11 (AUDIO round — E leads) · 2026-07-15
|
||
|
||
Stood up the audio pack, free + on-device. **Tool: 100% procedural synthesis** (numpy → ffmpeg) —
|
||
picked over neural gen because it's $0, deterministic/seeded, perfectly loopable, and parody-safe by
|
||
construction (documented in README § audio, per Fable's ask).
|
||
|
||
- **`pipeline/gen_audio.py`** — a small numpy DSP engine (oscillators, ADSR, filters, reverb,
|
||
seamless-loop crossfade, a note sequencer + drum synth). 23 assets, seeded, `--dry-run`/`--only`.
|
||
- **`web/assets/audio/`** — 6 ambience beds (street day/night, rain, 3 interior roomtones), 13 sfx
|
||
(doorbell, door, till, record-riffle, toast, tram bell + rumble, 6 footsteps), 4 instrumental 90s
|
||
music loops (record-shop, milkbar, video-synth, arcade). OGG/Opus + M4A/AAC. **8.1 MB** of 25 MB.
|
||
- **Manifest** `audio:{ambience,sfx,music}` — files + m4a fallback + loop + gain; interior roomtone/
|
||
music carry `types[]` for seed-pick per shop (like facades); footstep is `{surface:[variants]}`.
|
||
`build_manifest.py` builds it, `validate_manifest.py` checks it. **qa green, audio 23, 0 errors.**
|
||
- **Verified** technically (I synthesise, I can't subjectively *listen*): spectrogram sheet
|
||
`docs/shots/laneE/audio_spectrograms.png` + numeric — peaks ≤ -0.4 dBFS (no clip), **all loops
|
||
seamless**, durations correct. **Subjective ear-check is John's/F's** (flagged honestly).
|
||
- Provenance in `AUDIT.md` (§ Round 11). House audio law respected on the E side: silent-happy
|
||
(manifest lists only shipped files), lazy-load metadata, seeded flavour. B wires WebAudio, C gives
|
||
the `room.audio` contract, F integrates + owns the **v2.1 tag/push** — I did **not** push.
|
||
|
||
---
|
||
|
||
*Round 3 and earlier below.*
|
||
*Reviewed against `docs/LANES/LANE_E_ASSETS.md` + `docs/LANES/ROUND3_INSTRUCTIONS.md`.*
|
||
|
||
## Round 9 (Fable) — VRAM audit + depot hygiene (ship-round DD)
|
||
|
||
- **E1 — VRAM/texture audit: DONE + committed.** Measured every texture source (decoded RGBA+mips).
|
||
Resident VRAM: **~240 MB default boot** (skins-in-view ~85 + ped fleet ~90 + GLB ~25 + Lane-B sign
|
||
atlas ~40), **~420 MB worst case** (all flags + all 3 shop types). **Cheap win taken:** half-res'd
|
||
book+toy atlases (quick pull-cards; record kept full for the deep dig; covers verified legible) →
|
||
**stock VRAM 323→181 MB**, no index/UV change, atlases re-published. **Verdict: fine to ship v2.0**
|
||
— no compression/lazy-load needed for the tag. Flagged for v3: stockpack cache-cap (C) + KTX2/Basis.
|
||
- **E2 — depot/manifest hygiene: confirmed green.** `validate --depot` 0 err · pack-QA 3/3 · provenance
|
||
-drift clean (`_published.json` == 27 live) · 15 stock atlases live, **0 orphans**.
|
||
- **On-call:** flagged box-primitive-first tram rec to B; audio still parked on John.
|
||
|
||
## Round 8 (Fable) — pack kitchen (book/toy) + pack-QA gate
|
||
|
||
- **E1 — pack pipeline generalized + book/toy sample packs.** `build_stock_pack.py` now kind-
|
||
parametrized (record 256×256, **book 96×256 tall-thin spines**, toy 256×256), per-kind parody word
|
||
banks, same laws. Book/toy sample packs built + staged (`?localdepot`) + indexes committed to
|
||
`pipeline/packs/` so **Lane C wires book/toy shelves now**. Parody verified deterministic + no
|
||
distinctive-token leakage across all 3 kinds.
|
||
- **E2 — pack-QA gate: DONE.** `validate_pack.py` (wired into `validate_manifest.py` → qa gate 3):
|
||
a bad bake (out-of-range/inverted UV, empty title, bad price_band, dangling atlas, dup id) **fails
|
||
qa** — proven (inverted-UV+empty-title → exit 1). qa `--strict` GREEN 5/5.
|
||
- **Tags pushed** (John-delegated): origin/main = HEAD (1a20501) + v1.0/v1.1/v2.0-alpha/v2.0-beta all
|
||
live on origin — verified.
|
||
- **REAL packs LIVE (John unblocked DB access).** All 3 packs rebuilt from GODVERSE (m1ultra
|
||
Postgres clones, read-only SELECT, peer auth): **record 350** (`discogs_full.inventory` — John's
|
||
real store stock + real Discogs covers), **book 311** + **toy 273** (`dealgod.products` by
|
||
category, real product photos + prices). Metadata parody-transformed (no real trademarks); covers
|
||
are real art (decision #2); book prices seeded (info-only source). 15 atlases published to the
|
||
depot, indexes committed to `pipeline/packs/` + staged, all pass the pack-QA gate. `build_stock_pack.py`
|
||
gained `--from-tsv` (extract on the DB box → build here) + portrait book cell + seeded-price
|
||
fallback. Access path: deployment DB was Docker-internal/SSH-denied → used the m1ultra clones per
|
||
John. No fal spend.
|
||
- **On-call** (B rain-streak/puddle, D door-swing tell): none requested.
|
||
|
||
## Round 7 (Fable) — the two feeders (ped merge + stock pack)
|
||
|
||
- **E1 — ped sub-mesh merge: DONE + committed** (`447188a`, ref `lane-e/round7-peds`). All 19 rigs
|
||
merged to **ONE draw each** (1 prim + 1 atlased 1024 material) via `pipeline/merge_ped.py` (Blender
|
||
join → UV-pack → EMIT-bake all materials into one atlas). Skinning + 65-joint skeleton preserved,
|
||
`walk`/`idle` untouched. **Fleet 92→19 draws; near-cap 116→24 (79% cut).** Verified in the vendored
|
||
GLTFLoader (all load as SkinnedMesh + bind walk; man_suit poses byte-identical to original; 3-mat
|
||
man_athletic atlas correct). Handed to **Lane D (D1)** for full validation + full-density numbers →
|
||
gates F's roster flip. qa `--strict` GREEN 5/5.
|
||
- **E2 — GODVERSE stock pack: pipeline BUILT + PROVEN, real pack blocked on 2 external deps.**
|
||
`pipeline/build_stock_pack.py` = recordgod → curate → **parody-transform (deterministic, zero
|
||
real-trademark leakage: "Dark Side of the Moon"/"Pink Floyd" → "Quiet Milk Bar"/"The Galahs")** →
|
||
atlas WebPs + JSON index. Proven end-to-end with `--sample 24`; **sample pack staged** to
|
||
`web/assets/models/` so Lane C wires `?stock=real` against the real index format now. **Real 200-400
|
||
pack awaits (1) `GODVERSE_DSN` (dealgod Postgres creds — flagged to Fable/John) and (2) C's schema
|
||
confirmation** (proposed in LANE_E_NOTES). One command (`--from-db --count 300`) once they land.
|
||
- **E3 — on-call** (B rain-streak/puddle texture): none requested. No fal spend (balance ~$21.36).
|
||
|
||
## Round 6 (Fable) — non-destructive pipeline + on-call
|
||
|
||
- **E1 — publish.py non-destructive + provenance-drift gate: DONE + committed.**
|
||
- `publish.py` now **merges** into `_published.json` (read existing → union → sorted write),
|
||
never replaces — fixes the R5 `--only` clobber (record dropped 27→1). **Proven:** a real no-op
|
||
re-publish of `record_crate` kept the record at **27 (was 27)**, not 1.
|
||
- Post-publish + a new `--verify` mode **drift-check** the record against the live depot
|
||
(`/api/list` filtered to `procity_*`) and warn on either direction. `--verify` confirms 27 == 27.
|
||
- `validate_manifest.py` gains a **provenance-drift gate**: every manifest `procity_*` GLB must be
|
||
in `_published.json`, else QA fails. **Proven:** simulating the R5 clobber (record→1) makes the
|
||
validator exit 1 with per-file drift errors; restored → green. So the clobber class fails loudly.
|
||
- **Record verified**: `_published.json` (27) is byte-exact vs the live depot — Fable's R5
|
||
regeneration was correct, no fix needed.
|
||
- `qa.sh --strict` GREEN (5 gates incl. F1's new v2-flags harness).
|
||
- **On-call for B/C** (task 3): available for bus_shelter/bin scale-origin tweaks (B) and
|
||
glass_case/magazine_rack in-room issues (C). Broken-prop fal re-gen pre-authorized ≤2 attempts
|
||
(decision #5) — none needed yet. No speculative generation.
|
||
|
||
## Round 5 (Fable) — hero-prop bake + fal fallbacks + depot growth
|
||
|
||
- **E1 — hero-prop bake-to-lowpoly: DONE + committed.** New `pipeline/bake_lowpoly.py` (Blender
|
||
headless): voxel-remesh → decimate → **Cycles base-colour bake** onto fresh UVs (R3's lesson:
|
||
simplify-only floors on shell-soup; the re-bake is the point). All **6 heavy TRELLIS props ≤8k
|
||
tris** (drinks_fridge 77k→8k, milkshake_mixer 73k→8k, listening_booth 54k→8k, counter_till
|
||
32k→8k, novelty_record 26k→8k, arcade_cabinet 25k→8k). **Fleet 288k→48k tris, 18M→5.2M.** Each
|
||
eyeballed (`docs/shots/laneE/props_baked.png` — fridge needed a coarser voxel re-bake, rest clean)
|
||
+ verified loading in the vendored GLTFLoader. Re-published; `validate --depot` 0 err. Originals
|
||
gitignored in `pipeline/.props_orig/`. → Lane C re-measures the record interior (was ~273k).
|
||
- **E3 — fal.ai fallbacks: DONE.** glass_case + bus_shelter + magazine_rack via fal Hunyuan3D v2
|
||
(`pipeline/gen_props_fal.py`, key from MODELBEAST settings). Frosted/opaque concepts (`flux_local`)
|
||
gave the reconstructor a surface — all 3 succeeded where TRELLIS failed. Baked to ≤8k (EMIT albedo
|
||
bake — Hunyuan materials need base-colour→emission or DIFFUSE bakes near-black), verified loading,
|
||
published, `validate --depot` 0 err. **Total fal spend ~$0.64** (glass 1 att, bus 2 att, mag 1
|
||
att × ~$0.16), well under the <$1 estimate; guardrail held. Costs logged in AUDIT R5. Last two
|
||
primitive-only fittings (glass_case, magazine_rack) retired; new street `bus_shelter`. Manifest
|
||
now 17 fittings + 9 furniture (26 GLBs). Contact sheet `docs/shots/laneE/props_fal.png`.
|
||
- **E2 — depot growth: DONE (light, consumer-driven).** The v2 features are procedural (dig uses
|
||
generated sleeves, B's window shader + D's roster need no GLBs), so per Fable "no unconsumed
|
||
entries" I added two clear-consumer props: `street/bin` (council-bin → Lane B) + `fit/crate_stack`
|
||
(milk-crate-stack → Lane C floor piles/dig). Both 3k tris, re-normalized + published, verified
|
||
loading, `validate --depot` 0 err. Also **resolved Lane B's B3 ask** — novelty_record 26.5k→8k
|
||
via the E1 bake. Manifest now **18 fittings + 10 furniture (28 GLBs)**. Further growth on request.
|
||
|
||
## Round 4 (Fable) — ped decimation (gate-3 critical path) + asset fixes
|
||
|
||
- **E1 — ped decimation: DONE + committed** (`518678d`, ref `lane-e/round4-peds`). The 19 ped rigs
|
||
in `web/models/peds/` were **924k tris total** (31k–56k each) — the gate-3 blocker. Decimated to
|
||
**≤3k each** (1564–2805; fleet **924k→46.5k**, ~2.45k avg) via `gltf-transform weld → simplify
|
||
--ratio 0.05 --error 0.12` (meshoptimizer — works on these manifold skinned meshes, unlike R3
|
||
TRELLIS shell-soup). **Skinning + skeleton fully preserved** (65 joints intact, `walk`/`idle`
|
||
untouched); contact sheet `docs/shots/laneE/peds_decimated.png` clean at street distance. Originals
|
||
gitignored in `pipeline/.peds_orig/`; per-ped in `pipeline/_peds_decim.json`. → **Lane D validates
|
||
(D2)** then **Lane F re-measures gate 3** (~24-cap × ≤3k ≈ ≤67k fleet contribution).
|
||
- **E2a — `record_crate` FIXED, verified + published.** Lane C's `undefined 'uri'` crash was a
|
||
**sourceless texture** (packed AO/roughness map failed WebP encode; material's metallicRoughness+
|
||
occlusion slots pointed at it). New tool `pipeline/fix_glb_textures.py` strips those slots (three.js
|
||
loads textures lazily → no crash). **Verified it loads in the real vendored `GLTFLoader`** (meshes=1,
|
||
tris=5000). **Re-published to the depot** (the fixed version is live, tex refs all-sourced).
|
||
- **E2b — counter-with-till (~2 m): LIVE.** New `counter_till` fitting
|
||
(`procity_fit_counter_till_01.glb`, **1.61 × 0.64 m, 1.15 m tall**, 32k tris) — timber counter +
|
||
cabinet doors + period till. Generated on-device (`flux_local` concept → `trellis_mac`, 3 concept
|
||
rolls to land the ~2:1 aspect), normalized to house law, **verified loads in `GLTFLoader`**, and
|
||
**published**. Non-destructive (keeps the balcao long-counter as `counter`); manifest
|
||
`fittingAliases.counter` → `counter_till`; Lane C re-maps kind `counter` → `counter_till` in `glb.js`.
|
||
- **Publish: DONE** (John approved). Both re-exports live; **manifest `validate --depot` GREEN
|
||
(0 err, 23 GLBs live)**. `publish.py` gained an `--only <name>` filter + tailnet-passwordless
|
||
detection (`GOD3_DEPOT=http://100.94.195.115:8788`, decision #4).
|
||
- **Not doing** (per Fable decisions): fal.ai spend (glass-case/bus-shelter parked); hero-prop
|
||
bake-to-lowpoly.
|
||
|
||
## Round 3 (Fable) — hero props on MODELBEAST + local-depot mode
|
||
|
||
- **Hero props, free & on-device** (`pipeline/gen_props.py`): `flux_local` concept image → `trellis_mac`
|
||
image→GLB (1024_cascade) → `normalize.py` → thumbs → manifest. **5 generated + kept**
|
||
(listening_booth, drinks_fridge, arcade_cabinet, milkshake_mixer, novelty_record — contact sheet
|
||
`docs/shots/laneE/props_gen_local.png`), **cash_register reused** from the depot
|
||
(`vintage-cash-register.glb`). 3 didn't make it: **magazine_rack** → in-engine primitive (112k-tri
|
||
wire, the thin-wire gotcha); **glass_case + bus_shelter** → **fal.ai fallback list** (TRELLIS can't
|
||
do transparent glass; sf3d gives a featureless blob) — gated on John, ≤$2.70, both have primitive
|
||
fallbacks. Every kept thumbnail eyeballed; no legible branding (see AUDIT provenance).
|
||
- **Local-depot mode** (`core/loaders.js` `?localdepot=1` + `pipeline/stage_local_depot.py`): resolves
|
||
`depot:<file>` to `web/assets/models/` served by the dev server, so Lanes B/C/F validate GLB loading
|
||
**with no network / before publish**. Verified live in-browser (loads a `depot:` GLB, CDN path intact).
|
||
- **Manifest**: 14 fittings + 8 furniture (5 hero + cash_register added); `validate_manifest.py`
|
||
green (0 err). Provenance recorded in `AUDIT.md` (props are 100% synthetic/local — cleanest in the set).
|
||
- **Known tradeoff**: TRELLIS meshes are dense shell-soup — decimate floors 25–77k tris (heavier than
|
||
the ≤5k library law). Accepted for focal/optional/LOD-able props; texture-bake-to-lowpoly is a follow-up.
|
||
- **Published (self-serve, John's go 2026-07-14)**: the 5 prop GLBs + thumbs pushed to the depot via
|
||
the passwordless tailnet path (decision #4); all 22 GLBs re-verified live. **`qa.sh --strict` GREEN**
|
||
(4/4 gates; manifest validator 0 err / 0 warn with the props live). Provenance was recorded first.
|
||
- **Not committed**: this round is **held for Fable review** (per John) — all Lane E round-3 changes are
|
||
in the working tree, uncommitted. GLB binaries stay git-ignored/staged; the depot has them.
|
||
|
||
## Status at a glance
|
||
|
||
| Step | Deliverable | State |
|
||
|---|---|---|
|
||
| 1 Audit | `pipeline/AUDIT.md` (+ `glb_stat.py`, `_audit_stats.json`) | ✅ done |
|
||
| 2 Normalize | `pipeline/normalize.py` → **16 GLBs** + 16 thumbs, verified | ✅ done (local); ⛔ **publish gated on `GOD3_PW`** |
|
||
| 3 Manifest | `web/assets/manifest.json` + `validate_manifest.py` (+ `build_manifest.py`) | ✅ done, validates green |
|
||
| 4 Facade skins | `gen_skins.py` **`--local`** backend + 23-skin gap pack | ✅ **DONE — 23 skins generated on-device, free, harvested + mapped** |
|
||
| 5 Hero props | round-3 `gen_props.py` (MODELBEAST, free) → **5 local** + cash-register reuse | ✅ **DONE local** (see Round 3 above; 2→fal, 1→primitive) |
|
||
| 6 Local-depot | `loaders.js ?localdepot=1` + `stage_local_depot.py` | ✅ **DONE, verified in-browser** |
|
||
| — | `pipeline/README.md` + contact sheets in `docs/shots/laneE/` | ✅ done |
|
||
|
||
**Step 4 no longer costs anything.** The 23 facades were generated **locally on the M3 Ultra**
|
||
via MODELBEAST `flux_local` (FLUX.2-klein-4B on MPS) — free, ~5.7s each, weights already cached.
|
||
No OpenRouter key, no ~9¢ spend. See "Step 4 (done, local)" below.
|
||
|
||
**Two things still need John (both optional — the game runs asset-free):** (1) the depot password
|
||
`GOD3_PW` to publish the 16 GLBs live; (2) a go/no-go on the paid **hero-prop** step (5 ≈ $2.70).
|
||
|
||
## What landed
|
||
|
||
### Step 1 — Audit
|
||
- `pipeline/glb_stat.py`: dependency-free GLB parser (tris, bbox, texture px, Draco) — runs on
|
||
ultra's py3.9. Full library stats → `pipeline/_audit_stats.json`.
|
||
- `pipeline/AUDIT.md`: the library is a raw dump — almost nothing obeys the GLB law (only
|
||
`record_crate_plastic_01` was native-metres; scale/orientation arbitrary; tris 12→960k; textures
|
||
to 4096; several **branded/trademarked** assets quarantined). Candidate→PROCITY-id table with
|
||
status, coverage-vs-registry table, QUARANTINE list, depot cross-reference.
|
||
- Toolchain reality documented (Blender present on ultra but off PATH; no coreutils/timeout).
|
||
|
||
### Step 2 — Normalize & publish
|
||
- `pipeline/normalize.py`: headless Blender, extends `MESHGOD/finish_glb.py` (solidify/decimate/
|
||
bright/base-origin) + adds **height-normalize, outlier cleanup, texture→WebP≤1024, per-asset
|
||
up/yaw, 256px thumbnail**. Driven by `pipeline/batch.json`.
|
||
- **16 assets** normalized to house law (metres, +Y up, base origin, ≤5k tris, WebP ≤1024, no
|
||
Draco). Every one **eyeballed** — Y-up confirmed by re-importing exports through `render_glb.py`.
|
||
Footprints from Blender-space `_normalize_results.json`. Thumbs in `web/assets/thumbs/`.
|
||
- fittings (9): record_crate, wire_shelf, clothes_rack, bookshelf, cube_shelf_wide, counter,
|
||
coffee_table, work_table, box_crate
|
||
- furniture (7): bench, bench_modern, bench_wood, longbench, park_bench, streetlight, food_cart
|
||
- `pipeline/publish.py`: mirrors the meshgod→3GOD contract exactly (cookie `hmac(GOD3_PW,"3god-ok")`,
|
||
`POST /api/upload` + `/api/thumb`). Reads the secret from env — never contains/logs it. `--dry-run`
|
||
verified all 16 ready. **Not published yet: `GOD3_PW` isn't set here.** Run: `export GOD3_PW=… &&
|
||
python3 pipeline/publish.py` (idempotent, reversible).
|
||
|
||
### Step 3 — Manifest
|
||
- `web/assets/manifest.json` (v1): the contract Lanes B/C/D read. All **69 skins** catalogued
|
||
(25 facade / 10 sky / 10 ground / 8 wallpaper / 6 floor / 7 surface / 3 awning) + 16 fittings/
|
||
furniture with **footprints, heights, thumbs**. `conventions` block documents the GLB law,
|
||
blank-signboard rule, corner-lot policy, fallbacks, and depot load path.
|
||
- Every registry shop type has **≥2 facades** (verified in the validator). Facades tagged
|
||
`signboard: blank|baked` — 5 djsim/market/grimy skins carry baked sign text and must not get a
|
||
name overlay; the other 20 are clean overlay targets.
|
||
- `pipeline/validate_manifest.py`: Lane F gate — parses JSON, checks every skin exists, every GLB
|
||
is local-or-on-depot, thumbs exist, footprints/heights sane, ≥2 facades/type. **Green** (16
|
||
local-only warnings until publish; `--depot` enforces live).
|
||
- `pipeline/build_manifest.py`: reproducible generator (curated type-maps + derived skin/fitting
|
||
lists) so the manifest regenerates when assets change.
|
||
|
||
### Step 4 — Facade skin expansion (DONE, local)
|
||
- `pipeline/gen_skins.py`: ported from `thriftgod/gen_assets.py` — **same STYLE bible + blank-
|
||
signboard template** so new skins match. Resumable `.genraw/`, `--dry-run`/`--only`/`--harvest`.
|
||
- **`--local` backend (the one I used):** MODELBEAST `flux_local` → FLUX.2-klein-4B on the M3
|
||
Ultra's MPS GPU. Apache/**ungated**, weights already cached (15 GB), **free**, ~5.7 s/img, one
|
||
GPU job at a time. Direct subprocess to `~/Documents/MODELBEAST/server/operators/flux_local/run.py`
|
||
(`--outdir`/`--params` JSON) — no server, no cloud, no key. Cloud OpenRouter path kept as fallback.
|
||
- **All 23 generated → harvested → mapped.** 0 failures, 2:11 total. Eyeballed every one
|
||
(contact sheet `docs/shots/laneE/facades_gen_local.png`): every shopfront has a **genuinely
|
||
blank signboard** (Lane B name-overlay target), style indistinguishable from the 25 anchors.
|
||
The pack: toy (2), book (2), video (2), milkbar (3), wide dept anchors (2), stall/market (2),
|
||
warehouse (2), arcade portal (1), residential house fronts (4), corner-lot `-side` walls (3).
|
||
- **Coverage after mapping** (`build_manifest.py` `FACADE_TYPES`, mirrors registry.js + additive):
|
||
record 3 · opshop 3 · toy 5 · book 5 · video 4 · pawn 7 · milkbar 5 · dept 4 · **stall 4**
|
||
— every registry type ≥3. **`stall` went 2→4**, retiring the Lane A single-facade gap.
|
||
- **Corner-lot `-side` skins now exist** (brick/render/weatherboard): tagged `face:"side"` with
|
||
empty `types` in the manifest so no shop-front pool ever picks a windowless wall; Lane B maps
|
||
them to the exposed side face (`conventions.corner_lots` + new `conventions.facade_face`).
|
||
- `pipeline/SKIN_PROMPTS.md`: identical pack kept for the free manual flow (fallback if MODELBEAST
|
||
is ever down).
|
||
|
||
### Step 5 — Bespoke hero props
|
||
- `pipeline/meshgod_batch.json` + `MESHGOD_BATCH.md`: 9 solids (glass case, fridge, till, listening
|
||
booth, arcade cabinet, magazine rack, milkshake mixer, novelty record, bus shelter) in the
|
||
thriftgod_hero pattern. Documents the **MeshGod gotcha** (thin wires/flat discs → primitives),
|
||
reuse-first (cash-register/glass-case likely already on depot), and the primitive fallbacks.
|
||
**Not run** — gated on John (~$2.70). Nothing blocks: all have in-engine fallbacks.
|
||
|
||
## Adversarial review & fixes (self-run, 4-dimension × verify)
|
||
|
||
Ran a 19-agent adversarial review over all deliverables (correctness / contract-fit / doc-accuracy /
|
||
house-law), each finding independently verified. **15 confirmed** (1 blocker, 5 major, 9 minor).
|
||
All fixed and re-verified:
|
||
|
||
- **BLOCKER — off-origin exports.** `normalize.py` joined meshes but never flattened parent node
|
||
hierarchies, so assets under a parent empty shipped off-origin: `park_bench` was **~1 km off-origin,
|
||
floating 30 m**; `food_cart` 15 m off; `clothes_rack` sank 0.19 m. My earlier spot-check missed it
|
||
(the clean assets have no hierarchy). **Fixed:** `parent_clear(KEEP_TRANSFORM)` + drop non-mesh
|
||
nodes before join. Re-verified node-aware: all 16 now `center=[0, h/2, 0]`, **minY=0.0**.
|
||
- **Major — tri budget counted faces, not triangles** (latent for quad FBX/OBJ). Fixed: triangulate
|
||
in `clean_mesh` so the ≤5k gate + reported counts are true triangles.
|
||
- **Major — fitting-id namespace.** Added `fittingAliases` (registry semantic id → physical id) to
|
||
the manifest. (Lane C's `glb.js` already maps its kinds to my physical ids, so it worked — this
|
||
makes it explicit + future-proof.)
|
||
- **Major — facade map vs registry.js.** Aligned my facade→type map to be a consistent superset of
|
||
Lane A's `registry.js`; flagged that registry's `stall` pool has only 1 facade (Lane A gap).
|
||
- **Major — facing -Z not enforced.** Corrected the docs: symmetric props are orientation-agnostic;
|
||
no auto-facing (per-asset `yaw` available). Removed the false "enforced" claim.
|
||
- **Major — licence provenance.** Strengthened the publish-set caveat (record per-asset provenance
|
||
before pushing to the shared CDN) — a reason publish stays gated on John.
|
||
- **Minors:** hardened `glb_stat.py` (node-aware world bounds + WebP size sniff — the exact blind
|
||
spots that hid the blocker); `validate_manifest.py` liveness now uses a Range GET not HEAD;
|
||
Akai-MPC trademark flag; corrected audit counts (59 parsed `.glb`, rejects 8, food_cart tris).
|
||
|
||
## Acceptance check (LANE_E)
|
||
|
||
- ✅ `AUDIT.md` complete.
|
||
- ✅ ≥15 normalized GLBs (**16**) with thumbnails — ⛔ *live on 3GOD pending publish (needs cred)*.
|
||
- ✅ `manifest.json` validates green; fittings/furniture carry footprints for B/C to place untouched.
|
||
- ✅ Every registry shop type has ≥2 facades — now **≥3** (48 facades total). New skins
|
||
**generated** (not just planned) and eyeball-matched to the anchor style
|
||
(`facades_style_anchor.png` = 25 originals; `facades_gen_local.png` = 23 new local skins;
|
||
`fittings_normalized.png` = the 16 GLBs — all in `docs/shots/laneE/`).
|
||
- ✅ One-page `pipeline/README.md` (re-run each step, costs, where creds live).
|
||
|
||
## Notes for the other lanes
|
||
|
||
- **Lane B / C:** read `web/assets/manifest.json`. `fittings`/`furniture` → `depot:<file>` via
|
||
`core/loaders.js` (placeholder box on miss — the game runs asset-free). Footprints are `[w,d]`
|
||
metres on the ground; heights in metres; all base-origin, +Y up, facing −Z.
|
||
- **Lane B:** overlay shop names only on facades with `signboard:"blank"`. Wide `dept-*` anchors
|
||
(when generated) want a wider UV plane. `awning`/`ground`/`sky` skins are yours; `wall`/`interior`
|
||
are Lane C's.
|
||
- **Lane F:** wire `python3 pipeline/validate_manifest.py` into the gate (add `--depot` post-publish).
|
||
|
||
## Open questions for Fable/John
|
||
|
||
1. **Publish now?** I have 16 verified GLBs staged in `pipeline/_normalized/` ready to push to the
|
||
shared depot. It's an outward-facing write to shared infra + needs `GOD3_PW`, so I left it for
|
||
your go rather than assume it.
|
||
2. ~~Spend the ~9¢ on facades?~~ **Resolved — done for free on-device** (MODELBEAST flux_local).
|
||
Only the **~$2.70 hero-prop** MeshGod batch (Step 5) still wants a go/no-go; all props have
|
||
in-engine primitive fallbacks, so it's a polish upgrade, not a blocker. *(Note: image→GLB is
|
||
also local on this box via `sf3d`/`trellis_mac`, but those weights are HF-gated — a setup
|
||
step before that path is free too.)*
|
||
3. A few library assets were near-misses (garage_shelves, radiogram, market_stall — see AUDIT
|
||
rejects). Worth a manual Blender re-origin pass later, or leave to primitives? I left them out.
|
||
|
||
---
|
||
|
||
# Round 41 (§41.1) — THE MOTION LIBRARY *(Lane E, motion half)*
|
||
|
||
**Done, green, and it is the round's critical path — Lane D can start.** 46 clips, 6 grouped GLBs,
|
||
**3 498 124 B (3.34 MB)**, zero-draw, plus a new `web/assets/motion_manifest.json`.
|
||
Full provenance + the licence lines are in `pipeline/AUDIT.md` under "Round 41 §41.1".
|
||
|
||
## 0. Ruling 1 first — the Bandai hazard is closed (paths, exactly)
|
||
|
||
| | |
|
||
|---|---|
|
||
| **was** | `~/Documents/MIRPAMO/library/downloads/bandai_namco_1/` — 3,077 **CC BY-NC 4.0** BVH, 725 MB, unzoned, neutrally named, **nothing in any filename marking them**. One `mirpamo batch` from a shipped build. |
|
||
| **now** | `~/Documents/MIRPAMO/library/downloads/red/bandai_namco_1_NC-research/` |
|
||
| **and** | every clip renamed: **3,077 / 3,077** now end `_NC-research.bvh`; `find … ! -name '*_NC-research.bvh'` returns **0** |
|
||
| **new** | `~/Documents/MIRPAMO/library/downloads/red/README.md` — the zone law, mirrored from ultra's `MOTIONLIB/red/README.md` |
|
||
| **fixed** | `~/Documents/MIRPAMO/library/manifest.json`: `CC-BY-NC-ND 4.0` → **`CC BY-NC 4.0`** (+ `zone:"RED"`, `verified:true`, the renamed path as `name`, and the correction recorded in `notes`) |
|
||
|
||
**NOTHING WAS DELETED.** 552 MB of clip data is still on disk byte-for-byte; the instruction
|
||
preferred non-destructive and ultra holds the canonical zoned copy anyway
|
||
(`~/Documents/MOTIONLIB/red/bandai/`, re-verified live: **3,077 BVH / 552 MB**).
|
||
|
||
Why per-file and not just the directory: `bin/mirpamo` `cmd_batch` names output
|
||
`<rig>@<clipbasename>.glb`, so **the marker now propagates into any retargeted GLB automatically**.
|
||
`downloads/` is gitignored in MIRPAMO, so `red/` inherits that.
|
||
|
||
The ledger fix is real, not cosmetic: the bundled `dataset-1/LICENSE` and `dataset-2/LICENSE` both
|
||
open *"Attribution-NonCommercial 4.0 International"* — **no ND clause**, as ultra's `SOURCES.md`
|
||
already said. Also recorded while I was in there: `downloads/lafan1_ubisoft/` (the other
|
||
non-commercial source) is **scaffold only on this box** — 332 KB of 134-byte git-lfs pointers,
|
||
**0 BVH**, so it cannot leak; the real payload is ultra's `red/lafan1/`.
|
||
|
||
## 1. `mirpamo make smoke` — the tool ground truth, run BEFORE the batch
|
||
|
||
**PASSED**, Blender 5.1.2, exit 0 — procedural mesh → autorig → retarget → verify, `[mirpamo] verify OK`.
|
||
|
||
## 2. What ships
|
||
|
||
`web/models/clips/` (new dir), one fetch per group — ruling 6 is **6 files, not 46**:
|
||
|
||
| group file | category | clips | bytes | motion |
|
||
|---|---|---:|---:|---:|
|
||
| `idles.glb` | idle | 10 | 1 029 496 | 78.3 s |
|
||
| `social.glb` | social | 8 | 895 648 | 73.9 s |
|
||
| `venue.glb` | venue | 6 | 526 036 | 39.5 s |
|
||
| `browse.glb` | browse | 8 | 476 056 | 30.8 s |
|
||
| `sitlean.glb` | sitlean | 8 | 358 112 | 27.5 s |
|
||
| `locomotion.glb` | locomotion | 6 | 212 776 | 12.1 s |
|
||
| **TOTAL ADDED** | | **46** | **3 498 124 B = 3.34 MB** | **262 s** |
|
||
|
||
`web/assets/motion_manifest.json` — **16 KB** (new file; I did not touch `manifest.json`, which is
|
||
the props half's this round). **Total added bytes: 3 514 636 (3.35 MB).**
|
||
|
||
The 8 clips PROCITY already ships cost **4.30 MB**. This is **5.75× the clips for 78% of that**.
|
||
|
||
## 3. ZERO-DRAW — proved with `glb_stat.py`, not asserted
|
||
|
||
```
|
||
browse.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
|
||
idles.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
|
||
locomotion.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
|
||
sitlean.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
|
||
social.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
|
||
venue.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
|
||
```
|
||
|
||
66 nodes = 65 mixamorig joints + `Armature`, i.e. the **byte-shape of the existing `idle.glb`**
|
||
(which is 66 nodes / 1 skin / 0 meshes / 0 tris). Confirmed again after loading in three.js:
|
||
0 meshes, 0 triangles added to the scene by any group file.
|
||
|
||
## 4. THE MANIFEST SCHEMA — `web/assets/motion_manifest.json` (Lane D reads this)
|
||
|
||
**One line: load `groups[<file>]` once, then index `clips[<clipId>]` — the group GLB's
|
||
`gltf.animations[]` are NAMED by `clipId`, so `animations.find(a => a.name === clipId)` is the whole
|
||
lookup.**
|
||
|
||
```jsonc
|
||
{
|
||
"_schema": "…", "_licence": "…", // provenance travels with the data
|
||
"_tracks": "rotation-only", // see §5 — this is what rigs.js keeps anyway
|
||
"_encoding": "rotation outputs are int16 NORMALIZED …; all 65 joints animated incl. fingers",
|
||
"_note": "skeleton-only, 0 tris, 66 nodes — same byte-shape as models/peds/idle.glb",
|
||
"fps": 30,
|
||
"groups": { // ONE ENTRY = ONE FETCH
|
||
"idles.glb": { "category":"idle", "desc":"…", "clips":[<clipId>,…],
|
||
"bytes":1029496, "nodes":66, "durationSum":78.3 }
|
||
},
|
||
"clips": { // 46 entries
|
||
"sit_bench_swing": {
|
||
"group": "sitlean.glb", // which file to fetch
|
||
"category": "sitlean",
|
||
"duration": 2.1333, // seconds, MEASURED from the keyframe times
|
||
"loopable": true, // safe to play on repeat
|
||
"loopSeamDeg": 0.04, // MEASURED: max per-bone angle, first key vs last key
|
||
"source": "Little_Girl_Sitting_On_A_Bench_Swinging_Her_Legs__c9c89165.fbx",
|
||
"mixamo": "Sitting", // the Mixamo catalogue title
|
||
"channels": 65, "rotBones": 65,
|
||
"pair": "<clipId>", // OPTIONAL — a matched two-person / open-close partner
|
||
"loopDemoted": "…" // OPTIONAL — present iff curated-loopable but seam too wide
|
||
}
|
||
},
|
||
"totalBytes": 3498124, "clipCount": 46, "groupCount": 6, "dedupSavedBytes": 1341724
|
||
}
|
||
```
|
||
|
||
Consumer notes for D: the clips ride the **same `_canon` + `_rotOnly` path as `walk`/`idle`** — 65
|
||
raw tracks in, **64 out** (Hips.quaternion dropped). `loadPedFleet` today takes `r.anims[0]`; a
|
||
group file has N, so read them by name. Everything stays behind your gate — I added no loader code
|
||
and touched no file under `web/js/`.
|
||
|
||
## 5. Where the bytes went — three levers, each measured
|
||
|
||
Baseline, straight out of Blender as 46 separate GLBs: **8.20 MB**.
|
||
|
||
| lever | → | why it costs nothing real |
|
||
|---|---:|---|
|
||
| rotation-only tracks | 7.71 MB | `_rotOnly` decodes every translation/scale track and discards it the same frame. Dead bytes in this engine, not fidelity. `--tracks all` restores them |
|
||
| accessor dedup by content | 5.98 MB | lossless — **1 341 724 B** of byte-identical keyframe payloads collapsed |
|
||
| int16-normalized rotations | **3.34 MB** | glTF-legal; three r175 de-normalises in `GLTFLoader._getArrayFromAccessor` (checked in `web/vendor/`). **Worst angular error across all 46 clips: 0.0034°** |
|
||
|
||
**The lever I did NOT pull, for D:** finger channels are **49.7% of the payload** (measured — 3.37 MB
|
||
of the pre-quantisation total). `python3 pipeline/clips_pack.py --drop-fingers` takes the whole set
|
||
to **1.71 MB**. I kept fidelity; say the word and it is one flag and one minute.
|
||
|
||
## 6. Why there is NO retarget — and why that is right
|
||
|
||
The Mixamo bank and PROCITY's ped fleet are **the same mixamorig skeleton**. That is the premise of
|
||
the house one-canonical-clip-bank law (`rigs.js` `_canon`). Running mixamorig→mixamorig through
|
||
MIRPAMO's retargeter would (a) add transfer error for nothing and (b) **emit the ped MESH into the
|
||
clip file**, killing the zero-draw property that makes this round affordable. MIRPAMO's retargeter
|
||
is for foreign skeletons (its CMU bonemap) and for rigging raw meshes — it was still run (`make
|
||
smoke`, green) as the tool check. The Blender-5 slotted-action gotcha is avoided **by construction**:
|
||
nothing goes out through FBX at any stage — FBX in, glTF out.
|
||
|
||
## 7. The clips, by category (46) — `clipId | Mixamo title | duration | loopable | seam° | source FBX`
|
||
|
||
### idles.glb — idle (10) — the clone-army killer
|
||
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|
||
|---|---|---:|:---:|---:|---|
|
||
| `idle_breathing` | Breathing Idle | 9.97s | yes | 0.0 | `Breathing_Idle__c9c6d0d5.fbx` |
|
||
| `idle_standing` | Standing Idle | 6.03s | yes | 0.0 | `Standing_Idle__c9c831cf.fbx` |
|
||
| `idle_look_around` | Looking Around | 6.37s | yes | 0.0 | `Idle_Stand_Looking_Around__c9c6cdf7.fbx` |
|
||
| `idle_weight_shift` | Weight Shift | 9.47s | yes | 0.0 | `Shifting_Weight_From_Side_To_Side__c9c908f7.fbx` |
|
||
| `idle_cocky_lean` | Being Cocky | 2.93s | no | 0.0 | `Cocky_Lean_Back__c9c90e3c.fbx` |
|
||
| `idle_yawn` | Yawn | 8.37s | no | 0.0 | `Big_Yawn_While_Standing__c9c98361.fbx` |
|
||
| `idle_happy_1` | Happy Idle | 2.03s | yes | 0.3 | `Happy_Idle_Variation_1__c9ccf37e.fbx` |
|
||
| `idle_happy_2` | Happy Idle | 2.97s | yes | 0.0 | `Happy_Idle_Variation_2__c9ccf445.fbx` |
|
||
| `idle_old_man` | Old Man Idle | 12.20s | yes | 0.0 | `Old_Man_Standing_Idle__c9c6c004.fbx` |
|
||
| `idle_smoking` | Smoking | 17.93s | yes | 0.4 | `Idle_Smoking__c9c603fd.fbx` |
|
||
### browse.glb — browse (8)
|
||
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|
||
|---|---|---:|:---:|---:|---|
|
||
| `browse_hold_idle` | Holding Idle | 6.03s | yes | 0.1 | `Holding_An_Object_Idle__c9cda11a.fbx` |
|
||
| `browse_hold_walk` | Holding Walk | 1.40s | yes | 6.3 | `Holding_An_Object_While_Walking__c9cda1e7.fbx` |
|
||
| `browse_hold_turn_l` | Holding Turn Left | 1.23s | no | 172.5 | `Holding_An_Object_While_Turning_Left__c9cda2af.fbx` |
|
||
| `browse_hold_turn_r` | Holding Turn Right | 1.33s | no | 147.3 | `Holding_An_Object_While_Turning_Right__c9cda378.fbx` |
|
||
| `browse_open_cabinet` | Opening | 4.83s | no | 0.0 | `Opening_Cabinet_Door__c9c6d49e.fbx` |
|
||
| `browse_close_cabinet` | Closing | 6.20s | no | 0.0 | `Closing_Cabinet_Door__c9c6d55d.fbx` |
|
||
| `browse_carry_box` | Box Idle | 6.33s | yes | 0.0 | `Carrying_A_Box__c9cd8d90.fbx` |
|
||
| `browse_pick_up` | Picking Up Object | 3.47s | no | 92.2 | `Picking_Up_An_Object_With_One_Hand__c9c6c5eb.fbx` |
|
||
### sitlean.glb — sitlean (8)
|
||
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|
||
|---|---|---:|:---:|---:|---|
|
||
| `sit_bench_swing` | Sitting | 2.13s | yes | 0.0 | `Little_Girl_Sitting_On_A_Bench_Swinging_Her_Legs__c9c89165.fbx` |
|
||
| `sit_hands_thighs` | Sitting Idle | 6.43s | yes | 0.1 | `Sitting_In_Chair_Hands_Resting_On_Thighs__c9cb9783.fbx` |
|
||
| `sit_impatient` | Sitting | 7.03s | yes | 0.0 | `Sitting_And_Impatiently_Waiting__c9ccad56.fbx` |
|
||
| `sit_fidget_feet` | Sitting | 1.20s | yes | 0.2 | `Sit_With_Fidgeting_Feet__c9c699a5.fbx` |
|
||
| `lean_wall_postures` | One Shoulder Lean | 2.03s | yes | 0.0 | `Leaning_Against_A_Wall_With_Various_Postures__c9c695f0.fbx` |
|
||
| `lean_wall_leg_up` | Leaning | 3.37s | yes | 0.0 | `Leaning_Square_Against_A_Wall_With_One_Leg_Up__c9c696b0.fbx` |
|
||
| `lean_wall_shoulders` | Leaning | 3.37s | yes | 0.0 | `Leaning_Square_With_Shoulders_Against_A_Wall__c9c69770.fbx` |
|
||
| `lean_wall_walkman` | Leaning On A Wall | 1.97s | yes | 0.0 | `Little_Girl_Leaning_On_A_Wall_Listening_To_Music__c9c8bace.fbx` |
|
||
### social.glb — social (8)
|
||
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|
||
|---|---|---:|:---:|---:|---|
|
||
| `social_conversation` | Talking | 10.30s | yes | 0.3 | `General_Conversation__c9c88301.fbx` |
|
||
| `social_chat_water` | Talking At Watercooler | 44.60s | yes | 0.8 | `Having_A_Chat_At_The_Watercooler__c9c9f1c9.fbx` |
|
||
| `social_greeting` | Standing Greeting | 5.13s | no | 0.0 | `Greeting_While_Standing__c9c7996a.fbx` |
|
||
| `social_shake_1` | Shaking Hands 1 | 4.40s | no | 0.0 | `2_People_Shaking_Hands_Part_1_-_Female__c9c9f343.fbx` |
|
||
| `social_shake_2` | Shaking Hands 2 | 4.40s | no | 0.1 | `2_People_Shaking_Hands_Part_2_-_Male__c9c9f403.fbx` |
|
||
| `social_nod_yes` | Head Nod Yes | 2.63s | no | 0.0 | `Nodding_Head_Yes__c9c90d7b.fbx` |
|
||
| `social_shake_no` | Shaking Head No | 1.83s | no | 0.0 | `Shaking_Head_No_Dismissively__c9c90b3c.fbx` |
|
||
| `social_wave` | Waving | 0.57s | no | 0.0 | `Waving__c9c5ed32.fbx` |
|
||
### locomotion.glb — locomotion (6)
|
||
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|
||
|---|---|---:|:---:|---:|---|
|
||
| `walk_shopping_bag` | Walking With Shopping Bag | 1.30s | yes | 0.0 | `Male_Walking_With_Shopping_Bag__c9c64027.fbx` |
|
||
| `turn_left_90` | Unarmed Turn Left 90 | 1.53s | no | 84.6 | `Turning_90_Degrees_Left__c9ceef5f.fbx` |
|
||
| `turn_right_90` | Unarmed Turn Right 90 | 1.53s | no | 83.2 | `Turning_90_Degrees_Right__c9cef01d.fbx` |
|
||
| `turn_walk_180` | Walking Turn 180 | 1.03s | no | 180.0 | `Turning_180_Degrees_While_Walking__c9c9c73f.fbx` |
|
||
| `turn_in_place` | Turning | 3.70s | no | 36.7 | `Turning_In_Place__c9c70ef0.fbx` |
|
||
| `walk_to_stand` | Stop Walking | 3.03s | no | 55.3 | `Walking_To_Standing_Idle__c9c8d966.fbx` |
|
||
### venue.glb — venue (6)
|
||
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|
||
|---|---|---:|:---:|---:|---|
|
||
| `venue_clap_stand` | Clapping | 1.20s | yes | 0.1 | `Clap_While_Standing__c9c60ef2.fbx` |
|
||
| `venue_clap_seated` | Clapping | 1.20s | yes | 0.1 | `Clap_While_Seated__c9c60fce.fbx` |
|
||
| `venue_cheer` | Cheering | 2.93s | yes | 0.0 | `Male_Cheering_With_Two_Fists_Pump__c9c66dfd.fbx` |
|
||
| `venue_northern_soul` | Northern Soul Dance | 16.10s | yes | 0.4 | `Basic_Northern_Soul_Step__c9cbe56d.fbx` |
|
||
| `venue_bartending` | Bartending | 13.60s | yes | 0.0 | `Pouring_And_Serving_Drinks__c9c78799.fbx` |
|
||
| `venue_headphones` | Listening To Music | 4.50s | yes | 0.0 | `Male_With_Headphones_Listening_To_Music__c9c63329.fbx` |
|
||
|
||
## 8. Curation — three substitutions, because the named seed was wrong for this town
|
||
|
||
Every §41.1 seed was checked against `index.json` by name. Three resolved to something the brief
|
||
did not intend:
|
||
|
||
| §41.1 named | what it actually is in the bank | shipped instead |
|
||
|---|---|---|
|
||
| the whole `45/90/135/180_Degree_{L,R}_Turn` set | **rifle-aiming.** Every single one is Mixamo "Rifle Turn" or "Rifle Crouch Turn". A shopping town does not want citizens sweeping the street down a barrel | the civilian set: `Turning_90_Degrees_Left/Right` (Mixamo **"Unarmed Turn 90"**), `Turning_180_Degrees_While_Walking`, `Turning_In_Place`, `Walking_To_Standing_Idle` |
|
||
| `Examine_Forwards` / `_Backwards` | **static poses, not motion** — 262 KB single-frame entries titled "Female Crouch Pose" / "Female Action Pose" | `Opening_Cabinet_Door` + `Picking_Up_An_Object_With_One_Hand` |
|
||
| `Legs_Crossed_Leaning_On_Object` | static pose, "Male Standing Pose", 262 KB | `Leaning_Square_Against_A_Wall_With_One_Leg_Up` + `…_With_Shoulders_Against_A_Wall` |
|
||
|
||
`2_People_Shaking_Hands_Part_1/2` **does** exist (`…_-_Female` / `…_-_Male`) and **ships as a pair**,
|
||
flagged `"pair"` in the manifest — as do the two holding-turns and the cabinet open/close.
|
||
Two picks are deliberately for *this* game rather than generic: `Idle_Smoking` and
|
||
`Male_Walking_With_Shopping_Bag` (1990s Australian high street), plus `Pouring_And_Serving_Drinks`
|
||
for the pub and `Male_With_Headphones_Listening_To_Music` for the record shop.
|
||
|
||
## 9. Verification — every claim above is a measurement
|
||
|
||
**`pipeline/clips_verify.py` → GREEN, 0 errors, 0 warnings** (new, re-runnable; **F may gate on it**
|
||
— it is the "every referenced clip resolves" check). It asserts: file sizes match the manifest ·
|
||
every clip id resolves to a named animation and nothing extra rides along · every group is zero-draw
|
||
and Draco-free · all six carry the identical 66-node skeleton · every channel targets a real node ·
|
||
every declared duration equals the keyframe times in the bytes · every clip survives `_rotOnly` with
|
||
tracks left · quantisation error is bounded.
|
||
|
||
**In the repo's own vendored three.js r175**, through `GLTFLoader` + `SkeletonUtils.clone` of a real
|
||
`man_casual_01.glb`, all 46 clips: **65 raw tracks → 64 after `_rotOnly`**, durations match to 3
|
||
decimals, every sampled bone world-position finite and moving, 0 meshes / 0 tris added.
|
||
|
||
**The R16 trap was checked for on purpose.** R16 found that keeping `Hips.quaternion` on a seated
|
||
clip lays the body flat (head folded to hip height). Spine tilt (Hips→Head vs +Y) sampled at 24
|
||
points across every clip: **0 frames over 75°, on all 46.** Worst is 36.5° (`social_chat_water` —
|
||
a lean on the watercooler); the seated family sits at 27–28°, a natural seated forward lean.
|
||
**Nothing folds.**
|
||
|
||
**Loopability is measured.** `loopSeamDeg` = max per-bone angle between first and last rotation key.
|
||
28 of 46 loopable. One curated-loopable clip was **auto-demoted by the packer** — `turn_in_place`,
|
||
36.7° seam — and carries `loopDemoted` saying so. The one-shots read exactly as expected
|
||
(`browse_hold_turn_l` 172.5°, `turn_left_90` 180°): they are turns, they are not supposed to close.
|
||
|
||
## 10. Re-running it (all $0, all on-device, ~2 minutes)
|
||
|
||
Added to `pipeline/`: `clips_r41.json` (the checked-in selection — **every asset by name, no
|
||
wildcard, ruling 3**), `clips_export.py` (Blender), `clips_pack.py` (pure stdlib), `clips_verify.py`.
|
||
|
||
```bash
|
||
# 1. fetch the 46 named FBX from ultra's GREEN Mixamo bank (BY NAME — never a wildcard)
|
||
python3 - <<'EOF' > /tmp/fbx_list.txt
|
||
import json;print('\n'.join(c['file'] for g in json.load(open('pipeline/clips_r41.json'))['groups'].values() for c in g['clips']))
|
||
EOF
|
||
ssh johnking@100.91.239.7 "cd ~/Documents/MOTIONLIB/green/mixamo_full && tar czf - $(tr '\n' ' ' </tmp/fbx_list.txt)" | tar xzf - -C /tmp/fbx
|
||
# 2. FBX -> per-clip skeleton-only GLB (local Blender 5.1.2; FBX in, glTF out — never FBX out)
|
||
/Applications/Blender.app/Contents/MacOS/Blender --background --python pipeline/clips_export.py -- \
|
||
--list pipeline/clips_r41.json --fbx /tmp/fbx --out /tmp/clip_glb
|
||
# 3. merge into the <=6 grouped GLBs + write the manifest
|
||
python3 pipeline/clips_pack.py --stage /tmp/clip_glb --out web/models/clips \
|
||
--manifest web/assets/motion_manifest.json # [--drop-fingers] [--no-quantize] [--tracks all]
|
||
# 4. gate
|
||
python3 pipeline/clips_verify.py --stage /tmp/clip_glb # 0 errors = green
|
||
python3 pipeline/glb_stat.py --json web/models/clips # tris must be 0 on all six
|
||
```
|
||
|
||
## 11. Handoff
|
||
|
||
**Lane D (41.3) — you are unblocked.** `web/assets/motion_manifest.json` is the contract; §4 above is
|
||
the schema. `web/models/clips/*.glb` are plain uncompressed GLBs, no Draco, no decoder. Nothing in
|
||
`web/js/**` was touched — no loader code, no gate, no default changed, so `?noassets=1` and
|
||
`?classic=1` are byte-identical to R40 until you wire it. The per-group `bytes` are in the manifest
|
||
so you can pick what boots: `sitlean` + `locomotion` together are **571 KB**; the whole set is
|
||
**3.34 MB in 6 fetches**. If that is too much, `--drop-fingers` halves it in one minute.
|
||
|
||
**Lane F (41.6):** `python3 pipeline/clips_verify.py` is a ready-made clip-manifest-integrity gate —
|
||
exit 0 = green, and it fails loudly on a missing file, a renamed clip, a size drift, a non-zero-draw
|
||
group or a skeleton mismatch. `pipeline/glb_stat.py --json web/models/clips` is the zero-draw proof.
|
||
|
||
**Files I touched, and only these:** `web/models/clips/{idles,browse,sitlean,social,locomotion,venue}.glb`
|
||
(new) · `web/assets/motion_manifest.json` (new) · `pipeline/{clips_r41.json,clips_export.py,clips_pack.py,clips_verify.py}`
|
||
(new) · `pipeline/AUDIT.md` · `pipeline/README.md` · `E-progress.md` · `docs/LANES/LANE_E_NOTES.md`.
|
||
Outside the repo, for ruling 1 only: `~/Documents/MIRPAMO/library/**`. **No git command was run.**
|
||
I did not touch `web/assets/manifest.json` (props half's), `web/js/**`, or `tools/qa.sh`.
|