PROCITY/pipeline/AUDIT.md
m3ultra 12eb571357 Lane E R39 (6/n): THREE GLBs shipped as GLASS, not one — transmissionFactor 1 stripped, published, sha1-verified, and now GATED. Plus the arcade's three meshes.
Lane C measured the bookshelf's transmission halving interior draws (opshop/hall 191->104, book/hall
101->51, dept/hall 116->116 control). The cause, with vendored line numbers: three.module.js:16209
enters renderTransmissionPass(opaqueObjects, ...) whenever currentRenderList.transmissive is
non-empty, and :16433 re-renders the WHOLE OPAQUE LIST into a render target — every opaque draw
issued twice. Trigger is material.transmission > 0 (:6890), set from the glTF at GLTFLoader:1202.
And at 1.0 it is not 'a bit glassy': :494 does totalDiffuse = mix(totalDiffuse, transmitted.rgb,
material.transmission) — the diffuse term is REPLACED by the backdrop.

I SCANNED ALL 103 GLBs. Three carried it:
  procity_fit_bookshelf_01      mtl_10218_Bookshelves_v1     shipped and hurting (C's)
  procity_street_longbench_01   lambert3SG                   NOT WIRED — the 5.86 m arcade bench
                                                             the v9 charter names, on my R38
                                                             recommend list for Lane B
  procity_street_streetlight_01 Streetlight_HighResSG3       NOT WIRED — one per ~18 m of street
The two unwired ones are the worse finding: the street budget is 292/300 with EIGHT draws of margin,
and a pre-pass there costs another ~292, not eight. Armed, not fired.

FIXED SURGICALLY. pipeline/strip_transmission.py rewrites only the JSON chunk and copies BIN through,
printing sha1(BIN) before and after: BIN IDENTICAL on all three, tris/dims/mtl/img/tex unchanged.
KHR_materials_ior deliberately left (ior does not trigger the pre-pass).

PUBLISHED + VERIFIED under John's R22 standing authorization via the passwordless tailnet ingress:
6/6 HTTP 200 off the PUBLIC depot, byte counts exact, sha1 identical, depot copies re-scanned clean.
_published.json 56 -> 59.

GATED, which matters more than the fix: validate_manifest.py now HARD FAILS on transmissionFactor>0
with the three.js line numbers and the fix command in the message; glb_stat.py grew a transmission
column and a loud footer. Nobody looked for three epochs because a bookshelf is obviously not glass.

ALSO: the arcade's three meshes, published and catalogued the same round they were generated —
aframe_board 366 tris (<=16 inst), blade_sign 299 (17 inst), keycutter_sign 498 (1 inst). manifest
furniture 13->16, GLBs 36->39, validate 0 errors 0 warnings. blade_sign ships FLAGGED: its back face
carries TRELLIS-hallucinated art where the concept had no data, which is one more reason the
+0-draw perpendicular-quad answer is the recommendation and this GLB is the fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 21:11:34 +10:00

1103 lines
81 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PROCITY Asset Audit — Lane E, Step 1
*Generated 2026-07-14 from a live inspection of `johnking@100.91.239.7:~/Documents/3D=models/`
(the sorted house library) + the 3GOD depot `GET /api/list`. Tri counts / bounding boxes from
`pipeline/glb_stat.py` (dependency-free GLB parser); orientation & final scale verified by
rendering every candidate through Blender and looking at the thumbnails (never "should look right").*
## TL;DR
- The library is a **raw dump**: almost nothing obeys the house GLB law. Scale is arbitrary
(only `record_crate_plastic_01` was native-metres), up-axis varies, tri counts span **12 → 960k**,
textures reach **4096px**, and several assets carry **branded/trademarked designs** (quarantined).
- `pipeline/normalize.py` fixes scale/orientation/tris/textures uniformly. **16 assets** normalized
clean, verified upright & metre-scaled, WebP ≤1024, ≤5k tris, +Y up, base origin — ready to publish.
- Gaps with **no usable source** (→ `MESHGOD_BATCH.md` or in-engine primitives): glass display
case, fridge, till/register, spinner rack, magazine rack, market stall, escalator, VHS shelving.
- Depot already holds a few reusable house props (`street-bench`, `council-bin`, `milk-crate-stack`)
and 138 assets total (mostly TRELLIS `*-views-3d-*` / `image-3d-*` MeshGod outputs).
- **Skin caveat:** 5 of the 25 facades carry **baked-in sign text** (`djsim-milkbar` "MILK BAR",
`djsim-video`, `djsim-pub`, `market`, `grimy`) — they break the blank-signboard convention, so
the manifest tags them `signboard:"baked"` and Lane B must not overlay a name on them. The other
20 are genuinely blank.
## Toolchain reality (differs from the lane brief)
| thing | brief said | actual |
|---|---|---|
| Blender on ultra | "runs on ultra against Blender" | present but **not on PATH**`/Applications/Blender.app/Contents/MacOS/Blender` (v5.0.1). Renders 4 angles in ~0.9s. |
| coreutils / timeout | none | confirmed — used ssh `ConnectTimeout`, no `timeout`/`setsid` |
| python on ultra | 3.9 | 3.9.6 — `glb_stat.py`/`normalize.py` written to that floor |
| OpenRouter key | `thriftgod/.env` | absent; house convention `~/Documents/dealgod/lore/keys.env` (auto-read by gen scripts) |
| depot upload cred | — | `GOD3_PW` env, **not set here** → publish is gated on John (see `publish.py`) |
## Normalized set (16 — ready; `manifest.json` → `fittings` / `furniture`)
house law enforced on every one: metres · +Y up · origin at base (minY=0, centred X/Z — verified
node-aware) · ≤5k tris · WebP ≤1024 · no Draco. *Facing is not auto-corrected* — symmetric props are
orientation-agnostic; the few directional ones (counter, food_cart) keep their native front and
Lane B/C rotate when placing (per-asset `yaw` is available in `batch.json`).
| PROCITY id | source GLB | src tris → out | footprint (m) | height (m) | notes |
|---|---|---|---|---|---|
| **fit** record_crate | shop-fittings/record_crate_plastic_01 | 18320→5000 | 0.46 × 0.63 | 0.41 | ★ native-metres, textured red milk crate; serves record bin too |
| **fit** wire_shelf | shop-fittings/shelf_metal_wire_01 | 330→330 | 1.17 × 0.44 | 1.80 | ★ clean upright wire shelving |
| **fit** clothes_rack | shop-fittings/clothes_rack_01 | 3152→3152 | 1.34 × 0.43 | 1.50 | ★ rolling clothes rack (opshop) |
| **fit** bookshelf | shop-fittings/bookshelves | 45128→5000 | 0.20 × 1.02 | 2.00 | tall bookshelf w/ books (book/video/opshop) |
| **fit** cube_shelf_wide | shop-fittings/shelf_cube_bookshelf_01 | 17734→4999 | 0.75 × 1.24 | 1.10 | stocked cube shelving (toy/book) |
| **fit** counter | shop-fittings/balcao | 276→276 | 1.06 × 4.02 | 1.10 | long bar counter — Lane C may want to scale to lot |
| **fit** coffee_table | shop-fittings/coffeetable | 495→495 | 0.67 × 1.49 | 0.45 | low display/side table |
| **fit** work_table | shop-fittings/computertable | 1556→1556 | 0.44 × 1.04 | 0.75 | work/trestle table w/ lower shelf |
| **fit** box_crate | shop-fittings/warehouse_box | 78→78 | 1.35 × 1.32 | 0.60 | open bulk bin / bric-a-brac box |
| **street** bench | street-furniture/bench | 1942→1942 | 0.44 × 1.21 | 0.85 | slatted seat |
| **street** bench_modern | street-furniture/eb_hipster_bench | 640→640 | 1.68 × 0.47 | 0.45 | low modern bench |
| **street** bench_wood | street-furniture/woodbench | 11728→3999 | 1.34 × 0.59 | 0.85 | timber park bench |
| **street** longbench | street-furniture/longbench | 1172→1172 | 5.86 × 1.08 | 0.85 | long feature bench (arcade/market) |
| **street** park_bench | street-furniture/park_bench_01 | 9884→5000 | 1.92 × 1.62 | 0.85 | classic slatted park bench |
| **street** streetlight | street-furniture/streetlight | 6592→4999 | 1.64 × 1.82 | 5.00 | ornate lamppost |
| **street** food_cart | street-furniture/food_cart_bakso_01 | 30316→5000 | 3.00 × 2.07 | 2.00 | textured food cart (~3MB — heavy, 28 tex; optimize later) |
## Rejected during normalize (needs-manual-fix — NOT in manifest)
| source | issue (verified by render) | disposition |
|---|---|---|
| shop-fittings/cube_bookcase (`cube_shelf`) | node-scale distortion → 6.3 × 8.4 m footprint | dropped; `cube_shelf_wide` covers the need |
| shop-fittings/garage_shelves_metal_v2 | imports lying flat (green pallet); baked node transform, up-fix didn't help | needs manual re-origin in Blender; `wire_shelf` covers shelving |
| shop-fittings/table, saloon_table, table_02, desk_01, kitchen_table | degenerate/flat or node-distorted (`table` → 55×55 m; `desk` → 17×17 m) | rejected; `work_table`/`coffee_table` cover tables |
| shop-fittings/radiogram_furniture_v2_l1 | Blender "Please select a file" on import (bad GLB) | needs re-export |
| street-furniture/market_stall_01_CHECK | import error (`_CHECK` = sorter-flagged) | → MeshGod/primitive (see `MESHGOD_BATCH.md`) |
## QUARANTINE — licence unclear / branded (🔴 do NOT publish or ship)
The house rule is 🟢 web-ok only; anything branded or of unknown provenance is quarantined until
cleared. These are **excluded from the manifest**:
| asset | reason |
|---|---|
| `furniture/armchair-vitra-eames`, `ines-chair-calligaris` | protected designer marques (Vitra/Eames®, Calligaris®) |
| `shop-fittings/ikea-cupboard` | IKEA® product design (+259k tris) |
| `props-scenes/mk2_1210_turntable_-_old` | Technics SL-1210® industrial design (trademark) |
| `props-scenes/tuborg_fest_scene` | Tuborg® brand scene |
| `furniture/uploads_files_*`, `props-scenes/uploads_files_*` (Couch William, Harland Office Chair, nv toufa, Backpack) | CGTrader/free3d bulk dumps, licence not recorded |
| `furniture/sofa-c`, `hug-armchair`, `dragonchest`, `kitty-bed-01`, `bean_bag_chair` | provenance unrecorded — verify before use |
> **Blanket caveat (applies to the 16-asset publish set):** the *generic* fittings we normalized
> (benches, shelves, counter, crate…) carry **no explicit licence/provenance record** — I cross-
> checked all 16 source filenames and none is a recognizable trademark, and they were pre-sorted
> into the house library by John for exactly this use, so they're treated as usable per the lane
> brief. But "unclear ⇒ quarantine" is the house rule: **before `publish.py` pushes to the shared
> CDN, record a one-line source/licence per asset**, and for any CC-BY source carry attribution.
> If provenance genuinely can't be established for one, pull it from the publish set. This is a
> reason the publish step is deliberately gated on John, not automated.
## Provenance — round-3 hero props (for the depot publish sign-off)
Per the round-3 instruction ("record the per-asset provenance list in AUDIT.md first, then hand
John the two-liner"). The hero props have the **cleanest provenance in the whole set** — they are
100% generated on-device, from no third-party source:
| asset (`procity_fit/street_<id>_01.glb`) | origin | licence zone |
|---|---|---|
| **5 generated** — listening_booth, drinks_fridge, arcade_cabinet, milkshake_mixer (fit) · novelty_record (street) | **fully synthetic, local**: concept image = MODELBEAST `flux_local` (FLUX.2-klein-4B, Apache-2.0 model) → mesh = `trellis_mac` (TRELLIS.2, an original reconstruction of that concept image). No scanned/third-party geometry, no copyrighted source asset. | 🟢 original work (CC0-equivalent) |
| cash_register (`vintage-cash-register.glb`) | **reuse** of John's own `thriftgod_hero` asset, already live on 3GOD (round-3 reuse check) — not regenerated. | 🟢 same as existing depot set |
| magazine_rack | **not shipped** — TRELLIS wire rack floored ~112k tris (thin-wire gotcha) → in-engine primitive. | n/a |
| glass_case, bus_shelter | **not shipped** — TRELLIS failed on the transparent glass → fal.ai fallback list (John's go — Hunyuan v2 ~1417¢/gen: ~$0.280.34 for the pair, ~$1 worst case w/ retries) or primitive. | n/a |
- **Prompts were generic period objects with explicit `no text, no brands, no people`.** Every kept
prop was eyeballed for accidental baked branding (contact sheet `docs/shots/laneE/props_gen_local.png`).
The only textual marks are the **drinks-fridge's weathered faux-stickers** — verified at full concept
resolution to be **garbled, illegible pseudo-lettering** (no real trademark; e.g. a faux barcode +
nonsense glyphs), which reads as period flavour and blurs out at mesh-texture resolution. 🟢 cleared.
- Tri/size note: TRELLIS meshes are dense multi-shell scans; `normalize.py`'s COLLAPSE decimate floors
well above the ≤5k library budget (**2577k tris, ~0.33.5 MB/prop**; neither COLLAPSE nor meshopt
can reduce shell-soup without a voxel-remesh that destroys the baked UVs). Accepted for **focal,
optional, LOD-able** props (texture-intact) — flagged for a later texture-bake-to-lowpoly / gltfpack pass.
- These GLBs stage locally (`web/assets/models/`, git-ignored, served by `?localdepot=1`) and await
John's depot publish; they are **not** committed binaries (same policy as the library `_normalized` set).
## Coverage vs the registry fittings needs
| shop type | fittings need | covered by | gap → |
|---|---|---|---|
| record | bins, crates, counter, listening corner | record_crate, counter, wire_shelf | **listening booth** → MeshGod |
| opshop | clothes racks, bric-a-brac shelving, book wall | clothes_rack, wire_shelf, bookshelf, box_crate | — |
| toy | cube shelves, display tables, **glass case** | cube_shelf_wide, coffee_table | **glass case** → MeshGod |
| book | bookshelves, **spinner racks**, armchair | bookshelf, cube_shelf_wide | spinner rack, armchair → MeshGod/primitive |
| video | aisle shelves, **returns slot**, poster wall | bookshelf, wire_shelf | returns slot, poster wall → primitive |
| pawn | **glass cabinets**, wall hooks, barred window | counter | glass cabinet → MeshGod; pegboard = `tex-pegboard` skin |
| milkbar | counter, **fridge**, magazine rack | counter | **fridge**, magazine rack → MeshGod/primitive |
| dept | mixed sections, **escalator prop** | (all above) | escalator → primitive |
| stall | trestle tables, crates | work_table, box_crate, record_crate | **market stall** → MeshGod/primitive |
| street | bench/streetlight/bin/tree | bench×4, longbench, park_bench, streetlight, food_cart | bin (`council-bin` on depot), tree → billboard (Lane B) |
Gaps with no clean source are shortlisted in `MESHGOD_BATCH.md` (budget-gated) — everything has an
in-engine primitive fallback, so none of these block the game.
## Depot cross-reference (`GET /api/list`, 138 assets)
- **90 named + 48 TRELLIS-hash.** Reusable house props already live (verify visually + licence
before wiring): `street-bench.glb`, `council-bin.glb`, `milk-crate-stack.glb`, `cashbot.glb`.
⚠️ `akai-mpc-live-iii-*` is a **trademarked product design** — same quarantine class as Technics/
IKEA; do not treat as clean web-ok reuse without a licence check.
- **No `procity_*` assets yet** — this lane is the first to publish that namespace.
- Our 16 normalized GLBs are staged in `pipeline/_normalized/` and upload via `pipeline/publish.py`
once `GOD3_PW` is provided (idempotent, reversible).
## Full library inventory (dispositions)
`shop-fittings/` (33): normalized 9 · rejected 8 (7 node-distorted/degenerate + radiogram bad-GLB) ·
quarantine 1 (ikea) · unused/dup 15 (heavier or duplicate tables/shelves — `shelf_storage_rack_01`
alone is 960k tris).
`street-furniture/` (9): normalized 7 · error 1 (market_stall) · `building_shell_01` reserved for
Lane B (38k-tri building shell — not a fitting).
`furniture/` (17): mostly domestic (beds/sofas/chairs) — out of scope for shop fittings; 6 quarantined.
`props-scenes/` (10): `dj_roomv01` + turntable = record-shop dressing (turntable quarantined: Technics);
rest branded/scene dumps.
Raw stats for every **parsable `.glb`** (59 of the ~69 library entries; the rest are `.fbx`/`.obj`
or subdir sources not counted): `pipeline/_audit_stats.json`. `glb_stat.py` reports **world-space**
dims (node-transform-aware) and WebP/PNG/JPEG texture sizes. Normalizer output:
`pipeline/_normalize_results.json`.
## Round 4 — ped-rig decimation (gate-3 blocker)
The 19 street ped rigs in `web/models/peds/` were **~31k56k tris each (924k total)** — the cause of
gate 3 failing (~279k tris at the busiest intersection vs ≤200k). Decimated to **≤3k tris each**
(integrator decision #1) via `gltf-transform` `weld``simplify` (meshoptimizer):
```
gltf-transform weld in.glb _w.glb
gltf-transform simplify _w.glb out.glb --ratio 0.05 --error 0.12
```
meshoptimizer works cleanly here (proper manifold skinned character meshes, NOT the TRELLIS
shell-soup of the R3 hero props). Result: **924,153 → 46,551 tris** (48.6k → 2.45k avg; range
15642805, all ≤3k), **36M → 5M** on disk. **Skinning fully preserved**`JOINTS_0`/`WEIGHTS_0`
intact, skeleton unchanged (nodes + 65 joints identical to source), so the shared `walk.glb`/
`idle.glb` clips retarget unchanged (both left untouched). Contact sheet:
`docs/shots/laneE/peds_decimated.png` — silhouettes/textures read clean, no melted faces/hands
(fine for street distance). Originals backed up (gitignored) at `pipeline/.peds_orig/`; per-ped
before/after in `pipeline/_peds_decim.json`. Lane D validates rig binding + anims (task D2); Lane F
re-measures gate 3 (~24-rig near cap × ≤3k ≈ ≤67k fleet contribution).
## Round 4 — E2 asset re-exports (published)
- **`procity_fit_record_crate_01.glb` re-export (fixed).** The R3 normalize left a **sourceless
texture** (packed AO/roughness map didn't survive the WebP export; the material's
`metallicRoughnessTexture` + `occlusionTexture` pointed at it), which crashed three.js GLTFLoader
with `undefined 'uri'`. `pipeline/fix_glb_textures.py` strips material slots that reference a
sourceless texture (base-colour + normal kept; the orphan is never loaded). Verified it loads in
the vendored `GLTFLoader` (5000 tris, textures ok); re-published to the depot.
- **`procity_fit_counter_till_01.glb` (new).** ~2 m timber shop counter with a period beige till —
`flux_local` concept (3 rolls for the ~2:1 aspect) → `trellis_mac` mesh → `normalize.py`. 1.61 ×
0.64 m, 1.15 m tall, 32k tris (TRELLIS shell-soup floor, within decision-#2's 2577k for focal
props). **Provenance: 100% synthetic/on-device** (FLUX.2-klein-4B + TRELLIS.2 on the M3 Ultra), no
third-party source — cleanest licence zone. Replaces the R3 4 m till-less counter for the interior
counter slot (Lane C maps `counter``counter_till`); the balcao 4 m `counter` stays for long-bar use.
## Round 5 — E1: hero-prop bake-to-lowpoly (un-parked)
The 6 heavy TRELLIS hero props were **25k77k tris** (shell-soup — simplify-only floors there,
R3's lesson). Rebuilt topology + re-baked base colour via `pipeline/bake_lowpoly.py` (Blender
headless): **voxel-remesh → triangulate → decimate ≤8k → smart-UV → Cycles DIFFUSE(COLOR) bake
HP→LP → WebP export**. All **≤8k tris**, base colour baked onto fresh UVs, self-illuminated a
touch (matches normalize). Originals backed up (gitignored) at `pipeline/.props_orig/`; batch spec
`pipeline/batch_bake.json`; results `pipeline/_bake_results.json`.
| prop | tris before → after | voxel | size on disk | note |
|---|---|---|---|---|
| drinks_fridge | 77047 → 8000 | 0.03 | 5.3M → 0.9M | hollow glass-door box — needed a coarser voxel (0.012 grabbed the internal shelves into wireframe) |
| milkshake_mixer | 73492 → 7991 | 0.005 | 4.7M → 0.9M | small part, fine voxel |
| listening_booth | 53502 → 7986 | 0.012 | 3.4M → 0.9M | panel + stool |
| counter_till | 32226 → 7999 | 0.012 | 1.9M → 0.9M | timber counter + till |
| novelty_record | 26486 → 8000 | 0.014 | 1.8M → 0.9M | thick disc + base |
| arcade_cabinet | 25142 → 8000 | 0.012 | 1.5M → 0.8M | boxy cabinet |
Fleet: **288k → 48k tris**, **18M → 5.2M**. Every one **eyeballed** (contact sheet
`docs/shots/laneE/props_baked.png` — no melted labels/faces; the fridge re-bake was the only redo)
and **verified loading in the vendored `GLTFLoader`** (tris ≤8k, baked texture present).
Re-published to the depot (`validate --depot` 0 errors). **Provenance unchanged: 100% synthetic/
on-device** (FLUX.2-klein-4B + TRELLIS.2 + Blender bake, no third-party source). → Lane C
re-measures the record interior (was ~273k tris).
## Round 5 — E3: fal.ai fallback props (glass-case + bus-shelter + magazine-rack)
The two R3 TRELLIS glass rejects + the optional magazine-rack, generated on **fal.ai Hunyuan3D v2**
(John-authorized, decision #4). Pipeline: `flux_local` concept (frosted/opaque surfaces so the
reconstructor has something to grab — clear glass is what defeated TRELLIS) → `pipeline/gen_props_fal.py`
(fal `fal-ai/hunyuan3d/v2`, key from MODELBEAST settings, never hardcoded) → `normalize.py` (scale/
origin) → `bake_lowpoly.py` (voxel-remesh → ≤8k → **EMIT albedo bake**; Hunyuan materials need the
base-colour→emission route, DIFFUSE(COLOR) baked them near-black).
**Paid gen log (fal Hunyuan3D v2, ~$0.16/gen):**
| prop | attempts | cost | outcome |
|---|---|---|---|
| glass-case | 1 (frosted-glass concept) | ~$0.16 | ✅ solid case, timber base, items visible — TRELLIS couldn't; 40k→8k baked |
| bus-shelter | 2 (att.1 dropped the thin perspex panels → att.2 with an opaque back wall) | ~$0.32 | ✅ solid shelter, back wall + roof + bench; 40k→8k |
| magazine-rack | 1 (optional; solid-timber, not wire) | ~$0.16 | ✅ tiered timber rack w/ magazines — beat the R3 wire failure; 40k→8k |
**Total fal spend this round: ~$0.64** of the $22 balance (well under the <$1 estimate). Guardrail
held — nothing else on fal. All 3 ≤8k tris, base-colour baked, **verified loading in the vendored
GLTFLoader**, eyeballed (`docs/shots/laneE/props_fal.png` — no melted glass, no legible branding),
published, `validate --depot` 0 err. **Provenance: FLUX.2-klein-4B concept + fal Hunyuan3D v2 mesh +
Blender bake — synthetic, no third-party source** (Hunyuan output is model-generated geometry).
Raw fal GLBs + concepts kept in gitignored `pipeline/.genprops/`. These retire the last two
primitive-only fittings (glass_case, magazine_rack) + add a street bus_shelter.
## Round 5 — E2: depot growth (consumer-driven)
The round-5 v2 features are **procedural** (dig.js riffles generated sleeve art; B's window shader is
100% procedural; D's roster is spawn logic) — so no forced GLB needs. Per Fable "don't add manifest
entries no lane consumes", E2 is deliberately light: two already-synthetic depot props with clear
consumers, re-normalized to procity names + thumbs (both already ≤5k tris, metre-scale, base-origin):
| new id | source | footprint / h | consumer |
|---|---|---|---|
| `street/bin` (`procity_street_bin_01.glb`) | `council-bin.glb` (thriftgod_hero, on depot) | 0.57×0.68 m, 1.0 m | Lane B street dressing (towns need bins) |
| `fit/crate_stack` (`procity_fit_crate_stack_01.glb`) | `milk-crate-stack.glb` (thriftgod_hero) | 0.71×0.72 m, 0.9 m | Lane C record-store floor piles / dig context |
Both 3k tris, verified loading in the vendored GLTFLoader, published, `validate --depot` 0 err.
Provenance: MeshGod-generated (thriftgod_hero) — synthetic, web-ok. **Note:** also resolved Lane B's
B3 ask — `novelty_record` (was 26.5k, spiking the busy view) is now 8k via the E1 bake. No speculative
props added; further depot growth is available on request once C's dig / D's roster name concrete needs.
## Round 7 — E1: ped sub-mesh merge to one draw (gates the roster flip)
The 19 ped rigs carried **19 primitives / 13 materials each = 92 draws** for the fleet (≈116 at
the 24-rig near cap) — the optimisation D's memo gates full-density roster on. `pipeline/merge_ped.py`
(headless Blender) merges each ped to **ONE primitive + one atlased material**: join meshes → pack
every material's UV islands into a non-overlapping atlas UV → **EMIT-bake each source material's
base-colour into one 1024 atlas** (route base-colour→emission via the ORIGINAL uv so the atlas UV
doesn't hijack the lookup) → single material. **Skinning + skeleton fully preserved** (65 joints
intact on all 19, `JOINTS_0`/`WEIGHTS_0` kept; `walk`/`idle` untouched).
Result: **fleet 92 → 19 draws (1/ped)**; at the 24-rig near cap **116 → 24 draws** (79% cut). Size
5M → 4M. Verified in the vendored `GLTFLoader`: all 19 load with `isSkinnedMesh` + bind the shared
`walk.glb` clip; `man_suit` (2-mat) poses to **byte-identical dimensions vs the original**
(109.038 × 62.5) proving the skin deforms unchanged; `man_athletic` (3-mat, most complex atlas)
eyeballed correct (skin/top/pants regions all right). Pre-merge decimated peds backed up (gitignored)
in `pipeline/.peds_r6/`; per-ped in `pipeline/_peds_merge.json`. → **Lane D validates (D1)**
full silhouette/material/impostor/identity pass + full-density numbers — then **F flips the roster**.
## Round 7 — E2: GODVERSE stock pack pipeline (real sleeves for the dig)
`pipeline/build_stock_pack.py` builds C's `?stock=real` content pack **offline** (nothing queries the
dataset at runtime): read recordgod (`dealgod` Postgres `discogs_release`, 185k vinyl w/ cover URLs) →
curate N sleeves (deterministic md5 sample, varied) → **parody-transform title/artist** → fetch covers →
bake 8×8 atlas WebPs (256px cells) + a JSON index (`{id,title,artist,price,price_band,atlas,uv}`) →
stage for `?localdepot` / publish to depot.
**Parody law (metadata):** real (title, artist) → deterministic seed → recombined from period-AU word
banks; the output **shares no source token** (`Dark Side of the Moon`/`Pink Floyd` → `Quiet Milk Bar`/
`The Galahs`) so there is **zero real-trademark leakage**, yet it reads as plausible AU record stock,
and the same source row yields the same parody every build. Verified deterministic + token-disjoint.
**Pipeline PROVEN** end-to-end with `--sample 24` (synthetic solid-colour sleeves): 24 → one 2048 atlas
+ index, UVs correct, price bands (bargain/standard/collector/grail) working; sample pack staged to
`web/assets/models/` so Lane C can wire the consumer against the **real index format now**.
**The real 200-400 pack is blocked on two external deps (flagged to Fable):**
1. **`GODVERSE_DSN`** — the dealgod Postgres DSN (creds). Postgres is up on `localhost:5432`; the
builder reads the DSN from env (I don't hunt for creds). `--from-db --count 300` runs once it's set.
2. **Lane C's pack schema** (their task 1, not yet posted) — I built to the proposed fields above;
`INDEX_FIELDS` adjusts to C's LANE_C_NOTES contract when it lands.
Provenance of the real pack: source = Discogs metadata + cover images (recordgod), **metadata
parody-transformed** (no real titles/artists ship); covers are real release art (read-only, non-economy
per decision #2). Small book/toy packs are a follow-up once the record pack lands.
## Round 8 — E1/E2: book + toy stock packs + pack-QA gate
**E1 — pipeline generalized** (`build_stock_pack.py` now kind-parametrized: `record` 256×256 sleeves,
`book` **96×256 tall-thin spines**, `toy` 256×256 boxes). Per-kind parody word banks; same laws as
the record pack (build-time extract, deterministic parody-transform of metadata, atlas WebPs + JSON
index to C's schema — `stock_<kind>_index.json`, C reads `uv` not `cell`). Book/toy **sample packs
built + staged** (`web/assets/models/`, `?localdepot`) so Lane C wires book/toy shelves now; indexes
committed to `pipeline/packs/` as the contract. Parody verified deterministic + no distinctive-token
leakage across all 3 kinds ("Nevermind"/"Nirvana" → "Endless Blue"/"Mullet Milk Crates";
"The Hobbit" → "Under the Homestead"/"C. Underwood").
**E2 — pack-QA gate** (`validate_pack.py`, wired into `validate_manifest.py` → qa gate 3): every
`pipeline/packs/stock_*_index.json` entry must have a valid kind, in-range non-inverted UVs, a known
price band, non-empty title/artist, unique id, and an atlas listed in `atlases[]` — a bad bake fails
qa loudly (proven: inverted-UV + empty-title → exit 1). qa `--strict` GREEN 5/5.
**Blocker (flagged to Fable/John — real data):** the **real** ~150250-item book/toy packs (and the
real record pack) need **`GODVERSE_DSN`** (dealgod Postgres; up on :5432, read from env, no cred
hunt). One command each once set: `build_stock_pack.py --kind book --from-db --count 200`. The
current staged packs are synthetic placeholders (real index format); provenance of the real packs =
GODVERSE metadata (OpenLibrary/toygod) **parody-transformed** (no real titles/authors/brands ship) +
real cover art (read-only, non-economy). No fal spend this round.
## Round 8 (cont.) — REAL stock packs live (GODVERSE, John-authorized DB access)
The synthetic samples are replaced by **real** packs, built from GODVERSE via peer-auth on m1ultra
(`johnking@100.91.239.7`, native Postgres, `psql` at `/opt/homebrew/Cellar/libpq/18.4/bin`). Access
resolved after the deployment DB proved Docker-internal (port closed, SSH denied) — John pointed to
the m1ultra clones. All extraction was **read-only SELECT**; no creds guessed, no writes.
| pack | items | source (m1ultra) | covers | prices |
|---|---|---|---|---|
| **record** | 350 | `discogs_full`: `inventory` (John's real store stock) ⋈ `release``release_image` | real Discogs cover art (`monsterrobot.party` CDN, from `local_url`) | real store prices |
| **book** | 311 | `dealgod.products` WHERE `category_path LIKE 'books%'` | real product photos (`image_url`, portrait 176×256) | real `current_price` |
| **toy** | 273 | `dealgod.products` WHERE `category_path LIKE 'collectables-hobbies-toys%'` | real product photos (`image_url`) | real `current_price` |
Build: `build_stock_pack.py --kind <k> --from-tsv` (rows extracted to a gitignored TSV, covers fetched
at build time, resized into atlas cells). **Metadata is parody-transformed** (titles/artists/authors/
brands → house word banks, deterministic, shares no source token — "Dark Side of the Moon"/"Pink Floyd"
→ "Quiet Milk Bar"/"The Galahs") so **no real trademark ships in the pack text**; the **cover images are
real release/product art** (read-only, non-economy, per decision #2 — John's own store inventory + AU
marketplace listings). Books are info-only in the source → **prices seeded deterministically** (John's
call). 15 atlases + 3 indexes: atlases published to the depot (`/api/upload`), indexes committed to
`pipeline/packs/` (the C contract) + staged for `?localdepot`. All 3 pass the pack-QA gate. Toy filter
is broad (some comics/coins among the collectibles) — acceptable for a collectibles shop; tighten later.
## Round 9 — E1: VRAM / texture memory audit (v2.0 ship due-diligence)
Decoded GPU cost = w·h·4 (RGBA) ·1.333 (mipmaps). WebP/JPEG disk size is irrelevant to VRAM — a
2048² atlas is ~22 MB resident regardless of its 0.8 MB file. Measured across every texture source:
| category | files | total VRAM | notes / typical resident |
|---|---|---|---|
| facade skins | 48 | 117 MB | shared per-chunk; only in-view shops resident (~6085 MB) |
| sky domes | 10 | 23 MB | 1 dome resident (~2 MB) |
| ground | 10 | 9 MB | ~3 in view |
| interior tex (floor/wall/counter) | 16 | 12 MB | current room only (~3 MB) |
| wallpaper | 8 | 6 MB | current room only |
| **ped fleet** (19 merged rigs) | 19 | **~90 MB** (15×1024²=5.6 + 4×512²=1.4) | all resident (near-rig variety) |
| GLB fittings/furniture textures | 28 | ~3040 MB | street furniture + current room (~25 MB) |
| **stock: record** | 6 | **134 MB** (256px cells, full) | resident only under `?stock=real` (eager-preloaded at boot) |
| **stock: book** | 4 | **19 MB** (half-res this round) | lazy, on book-shop entry |
| **stock: toy** | 5 | **28 MB** (half-res this round) | lazy, on toy-shop entry |
**Resident-VRAM by boot mode** (peak, approximate):
- **Default boot (no flags):** in-view skins ~85 + ped fleet ~90 + GLB ~25 + Lane-B runtime sign-atlas ~40 ≈ **~240 MB**.
- **`?stock=real`:** +record 134 (eager) → ~375 MB; +book/toy after visiting those shops (stockpack caches, no dispose) +47 → **~420 MB**.
- **Worst case (all flags, all 3 shop types visited):** ~**450 MB** (weather rain = 1 instanced draw, winmap = procedural — both VRAM-negligible).
**Cheap win taken this round:** book + toy atlases half-res'd (256→128 / portrait 176→88 px cells; UVs
are fractional so **no index/UV change**, only the atlas files shrank). Book/toy are quick pull-cards
(buy-anywhere), not the deep record dig — covers verified still legible at half-res. **Stock VRAM
323 → 181 MB** (record kept full for close riffle examination). Atlases re-published to the depot.
**VERDICT: fine to ship v2.0.** ~240 MB default / ~420 MB worst-case is comfortable on desktop
(GB-class VRAM) and within a low-end/integrated ~512 MB1 GB texture budget. The stock packs (181 MB)
remain the dominant swing but no longer blow past the base. **No compression pass or lazy-load
required for the tag.** Two follow-ups flagged for **v3, not now** (bigger than a ship-round change):
1. **stockpack cache-cap** (Lane C's `stockpack.js`): dispose non-current shop-type atlases on
room-type change → bounds stock VRAM to ≤134 MB (one type) instead of accreting all three.
2. **KTX2/Basis GPU-compressed atlases**: 48× VRAM cut with no visible quality loss — the real
long-term fix for the record pack's 134 MB (needs a transcoder + loader change; out of scope now).
**Depot/manifest hygiene (E2):** `validate --depot` 0 err · pack-QA 3/3 valid · provenance-drift clean
(`_published.json` == 27 live procity GLBs) · 15 stock atlases live, **0 orphans**. All green post-R8.
## Round 11 — audio pack provenance
**Every audio file is 100% original procedural synthesis** (`pipeline/gen_audio.py`, numpy on the
MODELBEAST box → ffmpeg). No neural model, no sample library, no third-party recording touched — so
the whole pack is **CC0-equivalent original work** and **parody-safe by construction**: there is
nothing to imitate, every waveform is an oscillator/noise render. Music beds are original chord
progressions/melodies — no imitation of any specific real recording. Tool choice is documented in
`pipeline/README.md` (§ audio): synthesis over local MusicGen/stable-audio because it's $0,
deterministic/seeded, perfectly loopable, and licence-clean.
| group | files | origin |
|---|---|---|
| ambience (6) | street-day/night, rain, roomtone-retail/milkbar/video | filtered-noise beds + synthesised events (birds/crickets/drops/hum) 🟢 |
| sfx (13) | doorbell, door-open, till, riffle, toast, tram-bell, tram-rumble, footsteps ×6 | additive-bell + filtered-noise transients 🟢 |
| music (4) | record-shop, milkbar, video-synth, arcade | original synth loops (bass+chords+drums+lead) 🟢 |
- **Format**: OGG/Opus primary (48 kHz) + M4A/AAC fallback (44.1 kHz). **Loudness**: long beds/music
ffmpeg `loudnorm` I=-16 LUFS / TP=-1.5 (single-pass EBU R128); short SFX peak-normalised (-3…-6 dBFS).
- **Verified** (I synthesise, I can't subjectively listen): spectrogram sheet
`docs/shots/laneE/audio_spectrograms.png` (content/character) + numeric pass — peaks ≤ -0.4 dBFS
(no clip), **all loops seamless** (wrap-jump ≤ internal step), durations correct. **Final subjective
ear-check is John's/F's** — flagged honestly.
- **Budget**: 8.1 MB shipped (2.8 MB ogg + 5.3 MB m4a) vs the 25 MB cap; lazy-loaded per manifest.
- Files ship in `web/assets/audio/` (committed, like `gen/` skins); WAV scratch `pipeline/.genaudio/`
is git-ignored. Contract in `manifest.json``audio:{ambience,sfx,music}`.
## Round 12 — gig layer provenance (v3.0-alpha, `?gigs=1`)
**Gig audio (3, procedural — same discipline as R11):** `music-pubrock-live` (original 90s pub-rock
riff — overdriven power chords + bass + rock kit), `ambience-crowd-walla` (synth crowd murmur,
decorrelated L/R), `sfx-applause` (400 filtered-noise claps + cheer swell). 🟢 100% original synthesis,
$0, seamless loops, parody-safe. Flagged `gig:true` in the manifest; F plays them when a gig is on.
Pack now 26 assets, 9.1 MB of 25 MB.
**Gig posters (4 skins, `skins.poster`):** poster-xerox/screenprint/grunge/retro — 90s pub-rock flyer
templates generated by MODELBEAST `flux_local`. **Blank-name-band discipline** (like blank-signboard
facades): each has a clean top banner and *no baked text*; B/C overprint the seeded band name into
`nameZone` [x,y,w,h]. Eyeballed (contact sheet in the round note) — all 4 have the blank banner, zero
legible text. 🟢 original generation, portrait 768×1024 JPEG in `web/assets/gen/`.
**Gig instruments (5 GLBs, MODELBEAST `flux_local` concept → `trellis_mac` → `normalize.py`):**
🟢 100% original synthesis, $0. **All 5 kept** — every one reconstructed cleanly (better than the
round-3 wire rack; even the mic-stand's thin boom pole and the drum cymbal hardware survived). Held
by D's band rigs; primitive fallback if a GLB is absent (house law).
| instrument | tris | footprint (m) | h (m) | note |
|---|---|---|---|---|
| electric_guitar | 43k | 0.38×0.08 | 1.0 | sunburst solid-body, clean |
| bass_guitar | 36k | 0.34×0.12 | 1.15 | black bass, clean |
| drum_kit | 30k | 1.50×1.24 | 1.3 | full kit, cymbals survived |
| guitar_amp | **96k** | 0.51×0.29 | 0.6 | combo amp — heaviest (grille cloth → shell-soup) |
| mic_stand | 34k | 0.57×0.57 | 1.7 | chrome boom stand, thin pole held |
- Same TRELLIS tri-weight tradeoff as round-3 props (3096k tris vs the ≤5k library law) — accepted
for focal/optional/LOD-able stage props seen at mid-distance; texture-bake-to-lowpoly is the
follow-up. Amp (96k) is the outlier — a candidate for that pass first. Eyeballed (contact sheet).
- Staged to `web/assets/models/` for local validation (`?localdepot=1`). Depot publish was
**PENDING John's OK** at R12 → **DONE in R13** (see the R13 section below; the 3096k figures in
the table above are the R12 originals, decimated to 14k each in R13).
## Round 13 — gig-layer finalization (v3.0-beta, `?gigs=1`)
Supersedes the R12 instrument table + pending-publish note above. Three closes, all 🟢 provenance
unchanged (100% on-device synthetic / procedural — decimation and renaming don't touch the licence zone):
**1. Instrument decimation (`pipeline/decimate_props.py`, Blender collapse-decimate — NOT voxel-remesh,
so thin features survive).** The R12 instruments were the TRELLIS shell-soup tri-weight outlier; a
district means multiple concurrent stages, so all 5 were taken to a uniform **14 000 tris** (fleet
**236k → 70k, 70%**). Collapse (keeps original UVs/textures, no re-bake) held the mic boom pole + cable,
drum cymbals/stands and guitar necks — verified by re-rendered thumbs. The high-poly originals are kept
as `_hi` variants (published to the depot as archival source, not manifest-referenced).
| instrument | R12 tris → R13 | `_hi` original | footprint (m) | h (m) |
|---|---|---|---|---|
| electric_guitar | 43 451 → **14 000** | 43 451 (`_hi`) | 0.38 × 0.08 | 1.00 |
| bass_guitar | 36 031 → **14 000** | 36 031 (`_hi`) | 0.34 × 0.12 | 1.15 |
| mic_stand | 34 079 → **14 000** | 34 079 (`_hi`) | 0.57 × 0.57 | 1.70 |
| drum_kit | 26 238 → **14 000** | 26 238 (`_hi`) | 1.50 × 1.24 | 1.30 |
| guitar_amp | 96 583 → **14 000** | 96 583 (`_hi`) | 0.51 × 0.29 | 0.61 |
**2. gigKey rename + two new beds (debt #1).** The manifest music key IS the gigKey, always
`gig-<genreKey>`. `music-pubrock-live`**`gig-pubrock`**; added **`gig-grunge`** (band_room) and
**`gig-covers`** (rsl, organ-led) — all procedural numpy synth, $0, seamless, parody-safe (new synth
helpers `organ()` + `grungechord()`). Audio pack **26 → 28 assets**. Old `pubrock-live` files deleted.
**3. Depot publish — DONE (John OK'd direct publish, R13).** Pushed via the passwordless tailnet
ingress (this box is allowlisted): the 5 base names now serve **14k bytes** + the 5 `_hi` originals are
live. Verified on the public `digalot.fyi` depot (amp base **5.9 MB → 1.31 MB**, `_hi` 5.9 MB live).
`_published.json` **33 → 38**. `validate_manifest --depot` GREEN; Gate-3 provenance flag **cleared**.
## Round 36 wave 1 — the 2 a.m. ped assets, swept (provenance + depot regularization)
`046668e` (4 dance clips) + `b339402` (**5** trellis DJ bodies — the R36 brief said 3; the diffstat
says 5) landed July 20 outside the round system. R36 wave 1 swept all **9** into the pipeline; the
files themselves are untouched (byte-identical to both commits and to their 90sDJsim originals).
**Dance clips — zone corrected by measurement: ROKOKO, not Mixamo.** Source = Rokoko's free 263-asset
mocap library (`~/Documents/Rokoko_Free_Mocap_FBX_263`, `Rokoko Studio (Mocap)/Dancing/*_mixamo.fbx`,
Rokoko's own Mixamo-skeleton export), baked mesh-free by 90sDJsim `tools/bake_clips.py` (the
`fbx_to_clip.py` port). Licence per the pack's rokoko.com page: royalty-free including commercial use.
Structure (all 4): 0 meshes · 68 nodes · 198 tracks · 52-joint dangling skin with IBM · 0 NaN/Inf ·
binds 65/66 canonical targets on a shipped ped skeleton (only `mixamorigReference` unbound — dropped
silently by three.js; matches 90sDJsim's in-game 64/65). Durations 30.58/30.50/44.04/52.42 s.
| clip | bytes | sha1 |
|---|---|---|
| dance_party.glb | 688 220 | 38587f7b0cf4aaf199f55677a139db087acdd80d |
| dance_medium.glb | 1 028 164 | 4a30fb62163b6e5137c8fb513dfc70d5f6f322b4 |
| dance_drink.glb | 962 464 | d818ddb0868e19c87a435b26c8b6ffab14832be4 |
| dance_sway.glb | 1 132 560 | 8af9112538e5219fc6a7b6cb36bf1e87e5ac24ff |
**DJ bodies — on-device TRELLIS.2 mesh (CC0-equiv, the instrument zone) + Adobe Mixamo auto-rig,
converted/decimated by MESHGOD `scripts/fbx_to_ped.py`.** GLB law: no Draco, 1024² webp
(`EXT_texture_webp` — roster-standard), 1 mtl/1 img, 65-joint mixamorig skin, IBM 0 NaN. Ped law ≤5k:
PASS. ⚠ bind-pose skeleton spans ~0.950.99 units vs the existing roster's ~173180 (cm convention) —
absorbed at spawn (`rigs.js:163` height/span normalize), flagged to F's R36-w2 figure-scale gate.
| body | tris | bytes | sha1 |
|---|---|---|---|
| dj_phrtt_01.glb | 4 000 | 712 792 | 21cf0db5a291f88f7fd9b9cfdb0eac8413c7cf2e |
| dj_techno_01.glb | 4 000 | 702 760 | 4cb6bf44a752f921e14d1355f9944b7859c92386 |
| woman_dj_01.glb | 3 999 | 731 440 | 7d364dfc7e7659c0b15bead82a5d8bbd285e5286 |
| woman_dj_02.glb | 3 999 | 715 116 | 2413ededff1a7684999d6fd6fd58417b77f25c10 |
| woman_dj_03.glb | 3 998 | 718 128 | ec39692fa8019645bd200aab9eb6d1078535189c |
**Depot:** staged to `_normalized/` (the sit/look practice) and published via the passwordless tailnet
ingress under John's R22 standing authorization — `_published.json` **44 → 53**, drift check green, and
all 9 fetched back from the public depot (`digalot.fyi/3god/a/`) sha1-identical to local. Gates after
the sweep: `validate_manifest` 0 errors / 0 warnings · `publish.py --verify` ✓ 53/53 live.
## Round 38 — v8 wave 1: the asset wave (pipeline swap · audio · grounds · the mesh probe)
*Everything below is measured on this box today. Where the R38 review's costed figures differ from
what the hardware actually did, the measured number is given and the difference is named.*
### 1. THE PIPELINE FIX — `trellis_mac` → `trellis2_mlx`, and the output-contract trap it hides
`gen_props.py` hardcoded **`trellis_mac`** (the review's farm telemetry: p50 245.4 s, 33 jobs/7 d).
Swapped to **`trellis2_mlx`** (384 jobs/7 d, p50 134.6 s, better PBR bake). Operator contract read
from its `manifest.json`, not assumed — `pipeline_type` / `seed` / `steps` / `baker` /
`texture_size` / `max_bake_faces` / `alpha_mode`; we pass `1024_cascade`, `baker:metal`,
`max_bake_faces:500000` (fal parity, M3-Ultra-verified), `alpha_mode:opaque`.
**The two operators are NOT drop-in on output, and the naive swap would have shipped untextured
meshes.** `trellis_mac` writes one `*.glb`; **`trellis2_mlx` writes BOTH `candidate.glb` (raw) and
`candidate_pbr.glb` (baked)** into the same tree, and the old code took `sorted(rglob('*.glb'))[0]`
— which is alphabetically the **raw** one. Fixed with `_pick_glb()`: trust the operator's own
`result.json` first, then `candidate_pbr.glb` by name, then anything. `--legacy-trellis` keeps the
old operator for an A/B.
Also added the **`bg_remove_local` (RMBG-2.0) cutout stage** the review's path calls for
(flux → bg_remove → trellis2 → normalize), fail-soft: if RMBG errors the raw concept is fed through
and the mesh operator segments internally as before.
**Measured on the 5-prop R38 batch (wall clock, mesh stage only):** 122 s · 83 s · ~108 s · ~123 s
**mean ≈ 109 s**, i.e. *faster* than the farm's 134.6 s p50 and **~2.2× the old operator's
245.4 s**. The cutout stage costs **56 s** per prop. **The review's ~2× throughput claim is
confirmed with a real number.** Retry budget went unused: **5/5 first-attempt success, 0 failures,
0 OOM-kills.**
### 2. AUDIO — the inhabitation layer (item 1.1). 6 new PACK entries, $0, 🟢 original
Same doctrine as R11/R12 and for the same reasons: **100% procedural numpy synthesis**, no neural
model, no sample library, no third-party recording — **CC0-equivalent original work, parody-safe by
construction** (every waveform is an oscillator or a noise render, so there is nothing to imitate).
Deterministic through `crc32(name)` → the same dog is the same dog on every boot.
| asset | cat | dur | ogg | m4a | synthesis |
|---|---|---|---|---|---|
| `sfx-dog-bark-fence` | sfx | 3.2 s | 16 KB | 37 KB | 8 barks: pitch-dropping saw through 2 vocal-tract formants + attack noise; 2.2 kHz lid + a 28 ms paling slap-back + yard reverb 🟢 |
| `sfx-magpie-carol` | sfx | 2.6 s | 28 KB | 42 KB | fluted glides under a 2852 Hz quaver, **every note doubled a fifth up** (the syrinx has two sides — that is why a carol sounds like two birds) 🟢 |
| `sfx-roller-door` | sfx | 2.8 s | 24 KB | 45 KB | slow-fast-slow clatter train + sheet-metal drum + panel modes **amplitude-followed** off the clatter + the stop 🟢 |
| `ambience-mower-distant` | ambience | 12.0 s | 78 KB | 190 KB | firing pulse train w/ load wander, blade hiss, 900 Hz lid. Loop **genuinely folds**: every LFO is a harmonic of 1/12 s and the firing rate is a whole number of cycles per loop 🟢 |
| `ambience-radio-through-window` | ambience | 8.6 s | 82 KB | 136 KB | an **original** 4-bar turnaround (D A G E) → 3802600 Hz little-speaker band → 1.5 kHz glass lid 🟢 |
| `ambience-sprinkler` | ambience | 8.0 s | 53 KB | 127 KB | impulse arm ticking across a 62% arc then swinging back silent, under a sweeping spray hiss 🟢 |
**Budget: +281 KB ogg / +577 KB m4a = +0.86 MB.** Pack 10 MB → **10.9 MB against the 25 MB cap**,
which matches the charter's ~11 MB estimate. **Zero draws, zero tris** — as chartered, this item
cannot touch the render budget under any circumstance.
**Verified** (I synthesise; I cannot subjectively listen): spectrogram sheet
`docs/shots/laneE/inhabitation_spectrograms.png` + a numeric pass — peaks **0.330.71** (no clip),
loop wrap-jump **≤ 0.18× the 95th-percentile internal step** on all three beds (radio 0.00×,
mower 0.08×), and band-energy fractions that match the intent: mower **99% below 1 kHz**, radio
**87%/13%/0%** across <1k/14k/>4k, carol **86% in 14 kHz**, bark **89% below 1 kHz**.
**One defect found and fixed by looking**: at spray gain 0.5 the sprinkler's hiss masked its own
ticks and it read as a hose — the tick *is* the sprinkler. Rebalanced to 0.28 and re-rendered; the
ticks and the silent swing-back are now visible in the sheet. **Final subjective ear-check is
John's / F's** — flagged honestly, same as R11.
### 3. GROUND SKINS — the per-town palette (item 1.2). 4 new, `flux_local`, $0, 🟢
`gen_skins.py` had **no GROUND template at all** — the ten shipped grounds predate it. Added one,
plus a `--grounds` mode and a dedicated `GROUND_STYLE` prefix: the house `STYLE` bible says *"warm
fluorescent lighting"*, which is an **interior** cue and wrong for an outdoor overhead surface.
| skin | use | px | bytes | note |
|---|---|---|---|---|
| `ground-grass-dry` | yard | 512² | 157 801 | straw-blond patchy summer lawn, bare soil through it 🟢 |
| `ground-grass-temperate` | yard | 512² | 183 917 | dense damp deep-green lawn + clover 🟢 |
| `ground-coastal-sand` | yard | 512² | 131 231 | pale wind-rippled sand + shell grit 🟢 |
| `ground-bitumen-reddust` | road | — | — | **re-rolled, see below** |
**Cost correction.** The review costs `gen_skins.py --local` at **5.7 s**; measured here at
**~9.7 s each**. That is not a regression — the review's figure is for the 1024×576 facade
geometry and these are **1024² square tiles, 1.78× the pixels**. Per-pixel the two agree. Square is
the deliberate choice: grounds tile through `RepeatWrapping`, and the two `djsim-*` grounds (the
only 512×512 ones) are the correct precedent — the eight flux-era grounds are **512×279** and
stretch under a square UV tile. That is a pre-existing defect, recorded not fixed.
### 4. MESHES — the proof batch (items 1.3 + 1.4), `flux_local` → `bg_remove_local` → `trellis2_mlx`
**Licence: 🟢 CC0-equivalent** under `AUDIT.md:101`'s standing doctrine — a `flux_local` concept on
an Apache-2.0 ungated model (FLUX.2-klein-4B), reconstructed by TRELLIS.2 into original geometry.
Nothing downloaded, nothing pooled. **The 3GOD depot was not read as a pool** (971 live, 53
recorded, known trademarked items inside — it stays a read-only archive) and **the hardyards GLBs
are REJECTED on provenance**, as the round ruled: 39 GLBs with provenance documented for one file.
Every one of these five is ours.
**Operator timings, all five, wall clock (includes the 56 s RMBG cutout):**
| prop | trellis2_mlx | raw tris | outcome |
|---|---|---|---|
| magpie | **129.0 s** | 473 513 | ✅ shipped 894 tris |
| hills-hoist | **88.1 s** | 503 716 | ❌ **thin-structure failure** |
| paling-fence-panel | **112.3 s** | 479 611 | ✅ shipped 1 121 tris |
| tv-aerial | **128.0 s** | 494 204 | ❌ **thin-structure failure** |
| water-tank | **489.0 s** | 475 047 | ✅ shipped 500 tris |
**median 128.0 s** — inside the farm's 134.6 s p50 and **1.92× faster than `trellis_mac`'s 245.4 s**.
Mean is 189.3 s, dragged by one 489 s outlier (3.8× the median, same params, same box, no error) —
worth naming: **the operator's tail is long even when nothing fails.** 5/5 first-attempt success,
**0 retries used** against the 45% budget, 0 OOM-kills.
#### 4a. SHIPPED — three assets, all GLB-law clean
House law verified on every one: metres · +Y up · base origin · **1 material / 1 image / 512² WebP**
· no Draco.
| id | tris | footprint (m) | height (m) | notes |
|---|---|---|---|---|
| **magpie** | **894** | 0.15 × 0.40 | 0.358 | the 1.3 asset. Silhouette excellent at 894 — beak, legs, folded wing and tail all read. **Albedo flagged, see 4c.** 🟢 |
| **paling_fence** | 1 121 | 0.28 × 3.59 | 1.788 | weathered grey palings with gaps + two rails. Overshot its 400 target (see 4b) 🟢 |
| **water_tank** | **500** | 1.73 × 1.64 | 2.186 | corrugated galvanised tank, domed lid, outlet tap. **Cleanest asset in the batch** 🟢 |
Contact sheet (4 angles each): `docs/shots/laneE/r38_props_shipped.png`.
**⚠ NOT PUBLISHED — one step outstanding.** All three are normalized, baked to budget and staged in
`pipeline/_normalized/`, but they are **not on the 3GOD depot** and therefore deliberately **not in
`manifest.json`**: pushing to a public depot is an outward-facing action and this lane had no
in-session authorization for it. `validate_manifest.py` correctly fails any manifest GLB with no
`_published.json` record, so the results file is held at **`pipeline/_r38_results.PENDING_PUBLISH.json`**.
`build_manifest.py` is already wired to `_r38_results.json`: **publish, rename, re-run** and the three
land. The gate is green as committed (0 errors, 0 warnings).
#### 4b. THE DECIMATION FLOOR — a real finding, not a tuning miss
**`normalize.py`'s collapse decimator cannot reach small budgets on TRELLIS output**, and nobody had
ever asked it for ≤900 before. Measured, all five: 470 442→**13 849** (target 900) · 478 236→**250 068**
(600) · 476 974→**18 791** (400) · 485 565→**89 306** (400) · 475 047→**70 893** (500). The cause is
the documented shell-soup floor — collapse cannot merge across disconnected shells, so the floor *is*
the shell count. `bake_lowpoly.py` (R5's voxel-remesh → decimate → Cycles bake) is the correct tool
and clears it: magpie **894**, water_tank **500**, tv_aerial 299, hills_hoist 496. paling_fence floored
at **1 121** against a 400 target — a flat panel of separate palings has an irreducible shell count at
voxel 0.012; it is shipped at 1 121 rather than remeshed coarser, because coarser merges the palings
into a slab and the gaps are the whole point of a paling fence.
#### 4c. THE TWO FAILURES — thin structures, exactly as the review predicted
**`hills_hoist` and `tv_aerial` FAIL and are NOT shipped.** Both were deleted from `_normalized/`
rather than left where a later manifest build could catalogue them. Evidence:
`docs/shots/laneE/r38_thin_structure_failure.png`.
TRELLIS reconstructs the *solid* parts well — the hoist's pole, concrete footing and four arms are all
there at 250 k tris, and it is recognisably a Hills Hoist. **The wire is what dies**: it comes back as
thousands of tiny disconnected fragments, which is simultaneously why the mesh reads as frayed *and*
why it will not decimate. And **both escape routes fail in opposite directions**: collapse floors at
250 k / 89 k with the fraying intact, and a voxel remesh coarse enough to reach 600 tris **erases the
pole entirely** — the 496-tri hoist is a floating horizontal smear with no upright, and the 299-tri
aerial is scattered debris. There is no voxel size that keeps a 30 mm pole and a 2 mm wire at 600 tris.
**This is a DECISIVE capability answer, and it is the one the round asked for.** Thin-structure props
are not a TRELLIS class. **→ Lane B: use primitives for the hoist and the aerial**, which is the
`boxMats` path anyway and costs **+0 draws** — strictly better than the GLB would have been.
#### 4d. THE ALBEDO PROBE — TRELLIS crushes high-contrast markings, and the input is exonerated
The magpie came back as an **all-black bird — a crow, not a magpie**. Traced properly rather than
guessed at, because the fix differs completely depending on where it happened:
1. **The input is provably perfect.** The `bg_remove_local` cutout fed to TRELLIS has a bright white
nape, white wing patches, a white tail tip and a pale blue-grey beak. Not the concept's fault, not
RMBG's.
2. **The raw 500 k TRELLIS mesh has already lost it** (`docs/shots/laneE/r38_magpie_albedo_probe.png`):
the white nape survives only as a pale blue-grey wash and the wing bar and tail tip are gone. Its
1024² albedo atlas is near-uniformly black with sparse pale slivers. **So the loss is the
operator's PBR bake — our normalize/decimate/re-bake chain is exonerated.**
3. **The documented workaround does not work through our normalizer.** The manifest advertises
`baker: "vertex"` as "the 1s clean-albedo bake, cleanest colors". Re-ran the magpie with it
(159 s): the GLB comes back with **0 materials and 0 images** — colour lives only in vertex
attributes, and `normalize.py` (join → decimate → re-export) drops them. The result renders
**pure white**. `baker: vertex` is unusable here without normalizer work.
**Shipped anyway, flagged honestly**, because the silhouette is what a swooping bird reads as at
street distance and 1.3 cannot ship without a mesh. **Fix path for a later round** (stated, not done):
bake the LP's albedo from the *concept image* rather than from the reconstructed mesh, or hand-tint
the nape/wing/tail regions of the 512² baked atlas. **Both are cheap; neither is this round's job.**
### 5. REUSE BEFORE GENERATING — the review's nine, re-measured, and its "one line each" corrected
All nine confirmed idle and re-measured off the bytes. **But "one line each" is wrong for four of
them**, and the reason is in `furniture.js`'s own merge contract: `extractGLB` merges every mesh into
ONE geometry and **keeps only the FIRST material**. So a multi-material GLB does not fail — it renders
**entirely in material #1**, silently.
| id | tris | mtl | verdict |
|---|---|---|---|
| `novelty_record` | **8 000** | 1 | ✅ true one-liner. `furniture.js:275` already calls `glb('novelty_record',…)`; `GLB_FILES` has no key, so the call can never fire. **The blocking comment at `furniture.js:30` says "26.5k tris" and is STALE** — Lane E baked it to 8 000 in R5. |
| `streetlight` | **4 999** | 1 | ⚠️ **decimate first.** It is placed every ~18 m — at ~50 instances that is ~250 k tris on a 200 k budget. The review is right that it needs `decimate_props.py` to ~800 before it is wired. |
| `bench_modern` | **640** | 1 | ✅ true one-liner |
| `longbench` | **1 172** | 1 | ✅ true one-liner. 5.86 m — the arcade/market feature bench |
| `park_bench` | **5 000** | **2** | ⚠️ renders in material #1 only |
| `bench_wood` | **3 999** | **0** | ⚠️ **zero materials** → falls to `extractGLB`'s grey `0x888888` default |
| `coffee_table` | **495** | **2** | ⚠️ material #1 only |
| `box_crate` | **78** | **3** | ⚠️ material #1 only |
| `counter` (balcao) | **276** | 1 | ✅ 1.06 × 4.02 m, 1.10 m — exactly the `bar_counter` fitting still primitive-only in all three venue types. Different loader (`interiors/glb.js`), not `furniture.js`. |
**Nothing was re-generated that already existed.** No duplicate of any of these was made this round.
### 6. LICENCE — every R38 asset lands 🟢
| class | count | zone | method |
|---|---|---|---|
| audio | 6 | **🟢 original by construction** | pure numpy synthesis, `gen_audio.py`, deterministic via `crc32(name)` |
| ground skins | 4 | **🟢 original** | `flux_local` / FLUX.2-klein-4B (Apache-2.0, ungated), on-device |
| meshes | 3 shipped | **🟢 CC0-equivalent** (`AUDIT.md:101` doctrine) | `flux_local` concept → `bg_remove_local` (RMBG-2.0) → `trellis2_mlx` (TRELLIS.2-4B) → `normalize.py``bake_lowpoly.py` |
**The 3GOD depot was NOT used as a pool** — it stays a read-only archive (971 live, 53 recorded,
known trademarked items inside). **The hardyards set stays REJECTED on provenance** (39 GLBs,
provenance documented for one file); every mesh here is ours. **$0 spent — no cloud key touched.**
### 7. SESSION 2 — the depot step, the magpie tint, and the sizing law
*Everything in §7 is measured on this box after `1568c9b`; nothing in §1§6 is retracted.*
#### 7a. PUBLISHED — three GLBs, verified by fetching the bytes back off the PUBLIC depot
Pushed under **John's R22 standing authorization for Lane E assets** (the same authority as
`sit.glb` in R28 and the nine R36 sweep assets) through the passwordless tailnet ingress
(`GOD3_DEPOT=http://100.94.195.115:8788`, decision #4 — no secret read, no cookie, `publish.py`
never logs one). `_published.json` **53 → 56**; post-publish drift check `✓ all 56 recorded assets
live`. Thumbs uploaded with each (`glb:200 thumb:200`).
Then verified the way this lane always verifies: **fetch it back off the public host and compare
hashes**, not "the upload said 200".
| file | HTTP | local B | depot B | sha1 (identical both sides) |
|---|---|---|---|---|
| `procity_street_magpie_01.glb` | 200 | 132 592 | 132 592 | `b5f02b26d15f7a58f8761f54fdb98b14fa65ee10` |
| `procity_street_paling_fence_01.glb` | 200 | 165 752 | 165 752 | `a580a8833f02bccb2433776072bca2d4fbee8050` |
| `procity_street_water_tank_01.glb` | 200 | 77 152 | 77 152 | `9b08fb9774e892f04d5c36a09ab2100383f4e360` |
`_r38_results.PENDING_PUBLISH.json``_r38_results.json`, `build_manifest.py` re-run — manifest
GLBs **33 → 36**, all three under `furniture`. **`validate_manifest.py`: 0 errors, 0 warnings.**
#### 7b. THE MAGPIE TINT — a 3-D repaint of a 2-D atlas, because the atlas is unreadable by eye
§4d left the bird an all-black crow with the loss traced to `trellis2_mlx`'s PBR bake. The stated
fix path ("hand-tint the 512² atlas") is **not a paint job**: the atlas is a smart-UV bake, ~100
scattered islands, and no island announces itself as a nape. New tool **`pipeline/tint_atlas.py`**:
1. rasterize every UV triangle into the atlas grid → **texel → (position, normal)** maps
(barycentric, glTF top-left UV origin, 1-texel conservative pad) — 101 086 / 262 144 texels
covered, 38.6 %;
2. evaluate **soft anatomical windows on the POSITIONS** in the mesh's own metric frame
(x ±0.0777 lateral · y 0→0.3583 up · z 0.2021 tail → +0.2021 beak): white **nape** behind a
black crown, white **shoulder/wing bar** on the flanks only so the dorsal midline stays black,
white **rump + tail base** stopping short of the black terminal band — Australian magpie
(*Gymnorhina tibicen*), adult, black-backed form;
3. paint with the bake's own luminance kept as shading (`0.88 + 0.12·smoothstep(luma)`), so it is
white with the bake's light in it, not a flat decal;
4. **dilate 5 texels into the bake's UV padding**, or bilinear/mip at an island edge would pull the
old black through;
5. re-embed as WebP and rewrite the GLB. The image bufferView is verified last in the buffer, so
**geometry is untouched byte for byte**.
Because the masks live in 3-D, the soft edge is consistent across every island simultaneously — a
2-D blur cannot do this at all (it would bleed island-to-island through the black background).
**Result, measured:** painted texels' median luma **0.843** (p90 0.949) vs the bake's 0.133; whole
atlas 0.075 → 0.207. **GLB law re-checked after the rewrite: 894 tris · 1 mesh · 1 material · 1
image · 512² WebP · no Draco · 0.155 × 0.358 × 0.404 m — every number unchanged.**
**Verified in the game's own renderer, not just in Blender.** The *published* file was loaded off
`https://digalot.fyi/3god` through the vendored `GLTFLoader` + three.js (`MeshStandardMaterial`,
hemisphere + sun, sky background): **1 mesh, 894 tris, 512×512 texture decoded** — which also proves
the PIL-written WebP decodes in a browser. Sheet: `docs/shots/laneE/r38_magpie_tint.png`
before/after × 5 angles (EEVEE, the GLB's own material incl. the 0.28 emissive) + a **64 / 48 / 32 px
distance strip** (a 0.36 m bird at 310 m on a 1080-high screen is 50100 px) + the three.js frame.
**VERDICT: it reads at the size and speed the bird is seen.** Pied and unmistakable at 64 px and
48 px from flank and rear-quarter; still clearly black-and-white at 32 px. **Honest caveat:** from
directly below-and-in-front it stays dark — correct behaviour, because a real magpie's bib and belly
ARE black; the white is nape, shoulders and rump, i.e. what you see as it comes over and as it
leaves. Thumbnail re-rendered from the **rear quarter** of the same `bake_lowpoly` camera rig
(`render_views.py --thumb --rear`): the roster's standard front-quarter looks straight at a magpie's
black face and would have recorded none of the plumage.
**Standing after Lane B's `4514cb9`: the tinted GLB is an UPGRADE PATH, not a dependency.** B shipped
a 182-tri procedural bird with the marks as vertex colours, fetching nothing — 4.9× lighter, and the
right call. The GLB is on the depot and in the manifest; **nothing probes for it and nothing should**
(R26's blind-probe ruling). Cost if ever swapped in at `birdGeometry()` / `birdMaterial()`: +1 draw,
+894 tris at one instance.
#### 7c. THE SIZING LAW — `paling_fence` is a good asset and a rejected one
Lane B's measurement: **5 runs × 181 lots = 905 instances × 1 121 tris = 1.01 M triangles** against
~75 k spare (~47× over), and a GLB cannot ride the `boxMats` InstancedMesh that makes the other yard
props +0 draws. **REJECTED; it ships as a primitive.** `water_tank` (500 tris, one per yard, low
count, replacing a 24-tri pair) is **ACCEPTED**.
**The law for every later asset round: a mesh's triangle budget is `tris × the instance count the
placement rule implies`, not "≤5 k like a prop".** A fence panel repeated on every lot boundary is
in a different budget class from a one-per-town water tank, and the number to beat is set by the
placement *rule*, not by the asset. **Get the instance count from the consuming lane BEFORE
generating anything that tiles, edges, or repeats per-lot.** The published fence stays on the depot
and in the manifest — harmless, provenance-recorded, available if a low-count use ever appears.
#### 7d. New pipeline tools (all $0, on-device)
| tool | what it does |
|---|---|
| `tint_atlas.py` | the 3-D-region atlas repaint above; `--debug` renders the region masks as flat colours so the anatomy can be checked before any paint lands |
| `render_views.py` | Blender view sheet from the angles an asset is *seen* from (flank / rear-quarter / swoop / top / rear), `--eevee` for the GLB's own material under sky, `--thumb [--rear]` to re-render a roster-consistent 256 px thumbnail |
| `view_sheet.py` | composites those into one labelled sheet + the **distance strip** — the check no 256 px thumbnail can make |
**Licence unchanged: 🟢.** The tint is our own numeric edit of our own baked atlas; no asset, no
model and no third-party image entered the pipeline for it. **$0 — no cloud key touched.**
## Round 39 — the magpie A/B, and the arcade's kit (v9 39.4)
*Every number below was measured on this box today. `$0` — no cloud key touched, no third-party
asset or image entered the pipeline, and the 3GOD depot was **not** read as a pool (it stays the
read-only archive R38 declared it).*
### 0. STANDING INSTRUCTION — the sizing law, promoted out of the round notes
**Before generating anything that tiles, edges, repeats per lot, or is placed by a rule rather than
by hand: get the instance count from the CONSUMING LANE. The budget is `tris × that count`, not
"≤5 k like a prop".** R38 paid for this with `paling_fence` — a good 1,121-tri panel that became a
**rejected asset** at 5 runs × 181 lots = 905 instances = 1.01 M triangles. Two clauses were added
in R39 and both are now part of the law:
1. **Draws are counted per CLASS, and where the class lives decides everything.** A GLB cannot ride
`boxMats` (BoxGeometry, one shared untextured shell material), so every distinct GLB geometry is
+1 draw. As ONE town-wide `InstancedMesh` (`magpie.js`'s pattern) that is +1 **total**; as a
per-chunk furniture class it is +1 × up to **25 live chunks**, which against 8 draws of margin is
a breach, not a cost. Ask which one before generating.
2. **For a SKIN the budget is not bytes, it is the SLOT.** `skins.js:facadeAtlasUV` has **36 slots
(6×6)**; past 36 it `console.warn`s and **reuses one**, i.e. a 37th facade skin silently
repaints an existing shop. Measured: the synthetic uses **22 distinct facade skins** (21 at seed
123456) ⇒ **14 free**. That is the real budget line for a facade skin.
The law is now runnable rather than conversational: **`node pipeline/arcade_measure.mjs`** reads
Lane A's own `generatePlan` and prints the counts. It changed this round's deliverable — one of the
four commissioned assets stopped being an asset (§3, item 4).
### 1. THE MAGPIE A/B — rendering Lane B's own geometry, not a re-implementation of it
The R38 debt was *"two magpies exist and nobody has seen them side by side."* The trap in settling
it is that a hand-ported strawman proves nothing, so B's bird is not ported:
- **`pipeline/dump_bird.mjs`** imports `web/js/world/magpie.js` **unmodified** and calls the real
`createMagpie()`. The bare `three` / `three/addons/` specifiers its imports use are resolved to
the repo's **own vendored build** (`web/vendor/three.module.js`) through a `node:module`
`registerHooks` resolver — the importmap from `index.html:22-25`, as a node resolver. The vertices
dumped are the vertices the browser gets.
**Measured: 182 triangles, 216 verts, bbox 0.680 × 0.166 × 0.670 m.** The arithmetic agrees
independently: sphere(8,6) = 8×(2·62) = 80 · sphere(6,5) = 48 · cone(5) = 5 side + 5 cap = 10 ·
3 boxes = 36 · 4 planes = 8 ⇒ **182**. `magpie.js:9` and `:123` say **154** and are stale.
- **`pipeline/bird_to_glb.py`** wraps the dump as a GLB **in E's frame** (glTF +Y up, head at +Z;
three's frame is +Y up, nose at Z, and Blender's `export_yup` writes `glTF = (Bx, Bz, By)`, so
the Blender vertex is `(xt, zt, yt)` — a 180° yaw, det +1, winding preserved). Without that the
two rows of the sheet would be shot from opposite ends of the bird. `--fold` applies the perch
pose (`place(..., folded)` scales the instance X by 0.42). `glb_stat.py` re-measures **182**.
- **`pipeline/render_views.py --noemit`** strips emission from **both** candidates, and this is the
correction that makes the picture usable at all. `normalize.py` gives every baked GLB
`emissiveFactor 0.28` **with an `emissiveTexture`** — 0.28 × its own albedo, so blacks stay black
and whites brighten. A vertex-coloured mesh **cannot express that in glTF** (COLOR_0 multiplies
base colour only), and Blender's exporter silently writes a **flat 0.28**, which lifted B's black
bird to grey and produced a first sheet that was worthless in E's favour. Off both sides. The
consequence, stated on the sheet: **a baked GLB reads slightly better in game than it does here**
the safe direction to be wrong in.
- **`pipeline/view_sheet.py --ab`** lays the distance strip out as a **grid** (one line per
candidate, same view and same pixel size stacked) and repeats it under **linear motion blur**
a true N-position average, not a gaussian, with the smear length scaled to the SUBJECT's on-screen
width. Two levels, both derived: **25 %** (the approach) and **100 %** (the pass — the swoop
covers 34 m in 1.25 s = 27.3 m/s = 0.45 m per 60 fps frame ≈ one body length).
**The measurement that decided it** (`dump_bird.mjs --sim`, magpie.js's own clock: DEFEND_R 34 m,
drawn out to 74.8 m, COOLDOWN 5.5 s, SWOOP_T 1.25 s, RETURN_T 1.9 s; a 60 s walk at WALK 4.6 m/s
passing 4 m off the perch):
> the bird is drawn in **1,949 of 3,600 frames (54.1 %)** — and of those it is
> **perched 1,569 (80.5 %) · swooping 150 (7.7 %) · returning 230 (11.8 %)**.
**And the mechanism, which is why no re-tint could have saved B's bird.** Three of its four white
marks cannot be seen from where the player stands:
| mark | `magpie.js` | why it does not read |
|---|---|---|
| wing bars ×2 | `:145-148`, bar plane at **y 0.048**, wing plane at **y 0.045** | both horizontal (`rotateX(-π/2)`), both DoubleSide, and the bar's footprint (x 0.1740.316, z 0.0050.055) lies **entirely inside** the wing's (x 00.34, z 0.0850.065) ⇒ **from underneath the black wing occludes its own white bar**, and the player is always underneath (perch 4.32 m vs EYE 1.62 m) |
| nape | `:140`, box `0.052×0.014×0.055` at y 0.06, z 0.10 | at z 0.10 the head sphere's surface is **y 0.0728**; the box tops out at **0.067** ⇒ buried, and only its rear ~12 mm (z > 0.085) ever clears both head and body |
| nape + rump, from the flank | `:140-141` | horizontal slabs **14 mm and 20 mm** thick on a **166 mm** tall bird. At 64 px for a 0.67 m bird one pixel is ~10 mm ⇒ **1.3 px and 2 px of white** — and the flank is 80 % of the frames |
**VERDICT — Lane E recommends TAKING THE GLB**, with the cost stated: **+712 tris at one instance**,
draws a **wash** (both are one town-wide `InstancedMesh` at count 0 or 1), **+1 fetch / +1 material /
+1 512² texture**, and it is a **use-if-ready second path**, not a substitution — `?noassets` and
`?classic` still need the procedural bird. No blind probe is implied: `magpie` has been in
`manifest.json` and on the public depot since R38 §7a, so this is `furniture.js`'s existing shape.
Sheet: **`docs/shots/laneE/r39_magpie_ab.png`**. Fable rules.
### 2. THE ARCADE, MEASURED (`pipeline/arcade_measure.mjs`, 5 seeds, off Lane A's `generatePlan`)
| | measured |
|---|---|
| lane | ONE edge `kind:'arcade'`, **42.00 m × 5 m**, 2 blocks |
| shops | **1617** (17 at 20261990 / 20260101; 16 at 7 / 123456 / 999) |
| frontage | 3.004.94 m, mean **3.874.05 m**; depth 5.107.99 m; 411 two-storey |
| walkable | lot faces at ±2.50 m ⇒ **5.00 m** |
| roof | `AWNING_DEPTH 2.2` from each face ⇒ slabs reach ±0.30 m ⇒ **roofed except a 0.60 m centre slot**, ceiling **y 2.95 m** |
| floor | `ground.js`'s arcade branch: ONE quad **42.00 × 12.00 m = 504 m²**, wearing `footSkin` |
| ⚠ posts | `buildings.js:643` at `d/2 + 2.2 0.2`**±0.50 m from the centreline: two post rows 1.00 m apart down a 5.00 m lane.** No collider, so it walks — it just reads as a colonnade in its own doorway. → Lane B |
**Synthetic-only as a code rule:** `plan_osm.js:305` is one unconditional `addDistrict('mainstreet')`
⇒ no cache town has an arcade DISTRICT, while the corpus has **1,102 arcade-KIND edges = 39 km of
ordinary suburban footpath**. Key on `district.kind`, never `edge.kind`.
### 3. THE KIT — ranked by value per draw, because the margin is eight
| # | asset | form | instances | cost | licence |
|---|---|---|---|---|---|
| 1 | `facade-shuttered` | facade skin **768×595** | 12 shops | **+0 draws, +0 tris**, 1 of 14 free atlas slots | 🟢 |
| 2 | `ground-arcade-floor` | ground skin **512²** | 1 class | **+1 draw town-wide**, **+0 tris** (the 504 m² quad *moves* class) | 🟢 |
| 3 | `ground-arcade-roof` | ground skin **512²** | 1 quad | **+1 draw in the 12 arcade chunks**, **+2 tris** | 🟢 |
| 4 | hanging blade signage | **no asset — a refusal with a better answer** | 17 | **+0 draws, +0 assets, +4 tris/shop = 68** | — |
**Item 4 is the most useful line in the table.** `buildings.js` already draws each shop's sign into
a per-chunk 512×168 canvas atlas cell and pushes ONE quad into the merged `signQuads` mesh. Push a
**second, perpendicular quad with the same `signUV.get(shop.id)` rect** and the arcade gets 17
projecting signs **carrying each shop's real name**, riding a mesh that already exists: **+0 draws,
+2 tris single-sided / +4 back-to-back = 68 triangles for all 17.** The bracket is two `boxMats`
boxes, also +0 draws. A GLB blade sign at 17 instances cannot beat that; it can only look different.
**Aspect is not decoration.** `skins.js` paints every facade into a **square** 341 px atlas slot, and
arcade lots are 3.87 × 3.0 m ⇒ **1.29:1**, against the main-street pool's 1.78:1 — a 38 % horizontal
stretch if shot to the pool. `facade-shuttered` is therefore shot at 1.29:1 and lands nearly
undistorted (1024×768 → square → 1.29:1 quad ≈ 1.03 net). Recorded; not retro-fitted to the other 48.
**`use` values that name no slot — this time on purpose.** R38 recorded `gravel`/`reddust` as
selected by nothing *by accident*. `arcade-floor` and `arcade-ceiling` are selected by nothing *with
the price printed*: the arcade cannot ride `footpath` (one town-wide merged mesh — re-using it lays
terrazzo over every footpath in town), so each needs its own class. Measured for the ceiling:
**arcade lots span exactly 2 chunks on all five seeds** (`0,0` and `0,1`, `CHUNK` 64 m).
**⚠ THE CLASSIC ARGUMENT IS INVERTED HERE relative to R38, and reusing R38's would move a golden.**
R38's ground palette is classic-safe **by absence**`character.js:83-87` returns frozen v1 literals
for a null town key and classic has none, so that path contains no `if (classic)` at all. **The
arcade district exists ONLY on the synthetic, and the synthetic IS the classic boot**, so every piece
of this kit lands on the covenanted town and needs an **explicit** flag test: `ground.js:41-47`'s
`VERGE` shape, where `?classic=1` forces it off.
### 4. `pipeline/seamless_tile.py` — and the guard it needed
A tiled floor is the one skin class where the repeat seam is a *mistake* rather than a texture:
grass hides a joint, terrazzo does not, and the arcade floor is 504 m². The tool finds the pattern's
pitch by autocorrelating the mean-absolute column/row gradient (a grout line is a gradient spike) and
crops to a whole number of periods — arithmetic instead of feathering, which would smear the grout.
**It needed a guard, because the naive version made things worse and said nothing.** Measured:
| skin | pitch found (corr) | seam before → after | kept |
|---|---|---|---|
| `ground-arcade-roof` | x 212 px (r 0.25) | **x 26.7 → 8.3** | ✅ x cropped to 848 |
| `ground-arcade-floor` | x 196 (r 0.26) / y 108 (r 0.30) | **x 27.9 → 31.9**, y 29.6 → 33.4 | ❌ rejected, passed through |
So the crop is now a **proposal that has to beat the number it is trying to improve, per axis**, and
the tool prints `NO IMPROVEMENT AVAILABLE, passed through` when it cannot. **The finding underneath
it:** a diffusion-drawn tiled floor is **not periodic enough to cut on** — the model draws tiles with
perspective and irregular spacing, so the autocorrelation peaks weakly (r ≈ 0.26) and at the wrong
lag. **Fix path for a later round, stated not done:** synthesise the terrazzo procedurally in numpy
(the `gen_audio.py` doctrine — original by construction, exactly periodic, 🟢), instead of
photographing it with a diffusion model. The shipped floor is the flux one, consistent with the ten
photographic grounds already in the pool, seam measured and recorded rather than hidden.
### 5. LICENCE — every R39 asset lands 🟢
| class | count | zone | method |
|---|---|---|---|
| skins | 3 | **🟢 original** | `flux_local` / FLUX.2-klein-4B (Apache-2.0, ungated), on-device |
| meshes | see §6 | **🟢 CC0-equivalent** (`AUDIT.md:101` doctrine) | `flux_local` concept → `bg_remove_local` (RMBG-2.0) → `trellis2_mlx` (TRELLIS.2-4B) → `normalize.py``bake_lowpoly.py` |
| tooling | 4 files | — | `dump_bird.mjs`, `bird_to_glb.py`, `arcade_measure.mjs`, `seamless_tile.py` — all ours |
**$0 spent.** No cloud key touched. The hardyards set stays REJECTED on provenance (R38), and the
3GOD depot stays a read-only archive.
### 6. THE THREE MESHES — measured, published, byte-verified
`flux_local``bg_remove_local` (RMBG-2.0) → `trellis2_mlx` (1024_cascade, metal baker, 500 k bake
cap) → `normalize.py``bake_lowpoly.py`. **3/3 first attempt, 0 retries, 0 OOM-kills.**
| id | tris | footprint (m) | height (m) | mtl/img/tex | instances the rule implies | sha1 (local == depot) |
|---|---|---|---|---|---|---|
| `aframe_board` | **366** | 0.68 × 0.64 | 0.890 | 1 / 1 / 512² | **≤16/town** (arcade ~4 + v9's unmarked cue ≤12) ⇒ ≤5,856 tris | `2aa9732d23c7…` |
| `blade_sign` | **299** | 0.66 × 0.10 | 0.549 | 1 / 1 / 512² | **17** (one per arcade shop) ⇒ 5,083 tris | `210e270c22ec…` |
| `keycutter_sign` | **498** | 0.29 × 0.06 | 0.599 | 1 / 1 / 512² | **1** | `6f22995d6402…` |
House GLB law verified on all three: metres · +Y up · base origin · **1 material / 1 image / 512²
WebP** · no Draco. Decimation floors, again confirming R38's finding that collapse cannot reach
these budgets and the bake must: 443,014 → 104,997 (normalize) → **366** (bake) · 490,612 → 94,202 →
**299** · 493,916 → 28,940 → **498**.
**`blade_sign` ships with a defect, flagged not hidden.** Its FRONT face is the blank white the
house law requires, but its BACK face carries **TRELLIS-hallucinated artwork** (a plant, a globe,
scattered shapes) invented where the single concept photograph had no data. A blade sign in a 5 m
arcade is read from both sides, so this matters — and it is one more reason the **+0-draw
perpendicular-quad answer (§3 item 4) is the recommendation and this GLB is the fallback.** Fix
path if it is ever wanted, stated not done: the R38 `tint_atlas.py` machinery already rasterizes
UV→position maps, so flat-whiting every texel with `z < 0` is the same pass that painted the magpie.
**⚠ AND THE OPERATOR'S TAIL IS NOT AN OUTLIER — it looks like memory pressure.** Wall clock:
**516.3 s · 538.7 s · 176.8 s.** R38 reported a single 489 s job against a 128 s median and called
it an unexplained tail. Here the **first two** jobs — the two that followed a `flux_local` skin
batch and a `flux_local` concept batch in the same session — took ~3× the third, which ran after
everything else had exited. Same operator, same params, same box, no error, monotonically improving.
**Hypothesis, not proof: the tail is resident MPS memory from the image model, not the mesh model.**
Worth one controlled A/B in a later round; until then, plan a mesh batch as a *cold* run.
### 7. THE TRANSMISSION PRE-PASS — three GLBs shipped as glass (R39, from Lane C's measurement)
Lane C measured that zeroing `bookshelf`'s transmission halves interior draws. The cause is not
subtle once named: **three.js runs a transmission PRE-PASS.**
> `three.module.js:16209` — `if ( transmissiveObjects.length > 0 ) renderTransmissionPass( opaqueObjects, … )`
> `three.module.js:16433` — `renderTransmissionPass` renders the **whole opaque list** into a
> `WebGLRenderTarget`. **Every opaque draw in that scene is issued twice.**
> `three.module.js:6890` — the trigger is `material.transmission > 0`, nothing else.
> `GLTFLoader.js:1202-1204` — `materialParams.transmission = extension.transmissionFactor`.
> `three.module.js:494` (`transmission_fragment`) — `totalDiffuse = mix( totalDiffuse, transmitted.rgb,
> material.transmission )`, so at **1.0 the diffuse is replaced entirely by the refracted backdrop**:
> the asset is not "slightly glassy", it *is* glass.
**Lane C's numbers:** `opshop`/hall **191 → 104 (46%)** · `opshop`/wide 142 → 80 · `book`/hall
**101 → 51 (50%)** · `dept`/hall 116 → 116 (control — no bookshelf, no change).
**I scanned all 103 GLBs** in `pipeline/_normalized`, `web/assets/models`, `web/assets` and
`pipeline/packs` for `KHR_materials_transmission` / `KHR_materials_volume`. **Three carried it —
not one:**
| file | material | state before R39 |
|---|---|---|
| `procity_fit_bookshelf_01.glb` | `mtl_10218_Bookshelves_v1` | **SHIPPED and hurting** — the one C measured |
| `procity_street_longbench_01.glb` | `lambert3SG` (+ ior 1) | **NOT WIRED** — the 5.86 m arcade/market bench the v9 charter names, and LANE_E_NOTES R38 lists as a "true one-liner" for Lane B |
| `procity_street_streetlight_01.glb` | `Streetlight_HighResSG3` (+ ior 1) | **NOT WIRED** — one per ~18 m of street, also on R38's recommend list |
**The two unwired ones are the worse finding.** They are STREET assets, both recommended to Lane B
by this lane's own notes, and the street budget is **292/300 with eight draws of margin**. A
transmission pre-pass on the street does not cost eight draws — it costs **another ~292**. They were
armed, not fired. No other GLB in the repo carries either extension.
**The fix: `pipeline/strip_transmission.py`.** Dependency-free GLB surgery — rewrite the JSON chunk,
copy the BIN chunk through untouched, and **print the sha1 of the BIN chunk before and after** so
"geometry unchanged" is a checked claim. All three: `BIN IDENTICAL`. Tris, dims, material and image
counts and max texture size are unchanged (5000 / 1172 / 4999). `KHR_materials_ior` is deliberately
**left alone** — ior does not trigger the pre-pass, and removing more than the defect widens the
diff for no measured gain.
**Appearance:** before/after through the same rig, `docs/shots/laneE/r39_transmission_fix.png`. In
Blender/EEVEE the difference is small; in three.js it is the whole diffuse term (see the shader line
above). Either way the direction is not a trade: **a bookshelf, a park bench and a streetlight are
not glass.** This was an exporter default that rode in with the source meshes and has been doubling
interior draws for however many epochs it has shipped.
**Published and verified**, under John's R22 standing authorization, through the passwordless tailnet
ingress (`GOD3_DEPOT=http://100.94.195.115:8788`; no secret read, no cookie). Then fetched back off
the **public** host and compared: **6/6 HTTP 200, byte counts exact, sha1 identical**, and the depot
copies re-scanned — **`clean, nothing to do` on all six.**
| file | HTTP | bytes (local == depot) | sha1 |
|---|---|---|---|
| `procity_fit_bookshelf_01.glb` | 200 | 356 636 | `1769af492fbf9338…` |
| `procity_street_longbench_01.glb` | 200 | 56 600 | `2813e81c6bb1feb5…` |
| `procity_street_streetlight_01.glb` | 200 | 156 328 | `9c5ec45d4d861db5…` |
| `procity_street_aframe_board_01.glb` | 200 | 88 572 | `2aa9732d23c72c2e…` |
| `procity_street_blade_sign_01.glb` | 200 | 64 196 | `210e270c22ec4dd6…` |
| `procity_street_keycutter_sign_01.glb` | 200 | 79 708 | `6f22995d6402a509…` |
`_published.json` **56 → 59**; drift check green. `build_manifest.py` re-run (furniture 13 → 16,
GLBs 36 → 39); **`validate_manifest.py`: 0 errors, 0 warnings.**
**AND IT IS NOW UNGATEABLE-PAST, which matters more than the fix.**
- `glb_stat.py` grew a **`transmission` column** and prints a loud block when any file has it.
- `validate_manifest.py` **HARD FAILS** on `transmissionFactor > 0` for every manifest GLB, with the
three.js line numbers in the message and the fix command. Nobody looked for three epochs because a
bookshelf is obviously not glass; a gate is the only thing that makes "nobody looked" impossible.
**Licence unchanged 🟢, $0.** No geometry, no texture and no third-party asset was added or altered
— one extension object was deleted from three material blocks.