John looked at R41's proof shots and named three faults. All three are fixed, at zero draw cost, and every lane corrected somebody's numbers on the way — including its own. E-LOOK (§42.1+§42.2a) — THE PREMISE WAS WRONG. "The pub props have no texture" (my brief, quoting E's own R41 note) is false: all 37 carry COLOR_0 and three.js renders it. The colour was BLEACHED — jukebox at saturation 0.02, 15 assets under 0.05; what the source library called colour was baked AO. So the fix was a palette imposed on colour already present, in place: 46 assets, draw delta +0 and tri delta +0 on every one, proved the honest way — the concatenated POSITION/NORMAL/TEXCOORD/index bytes are sha1-identical to R41, so footprints and origins provably did not move. (A first-pass bbox check ignored node rotations and "found" 3 phantom violations; thrown out for the byte comparison.) The banquette was 1990s office teal and went burgundy; the beer umbrella was white and went brewery green. THE DECK, diagnosed before being touched: 7 greys, chassis 0.05 luminance on a 0.033 bench = 1.21:1, and the one high-contrast feature was INSIDE-OUT (white ring around a dark disc; a turntable is a dark record on a light platter). Now Technics silver at 6.74:1, black vinyl, red start/stop. Node contract re-read from the shipped GLB: 4/4 + 1/1 PASS. Filed to R43, not fixed: deck_1200_rigged has NO PLINTH (bottom 43mm of 119mm lost to R41 join damage). E-CAST (§42.3) — 19 of 24 ship. Rejected: comical_luchador_01 (Mixamo Ch43, purple mask reading EL CHUPACABRA, identified from the file's own material name), a 3-heads-tall cartoon child, a robed wuxia elder, a WW2 infantryman — and dj_phrtt_01, AN UNCLOTHED BODY whose only texture is a bare-skin atlas, which was in the DEFAULT crowd and live on the depot. It is the R41 deny-list's failure class through a different door: an innocent name from another pipeline. Only rendering all 24 and LOOKING found it. Ruling 2 (prefer CC0) was REFUSED ON MEASUREMENT: Quaternius is 25 nodes / 23 joints and ZERO of 64 canonical tracks bind — it would have loaded silently and never animated. Replacements came from Mixamo's Ch## pool, filtered to the contract-clean 65-bone family (candidates ran 65..99 bones; named characters carry facial/hair bones). THE LICENCE GAP WAS UNREAD, NOT MISSING: every source FBX carries ApplicationVendor "Mixamo, Inc." internally. Adobe royalty-free, no attribution owed. B's amber rows are now real. B (§42.5) — MY ASSIGNED FIX WAS USELESS AND B PROVED IT. document.fonts.ready shipped, then measured: the atlas canvases are already byte-identical across boots, the game ships NO webfonts (document.fonts.size === 0), and ready fires 50ms BEFORE the first fillText. The real cause of unreproducible bookmarks is the wall-clock CROWD — 983 changed px of 921,600, ALL in the pedestrian band; 0 px with the crowd out. New DBG.freeze() takes stable bookmarks 2/5 -> 5/5. Also found: ?pop=0 boots 140 citizens (0 is falsy against the default). THE BENCH: D's filing correct on both counts. Front-vs-road 90.0deg -> 0.0deg, min=med=max over 60/60 instances; the streetlight arm now reaches 1.05m over the road where it reached 0.00. Both used a base yaw that pointed +Z down the street while their own comments claimed otherwise — the side term was already right, which is why it survived 41 rounds of reading. Also: on any box with CDN access 9 of 60 benches are the depot GLB, 90deg out from the house law — normalised at the geometry, once, at load. benchStops(plan) exported as one truth. C (§42.2b) — 122 vs 123 WAS THE CAMERA ASPECT, and nobody was wrong. Draw counts are post-frustum-cull and the sweep camera inherits the window aspect: 1.25->116, 1.60->122 (C's harness), 1.778->123 (F's), 2.37->124. Pinned MEASURE_ASPECT=16/9 with a 1.25 control. E's "keep the bench dark" advice was measured and declined with reason (deck-vs-bench was already 4.73:1, over threshold). "Reads as a black box" is a claim about pixels, so C measured pixels: near-black in the booth crop 57.5% -> 26.7%, distinct colours 228 -> 389. Bench rebuilt 3->7 boxes with an open record bay; headphones on the mixer; faceDoor aims booths at the player's entry — alignment within 45deg went 49% -> 92%. Two bugs found by looking: a fitting could be planted INSIDE A WALL (0.77m of a 0.86m half-depth in plaster), and every booth part sat 25mm sunk into its own worktop. D (§42.4) — five swaps IN PLACE at the same index (pickRig indexes these arrays and pedIndex is in the identity signature and the impostor atlas; removal would renumber the town). Identity signature pre- vs post-swap: 0 differing lines of 150 — who each citizen is did not move, only which body they wear. dj_phrtt_01 verified gone AT THE WIRE, not from source: a fresh default boot fetches 32 ped GLBs, none retired. THE CONTROL ARM FOUND A 41-ROUND-OLD BUG: the ped being RETIRED, comical_boy_01, binds only 3,132 of 3,460 tracks — six thumb bones lack the mixamorig prefix. It had been silently dropping six tracks per clip since it shipped. Testing the thing being thrown away found it. Imported B's benchStops mid-round: sitters now 0.0deg to the road, signatures byte-identical across the rewire. Corrected F's filing: the browse smoke was flaky-by-construction (a 16-25s race against a live sim), not deterministically red. F (§42.6) — qa.sh --strict 24 passed / 0 failed / 0 warn / 0 skipped. New Gate 11: period law (0/5 banned; CONTROL: R41's roster trips all five) and roster licence (24/24, 0 unverified; CONTROL: an amber row turns 20 red). r41_shots.py promoted warn -> strict. R41'S BYTE-HASH BAN IS REVERSED AND ITS STATED REASON WITHDRAWN AS WRONG. A gate may pin a hash under five in-run assertions (classic+flags boot, freeze().stillLive all-false, a second freeze removing 0 draws, settle until DBG.info() stops changing, hash with the dbg panel hidden): 5/5 bookmarks now pin to one sha256 over 3 fresh boots. Two findings got it there — the chunk streamer (a fixed settle is a bet on load) and the fps readout, which with everything else frozen was the last thing moving at 53-62 px. ?pop=0 fixed (Number.isFinite) — a no-op for 35 rounds. Three red flags_check gates fixed. Street: noon 282 · NIGHT 291/300 — the nine-draw margin INTACT, every R41 row reproducing to the digit. Interior at pinned 16:9: 123 @ dept/auto, margin 227. selfcheck 157,647/157,647, 0x5f76e76 unmoved. No tag — John's playtest rules the epoch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
110 KiB
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(what other lanes need from this one, newest first) andpipeline/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 42 (§42.1 + §42.2 asset half — THE LOOK) · 2026-08-07
Entry here by explicit R42 tasking (as R40 and R41 were). Cross-lane hand-off in
docs/LANES/LANE_E_NOTES.md §42. The cast half of this round (§42.3) is its own section below.
46 assets repainted, 0 draws added, 0 triangles added, 0 vertices moved. 39 pub props dressed into a 1990s Australian pub/cafe/milk-bar palette; the record-shop DJ booth diagnosed and given contrast. Published + sha1-verified; both validators 0 errors.
The finding that reframed the round: my own R41 note was wrong
R41 §41.2c says "37 of 39 have no texture at all", and the R42 tasking (reasonably) read that as no colour. Measuring the shipped files says something different and more useful:
- all 37 DO carry
COLOR_0, and three.js does render it.GLTFLoadersetsvertexColorson any primitive with aCOLOR_0attribute, andbatch.jsnever touches a GLB's interior (the loaded root is aGroup, not aMesh, andbatchRoomonly bucketsfit.childrenthat are meshes). Nothing was suppressing the colour. - The colour is simply bleached. Measured mean vertex colour:
beer_umbrellasRGB 238,238,238 ·ash_urn225,225,225 ·jukebox175,174,173 at saturation 0.02 ·old_couch176,168,155. 15 of the 37 sit below 0.05 saturation. The source library's "colour" is really baked ambient occlusion.
That changes the cheapest correct fix. It is not a missing map to be authored — it is a palette
to be imposed on colour that is already there, which is why the whole round is a rewrite of the
existing COLOR_0 buffer, byte-length for byte-length, and not one new texture. (Measure the
thing, not the note about the thing — including your own note.)
The palette, and why these colours
pipeline/r42_palette.json (every asset by name, no wildcards — R41 ruling 3). 28 named
swatches drawn from what a suburban Australian pub, cafe and milk bar actually was in 1996:
beer-stained carpet red #7a2233 · burgundy vinyl #5e1f2a · brown vinyl #6b4526 · dark stained
timber #2e1c12 · pine #b08a54 · cream laminex #dcd0ae · brass #a97f2c · chrome #c6cbd0 ·
stainless #98a0a6 · "hotel green" #2f4a3a · beer gold #c98b22 · terracotta #9c5330.
Deliberate calls: the banquette was a 1990s teal (37,94,118) that reads as office furniture,
and went burgundy; beer_umbrella was pure white and went brewery green; bin_round and
wheelie_bin went council dark-green; the pokie and planter kept their own hue because they
had one worth keeping.
The five routes — chosen by what the source colour IS, not by taste
| route | when | what it does | used on |
|---|---|---|---|
ramp |
source is bleached grey (the majority) | luminance → a 2–5 stop palette gradient, normalised between the asset's own 1st/99th luminance percentiles. The greyscale IS the baked shading, so mapping it through a dark→light ramp keeps every AO gradient and adds hue. | 34 |
tint |
source already carries meaningful hue | keep the hue relationship, force saturation, remap value | 6 |
huesplit |
one asset, two materials, separable by hue | two ramps selected by source hue — "a planter is a pot and a plant, and they are not one ramp" | 2 (planter, topiary) |
factor |
no COLOR_0 at all (a real texture) |
tint the map via baseColorFactor |
2 (keg, urinal_trough) |
lut |
source colours are a discrete 5–12 entry palette where each entry is a part | exact colour → target, ordered rules, first match wins, optional position predicates (cyl / y_min / y_max) |
2 top-level (deck_1200_rigged, deck_1200) + 5 of the 7 per-mesh sub-specs |
Why this is draw-neutral, stated as mechanism not as hope. three.js issues one draw per glTF
primitive; materials are free. The tool rewrites the COLOR_0 accessor in place — same
accessor, same component type, same element count, same byte length — and only edits the material
block for metallic/roughness/emissive, cloning a material when a per-mesh override would otherwise
leak onto a sibling primitive. Primitive counts are untouched by construction, and batch.js
cannot merge or split GLB interiors at all.
Per-asset result — 46 rows, every delta measured from the shipped file
| id | route | mean sRGB before → after | sat before → after | prims (=draws) | tris | draw Δ | tri Δ |
|---|---|---|---|---|---|---|---|
old_couch |
ramp | (176, 168, 155) → (109, 73, 40) |
0.25 → 0.86 | 1 → 1 | 5000 → 5000 | +0 | +0 |
jukebox |
ramp | (175, 174, 173) → (132, 96, 46) |
0.02 → 0.88 | 1 → 1 | 4999 → 4999 | +0 | +0 |
banquette |
ramp | (37, 94, 118) → (113, 44, 54) |
0.9 → 0.85 | 1 → 1 | 4999 → 4999 | +0 | +0 |
plywood_bar |
ramp | (162, 150, 133) → (89, 60, 31) |
0.35 → 0.87 | 1 → 1 | 5000 → 5000 | +0 | +0 |
bistro_table |
ramp | (128, 100, 61) → (80, 64, 46) |
0.79 → 0.66 | 1 → 1 | 4999 → 4999 | +0 | +0 |
pokie |
tint | (201, 179, 154) → (177, 150, 119) |
0.45 → 0.58 | 1 → 1 | 5000 → 5000 | +0 | +0 |
bain_marie |
ramp | (181, 182, 181) → (163, 165, 164) |
0.01 → 0.03 | 1 → 1 | 4958 → 4958 | +0 | +0 |
pie_warmer |
ramp | (186, 185, 186) → (151, 145, 138) |
0.0 → 0.18 | 1 → 1 | 5000 → 5000 | +0 | +0 |
keg |
factor | (textured — baseColorFactor) | — | 1 → 1 | 5000 → 5000 | +0 | +0 |
lounge_chair |
ramp | (219, 199, 165) → (128, 78, 55) |
0.47 → 0.83 | 1 → 1 | 5000 → 5000 | +0 | +0 |
ash_urn |
ramp | (225, 225, 225) → (164, 170, 175) |
0.0 → 0.13 | 1 → 1 | 5000 → 5000 | +0 | +0 |
ashtray_post |
ramp | (198, 192, 188) → (141, 146, 151) |
0.12 → 0.13 | 1 → 1 | 5000 → 5000 | +0 | +0 |
beer_umbrella |
ramp | (238, 238, 238) → (51, 100, 74) |
0.0 → 0.73 | 1 → 1 | 4995 → 4995 | +0 | +0 |
bin_round |
ramp | (190, 190, 191) → (88, 104, 92) |
0.01 → 0.3 | 1 → 1 | 4999 → 4999 | +0 | +0 |
broken_basin |
ramp | (187, 183, 175) → (190, 185, 169) |
0.14 → 0.23 | 1 → 1 | 5000 → 5000 | +0 | +0 |
cable_snake |
ramp | (88, 88, 89) → (17, 17, 19) |
0.03 → 0.16 | 1 → 1 | 5000 → 5000 | +0 | +0 |
can_stack |
ramp | (181, 174, 168) → (145, 132, 119) |
0.15 → 0.35 | 1 → 1 | 5000 → 5000 | +0 | +0 |
cdj_deck |
ramp | (56, 56, 57) → (64, 66, 68) |
0.06 → 0.12 | 1 → 1 | 4998 → 4998 | +0 | +0 |
champ_bucket |
ramp | (213, 213, 213) → (189, 194, 198) |
0.0 → 0.1 | 1 → 1 | 4999 → 4999 | +0 | +0 |
concrete_plinth |
ramp | (169, 170, 168) → (136, 132, 123) |
0.02 → 0.19 | 1 → 1 | 5000 → 5000 | +0 | +0 |
crowd_barrier |
ramp | (217, 218, 219) → (158, 165, 169) |
0.02 → 0.14 | 1 → 1 | 5000 → 5000 | +0 | +0 |
distro_box |
ramp | (155, 153, 153) → (81, 79, 80) |
0.03 → 0.05 | 1 → 1 | 5000 → 5000 | +0 | +0 |
fire_ext |
ramp | (240, 109, 99) → (88, 45, 22) |
0.86 → 0.92 | 1 → 1 | 5000 → 5000 | +0 | +0 |
firepit |
ramp | (97, 93, 92) → (68, 40, 26) |
0.12 → 0.82 | 1 → 1 | 5000 → 5000 | +0 | +0 |
flight_case |
ramp | (161, 161, 161) → (41, 43, 48) |
0.01 → 0.25 | 1 → 1 | 5000 → 5000 | +0 | +0 |
marble_sink |
ramp | (230, 228, 222) → (211, 205, 189) |
0.08 → 0.22 | 1 → 1 | 4998 → 4998 | +0 | +0 |
pallet |
ramp | (186, 166, 143) → (129, 96, 56) |
0.44 → 0.82 | 1 → 1 | 5000 → 5000 | +0 | +0 |
picnic_table |
ramp | (142, 142, 126) → (123, 91, 54) |
0.24 → 0.82 | 1 → 1 | 4999 → 4999 | +0 | +0 |
planter |
huesplit | (162, 190, 162) → (111, 128, 67) |
0.3 → 0.74 | 1 → 1 | 4897 → 4897 | +0 | +0 |
topiary |
huesplit | (162, 171, 153) → (140, 129, 71) |
0.23 → 0.76 | 1 → 1 | 4999 → 4999 | +0 | +0 |
raffle_drum |
ramp | (214, 214, 215) → (146, 110, 41) |
0.01 → 0.92 | 1 → 1 | 5000 → 5000 | +0 | +0 |
rope_post |
ramp | (230, 214, 170) → (176, 136, 64) |
0.49 → 0.88 | 1 → 1 | 5000 → 5000 | +0 | +0 |
sandwich_board |
ramp | (209, 180, 148) → (123, 92, 54) |
0.53 → 0.81 | 1 → 1 | 5000 → 5000 | +0 | +0 |
smoke_machine |
ramp | (85, 85, 85) → (23, 23, 26) |
0.01 → 0.16 | 1 → 1 | 5000 → 5000 | +0 | +0 |
speaker_stack |
ramp | (100, 100, 101) → (25, 25, 28) |
0.03 → 0.16 | 1 → 1 | 5000 → 5000 | +0 | +0 |
strobe_light |
ramp | (45, 44, 45) → (36, 36, 37) |
0.04 → 0.07 | 1 → 1 | 5000 → 5000 | +0 | +0 |
sub_bass |
ramp | (78, 78, 78) → (24, 24, 27) |
0.02 → 0.18 | 1 → 1 | 4998 → 4998 | +0 | +0 |
urinal_trough |
factor | (textured — baseColorFactor) | — | 1 → 1 | 5000 → 5000 | +0 | +0 |
wheelie_bin |
ramp | (104, 108, 111) → (57, 78, 61) |
0.12 → 0.47 | 1 → 1 | 5000 → 5000 | +0 | +0 |
deck_1200_rigged |
lut | (162, 162, 163) → (172, 162, 166) |
0.01 → 0.12 | 5 → 5 | 3821 → 3821 | +0 | +0 |
deck_1200 |
lut | (152, 151, 152) → (167, 166, 170) |
0.01 → 0.06 | 1 → 1 | 5000 → 5000 | +0 | +0 |
mixer_ttm54i_rigged |
tint | (171, 164, 158) → (183, 171, 162) |
0.16 → 0.23 | 2 → 2 | 5000 → 5000 | +0 | +0 |
mixer_pmc05pro_rigged |
tint | (173, 167, 166) → (186, 179, 176) |
0.09 → 0.12 | 2 → 2 | 5000 → 5000 | +0 | +0 |
mixer_shdj1200_rigged |
tint | (161, 151, 137) → (179, 165, 149) |
0.29 → 0.34 | 2 → 2 | 4999 → 4999 | +0 | +0 |
mixer_pmx9000 |
tint | (162, 157, 156) → (171, 166, 164) |
0.08 → 0.09 | 1 → 1 | 5000 → 5000 | +0 | +0 |
sl_deck_3 |
tint | (206, 180, 158) → (217, 182, 153) |
0.44 → 0.54 | 1 → 1 | 5000 → 5000 | +0 | +0 |
Draw delta total: 0. Triangle delta total: 0. And the stronger claim, checked rather than
asserted: for all 46, the concatenated bytes of every POSITION, NORMAL, TEXCOORD_0 and index
accessor are sha1-identical to the R41 files. Footprint, height, base origin and the manifest's
own numbers therefore cannot have moved. (My first pass measured base-origin with a hand-rolled
bbox walker that ignored node rotations and duly "found" three phantom violations on the mixers —
the byte comparison is the honest check, and it is the one reported.)
§42.2 — THE DECK: diagnosis first, then the fix
Rendered three ways with a new tool (pipeline/isolate.py — a perspective camera at 1.6 m eye
height, on a bench of a stated colour, which is the only rig that can answer "does this read from
where the player stands"). Proof: docs/shots/laneE/r42_deck_diagnosis.png, all three panels on
the R41 file, before any change.
| hypothesis | test | measurement | verdict |
|---|---|---|---|
| (a) the asset | deck alone, neutral #f2efe6 bench, room light |
7 greys, max saturation 0.02; the dominant chassis band is linear 0.05 (sRGB 62) over 2,460 of 4,622 Body vertices; and the only high-contrast feature is a white ring around a dark disc — inside-out, because a turntable reads as a dark record on a light platter |
PRIMARY CAUSE |
| (b) the composition | the real record_hero trio on Lane C's real bench colour #3a3128 (fittings.js djBooth) |
chassis rel. luminance 0.050 vs bench 0.033 = WCAG contrast 1.21:1 (1.36:1 on the #2a2a30 variant, 1.65:1 on the #1c1a18 top edge) |
CONTRIBUTING — it erases what little separation the asset had |
| (c) the lighting | (b) again at 2.2x the lamp | brighter slab, same read: a zero-saturation, zero-internal-contrast model gives light nothing to reveal | NOT THE CAUSE |
The fix (LUT, per mesh, 0 draws, 0 tris): chassis → Technics silver #b9bdc2 (rel. luminance
0.506, so 6.74:1 against the same bench — x5.6); platter surface → alloy #c9ced3; the disc
that already sat on the platter → black vinyl #17171a, so the record now reads dark-on-light
instead of the other way round; StartStopBtn and the front pad → #c0202a; the tonearm
(Headshell) → graphite #2c2e33 so it silhouettes against the now-silver top plate; PitchCap
→ #f0ece2 with a warm emissiveFactor. The static twin deck_1200 is one joined primitive, so
its record disc is found by position — measured at y = 0.079, up-facing, r < 0.158 of the
platter centre (-0.0455, 0) — not by node. The three rigged mixers and mixer_pmx9000 /
sl_deck_3 got a tint lift so the booth reads as gear rather than as a slab.
No label on the record, and the reason is topology. The platter disc is a triangle fan: 160 vertices at r = 0 and 162 at r = 0.15, nothing between. A centre colour therefore interpolates across the whole disc — the first take painted a red label and produced a red dinner plate. Flat vinyl black is the only honest colouring of that topology; a real label needs ~32 triangles of new geometry, which is filed below rather than smuggled in.
Ruling 5 honoured: no driver. Nothing was wired to Platter_SPIN, Arm_YAW, Fader_PITCH,
Btn_STARTSTOP or Fader_CROSS. They are recorded, intact, and still waiting for R43.
Node contract — re-verified from the shipped GLB, after the repaint
$ python3 pipeline/check_contract.py --list pipeline/_normalized/procity_fit_deck_1200_rigged_01.glb ...
procity_fit_deck_1200_rigged_01.glb Platter_SPIN,Arm_YAW,Fader_PITCH,Btn_STARTSTOP 4/4 PASS
procity_fit_mixer_ttm54i_rigged_01.glb Fader_CROSS 1/1 PASS
procity_fit_mixer_pmc05pro_rigged_01.glb Fader_CROSS 1/1 PASS
procity_fit_mixer_shdj1200_rigged_01.glb Fader_CROSS 1/1 PASS
procity_fit_deck_1200_01.glb - STATIC by design (R41)
A geometry fault found while diagnosing — reported, not fixed
deck_1200_rigged has no plinth. Measured: 9 of its 8,740 vertices (0.10 per cent) sit below
y = 0.05, against 822 of 11,138 (7.38 per cent) in the static twin deck_1200 built from the same
source. The bottom 43 mm of a 119 mm body is absent, and the model's base origin is held by a
nine-vertex fragment at the back rather than by its own footing — so glb.js's
position.y = at.y - bb.min.y * s plants the chassis floating ~43 mm over the bench top. It is
visible in r42_deck_before_after.png: the rigged deck (left) is a plate, the static (right) has
feet. This is R41 join/decimate damage, it is geometry rather than look, and repairing it means
re-running rig_join.py against ultra's source with a different budget — filed to R43 with the
number rather than attempted in a colour round.
Gate, publish, validate
Four-surface transmission gate, with its control demonstrated (house law: a gate that has never fired is not a gate):
$ python3 pipeline/strip_transmission.py --dry-run <46 repainted GLBs>
46/46 "clean, nothing to do" # 0 transmissive materials
$ # CONTROL ARM - inject a real KHR_materials_transmission into one repainted asset:
$ python3 pipeline/strip_transmission.py --dry-run control_couch.glb
control_couch.glb: material[0] 'DefaultMaterial' - removed KHR_materials_transmission
{'transmissionFactor': 0.85} # the gate fires
House-law sweep over all 46, measured from the served copies: max tris 5,000 (<=5k), max
texture 1024 (<=1024 WebP), Draco 0, transmission 0, _normalized/ vs
web/assets/models/ sha1 46/46 identical. 0 violations.
$ GOD3_DEPOT=http://100.94.195.115:8788 GOD3_PW=... python3 pipeline/publish.py \
--files pipeline/_r42_publish_list.txt
published 46/46 this run -> _published.json now 174 (was 174).
OK _published.json matches the live depot (all 174 recorded assets live).
$ sha1 verify (fetch each back from the depot, compare bytes)
46/46 identical, 0 mismatched 25.0 MB # -> pipeline/_r42_sha1.json
$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/validate_manifest.py
manifest v1 - facades 49, skins 92 groups, GLBs 149 (+45 non-manifest GLBs transmission-swept), audio 34
OK - 0 errors, 0 warning(s) # rc=0
$ ... validate_manifest.py --depot
OK - 0 errors, 0 warning(s) # rc=0
manifest.json is byte-identical after build_manifest.py (colour is not a manifest fact —
footprint, height, tris, draws and control nodes all unchanged). 46 thumbnails re-rendered through
thumbs.py so what Lane C sees in the thumb is what the game now draws.
Tools added / changed (all in pipeline/)
| file | what |
|---|---|
palette.py |
new — the repaint engine. ramp/tint/huesplit/lut/factor, in-place COLOR_0 rewrite, per-mesh overrides, material cloning, --report. Not idempotent by nature (ramp/tint normalise against the source's own spread), so every output is stamped asset.extras.procity_palette and a stamped input is skipped unless --force — the pristine pre-R42 GLBs live at pipeline/.props_orig/r42/ (git-ignored). |
isolate.py |
new — one staged scene, perspective camera at eye height, optional bench of a stated colour. The tool the deck diagnosis needed and contact_sheet.py could not be. Two bugs found by looking at its own output: a hand-rolled camera Euler pair that aimed at the sky (now to_track_quat), and a lamp sized for a 0.45 m turntable that burned a 5 m pub corner to white (now scaled to the set). |
montage.py |
new — labelled before/after grids from finished PNGs, with wrapped captions. A round judged by eye has to pair a render made now against one committed in a previous round, which is not a Blender job. |
contact_sheet.py |
--pitch M — the default 1 m tile pitch overlaps a 2.68 m beer umbrella onto its neighbours; the 39-prop sheet needs 2.9. Also: --title was silently invisible at any pitch > ~1.4 (it was parked at a metre offset that scales with pitch and walks off the top of the frame). The caption is now solved for in screen units against the row-0 ground line — the first 39-prop sheet rendered with no title at all and nothing said so. |
r42_palette.json |
the by-name spec: 28 swatches + 46 assets, each with its route, its stops/rules and a one-line reason. |
_r42_publish_list.txt, _r42_sha1.json |
the explicit publish set and the fetch-back digests. |
Proof shots (docs/shots/laneE/)
r42_pub_contact_39.png (all 39 dressed, one sheet) · r42_pub_ab_top5.png (the top five by
screen area, same scene/lights/camera, R41 vs R42) · r42_deck_diagnosis.png (the three-hypothesis
diagnosis, all three panels on the R41 file) · r42_deck_isolated_ab.png (the deck alone,
before/after) · r42_deck_before_after.png (the record_hero booth on Lane C's real bench colour).
For Lane C (§42.2 composition half)
- Nothing you place changed shape. Same ids, same
footprint/height, same draws, samecontrol_nodes.manifest.jsonis byte-identical — a re-fetch of the depot is all that is needed. - The booth bench is now the darkest thing in that composition. At
#3a3128the silver chassis reads at 6.74:1;#1c1a18gives 9.19:1 — but a lighter bench would reduce it. cdj_deck,sl_deck_3andmixer_pmx9000(thevenuebooth) got the same lift, so that booth reads too.- The pub palette assumes a dark, warm floor. The shots use
#5e2c26; the props were tuned to sit on carpet red, not on pale boards.
Filed to R43
deck_1200_riggedhas no plinth (9 of 8,740 verts below y = 0.05; the bottom 43 mm of a 119 mm body). Needs arig_join.pyre-run against ultra's source, not a repaint.- A record label needs ~32 triangles. The platter disc is a fan; colour cannot make a label.
Appending a small label disc to the
Platterprimitive is +32 tris, 0 draws, and would make the deck unmistakable at any distance. - The DJ control-node driver (already filed by the integrator) — the nodes are intact and verified again this round.
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=1gate — the synthetic IS the classic boot) to B inLANE_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_localconcept →trellis_mac→normalize.py, viagen_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.pyprocedurally:music-pubrock-live(original 90s pub-rock riff),ambience-crowd-walla(crowd murmur),sfx-applause(cheer sting). Allgig:truein 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.pyreads_gig_results.json;validate_manifest.pycovers audio. Provenance inAUDIT.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 thenvalidate --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 carrytypes[]for seed-pick per shop (like facades); footstep is{surface:[variants]}.build_manifest.pybuilds it,validate_manifest.pychecks 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 theroom.audiocontract, 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 --depot0 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.pynow 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 topipeline/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 intovalidate_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--strictGREEN 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.productsby 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 topipeline/packs/+ staged, all pass the pack-QA gate.build_stock_pack.pygained--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, reflane-e/round7-peds). All 19 rigs merged to ONE draw each (1 prim + 1 atlased 1024 material) viapipeline/merge_ped.py(Blender join → UV-pack → EMIT-bake all materials into one atlas). Skinning + 65-joint skeleton preserved,walk/idleuntouched. 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--strictGREEN 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 toweb/assets/models/so Lane C wires?stock=realagainst 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.pynow merges into_published.json(read existing → union → sorted write), never replaces — fixes the R5--onlyclobber (record dropped 27→1). Proven: a real no-op re-publish ofrecord_cratekept the record at 27 (was 27), not 1.- Post-publish + a new
--verifymode drift-check the record against the live depot (/api/listfiltered toprocity_*) and warn on either direction.--verifyconfirms 27 == 27. validate_manifest.pygains a provenance-drift gate: every manifestprocity_*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 --strictGREEN (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 --depot0 err. Originals gitignored inpipeline/.props_orig/. → Lane C re-measures the record interior (was ~273k).
- verified loading in the vendored GLTFLoader. Re-published;
- 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 --depot0 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 streetbus_shelter. Manifest now 17 fittings + 9 furniture (26 GLBs). Contact sheetdocs/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 --depot0 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, reflane-e/round4-peds). The 19 ped rigs inweb/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) viagltf-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/idleuntouched); contact sheetdocs/shots/laneE/peds_decimated.pngclean at street distance. Originals gitignored inpipeline/.peds_orig/; per-ped inpipeline/_peds_decim.json. → Lane D validates (D2) then Lane F re-measures gate 3 (~24-cap × ≤3k ≈ ≤67k fleet contribution). - E2a —
record_crateFIXED, verified + published. Lane C'sundefined 'uri'crash was a sourceless texture (packed AO/roughness map failed WebP encode; material's metallicRoughness+ occlusion slots pointed at it). New toolpipeline/fix_glb_textures.pystrips those slots (three.js loads textures lazily → no crash). Verified it loads in the real vendoredGLTFLoader(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_tillfitting (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_localconcept →trellis_mac, 3 concept rolls to land the ~2:1 aspect), normalized to house law, verified loads inGLTFLoader, and published. Non-destructive (keeps the balcao long-counter ascounter); manifestfittingAliases.counter→counter_till; Lane C re-maps kindcounter→counter_tillinglb.js. - Publish: DONE (John approved). Both re-exports live; manifest
validate --depotGREEN (0 err, 23 GLBs live).publish.pygained 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_localconcept image →trellis_macimage→GLB (1024_cascade) →normalize.py→ thumbs → manifest. 5 generated + kept (listening_booth, drinks_fridge, arcade_cabinet, milkshake_mixer, novelty_record — contact sheetdocs/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): resolvesdepot:<file>toweb/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 adepot:GLB, CDN path intact). - Manifest: 14 fittings + 8 furniture (5 hero + cash_register added);
validate_manifest.pygreen (0 err). Provenance recorded inAUDIT.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 --strictGREEN (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 (onlyrecord_crate_plastic_01was 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, extendsMESHGOD/finish_glb.py(solidify/decimate/ bright/base-origin) + adds height-normalize, outlier cleanup, texture→WebP≤1024, per-asset up/yaw, 256px thumbnail. Driven bypipeline/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 inweb/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 (cookiehmac(GOD3_PW,"3god-ok"),POST /api/upload+/api/thumb). Reads the secret from env — never contains/logs it.--dry-runverified all 16 ready. Not published yet:GOD3_PWisn'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.conventionsblock 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;--depotenforces 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 fromthriftgod/gen_assets.py— same STYLE bible + blank- signboard template so new skins match. Resumable.genraw/,--dry-run/--only/--harvest.--localbackend (the one I used): MODELBEASTflux_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/--paramsJSON) — 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-sidewalls (3). - Coverage after mapping (
build_manifest.pyFACADE_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.stallwent 2→4, retiring the Lane A single-facade gap. - Corner-lot
-sideskins now exist (brick/render/weatherboard): taggedface:"side"with emptytypesin the manifest so no shop-front pool ever picks a windowless wall; Lane B maps them to the exposed side face (conventions.corner_lots+ newconventions.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.pyjoined meshes but never flattened parent node hierarchies, so assets under a parent empty shipped off-origin:park_benchwas ~1 km off-origin, floating 30 m;food_cart15 m off;clothes_racksank 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 nowcenter=[0, h/2, 0], minY=0.0. - Major — tri budget counted faces, not triangles (latent for quad FBX/OBJ). Fixed: triangulate
in
clean_meshso the ≤5k gate + reported counts are true triangles. - Major — fitting-id namespace. Added
fittingAliases(registry semantic id → physical id) to the manifest. (Lane C'sglb.jsalready 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'sstallpool 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
yawavailable). 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.pyliveness 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.mdcomplete. - ✅ ≥15 normalized GLBs (16) with thumbnails — ⛔ live on 3GOD pending publish (needs cred).
- ✅
manifest.jsonvalidates 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 indocs/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>viacore/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". Widedept-*anchors (when generated) want a wider UV plane.awning/ground/skyskins are yours;wall/interiorare Lane C's. - Lane F: wire
python3 pipeline/validate_manifest.pyinto the gate (add--depotpost-publish).
Open questions for Fable/John
- 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 + needsGOD3_PW, so I left it for your go rather than assume it. 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 viasf3d/trellis_mac, but those weights are HF-gated — a setup step before that path is free too.)- 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.
{
"_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.
# 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.
Round 42 (§42.3) — RECAST THE TOWN (Lane E, cast half)
The brief was "audit the shipped roster, replace what does not belong, fill the credits". The audit found five rejects, not the one the shot showed, and the licence question — the one B filed as unresolvable from this repo — turned out to be answerable from the asset files themselves, which changed the whole shape of the round: nothing had to be replaced for licence, only for period.
§42.3-A — THE LICENCE, CLOSED BY MEASUREMENT (B's §41 finding, resolved)
B shipped ped-bodies-base as amber unverified and was right to: nothing in this repo recorded
where the 19 bodies came from. But the answer was inside the source FBX, not in a document.
Every ped's source file on ultra carries its own provenance in the FBX header:
Original|ApplicationVendor KString "Mixamo, Inc."
Original|ApplicationName KString "mixamo.com"
DocumentUrl KString "working/exporter_<id>_<hash>.tmp" ← Mixamo's server-side exporter
Checked on all 24 character sources: 24/24 carry it. That is machine-readable, per-asset, and it is in the file rather than in a note about the file.
The second half of the identification came from the GLBs already in the tree. merge_ped.py
renames a merged ped's material to ped_merged, but the four peds that were never merged kept
their original material names — Ch43_Body, Ch09_body, Ch18_Body, Ch39_Body — which are
Mixamo's numbered-character ids. Reading the same names out of the source FBX recovers the id for
the other 15. So every ped is now identified to a specific Mixamo character, not to a vendor.
Zone check: CITY_SPEC's NPC contract declares web-ok only (Mixamo, CC0/CC-BY, CGTrader RF).
The roster is entirely the first of those three. B's specific worry — "if any one of these 19
is CC-BY the credit is owed today and unrecorded" — is answered: none is CC-BY, no attribution
is owed, and required: false is correct for the same reason it already is on the Mixamo clip
row. The gap was a record-keeping gap, never a compliance one.
§42.3-B — THE AUDIT (24 shipped bodies, every one rendered and looked at)
Rendered with a new tool, pipeline/ped_sheet.py, because contact_sheet.py could not do it:
the ped GLBs are centimetre-unit (bbox ~177 "m") and that tool derives its frame height from the
pre-scale bbox, so it emitted a 2 100 × 50 734 px strip. The cast tool normalizes every rig to the
same standing height the way buildFigure() does and shoots at eye level — a casting sheet, not a
top-down. Sheets: docs/shots/laneE/r42_roster_contact.jpg, r42_roster_rejects.jpg.
| file | Mixamo | what it depicts | period verdict | licence | evidence |
|---|---|---|---|---|---|
| man_worker_hivis_01 | Ch17 | hard hat, orange hi-vis vest, dirty work trousers, boots | SHIP — the Australian tradie, the single most right model in the roster | Mixamo RF | FBX vendor + Ch17_Body |
| man_suit_01 | Ch33 | navy suit, tie, dress shoes | SHIP | Mixamo RF | FBX vendor + Ch33_Suit |
| man_casual_01 | Ch01 | white polo, blue jeans, sneakers | SHIP | Mixamo RF | FBX vendor + Ch01_Shirt |
| man_casual_02 | Ch12 | pale blue short-sleeve shirt, dark trousers | SHIP | Mixamo RF | FBX vendor + Ch12_body |
| man_casual_03 | Ch28 | black long-sleeve, dark trousers, sneakers | SHIP | Mixamo RF | FBX vendor + Ch28_Body |
| man_grunge_01 | Ch42 | red graphic tee, denim shorts, tattoos | SHIP — reads 1996 harder than anything else here | Mixamo RF | FBX vendor + Ch42_Body |
| man_athletic_01 | Ch08 | grey trackie top and pants | SHIP | Mixamo RF | FBX vendor + Ch08_Body |
| man_bouncer_01 | Ch18 | black security uniform, vest, epaulettes | SHIP — a pub with a band has a bloke on the door | Mixamo RF | shipped GLB material Ch18_Body |
| man_dj_streetwear_01 | Ch06 | black hoodie, headphones, joggers, red/white sneakers | SHIP, marginal — silhouette reads 2010s, but it is the record-shop archetype and no better one exists in the pool | Mixamo RF | FBX vendor + Ch06_body |
| woman_casual_01 | Ch02 | orange sweatshirt (cat motif), denim shorts, yellow trainers | SHIP, marginal — the chunky trainers date it late | Mixamo RF | FBX vendor + Ch02_Body |
| woman_casual_02 | Ch26 | green/white striped top, jeans, red heels | SHIP | Mixamo RF | FBX vendor + Ch26_Body |
| woman_business_01 | Ch07 | navy suit, heels | SHIP | Mixamo RF | FBX vendor + Ch07_Body |
| woman_athleisure_01 | Ch37 | pink zip jacket, black leggings, trainers | SHIP, marginal — technical-fabric leggings read 2010s | Mixamo RF | FBX vendor + Ch37_Shirt |
| woman_smart_01 | Ch27 | blazer, trousers, heels | SHIP | Mixamo RF | FBX vendor + Ch27_Suit |
| woman_chef_01 | Ch13 | white double-breasted chef jacket, dark trousers | SHIP — the milkbar/bakery counter | Mixamo RF | FBX vendor + Ch13_Body |
| man_elder_01 | Ch39 | robed fantasy elder: purple-and-gold robe with wide sleeves, ornate belt boss, sash, cloth leg wraps, long white beard | REJECT — ruling 1, fantasy. It is not a retiree; it is a wuxia/wizard elder | Mixamo RF | shipped GLB material Ch39_Body |
| man_soldier_ww2_01 | Ch49 | WW2 infantryman: olive drab uniform, steel helmet, webbing, ammunition pouches, tall jackboots | REJECT — ruling 1. Visible on the street in the R41 posture shot | Mixamo RF | FBX vendor + Ch49_body |
| comical_boy_01 | Ch09 | cartoon child, ~3 heads tall, red helmet, oversized boots | REJECT — ruling 1, cartoon; also breaks the human-sized line on humanoid shots | Mixamo RF | shipped GLB material Ch09_body |
| comical_luchador_01 | Ch43 | PURPLE MASKED LUCHADOR. Purple singlet, chest reads EL CHUPACABRA, orange arms, purple mask/gloves/boots, barrel torso |
REJECT — this is the model in the R41 pub shot. Ruling 1 names it explicitly | Mixamo RF | shipped GLB material Ch43_Body |
| woman_dj_01 | — (TRELLIS) | blue hair, olive utility vest, cargo pants | SHIP — venue-gated, reads as a 90s raver | on-device, CC0-equiv | credits ped-bodies-dj |
| woman_dj_02 | — (TRELLIS) | green hair, black jacket, crop top, neon boots | SHIP — venue-gated | on-device, CC0-equiv | credits ped-bodies-dj |
| woman_dj_03 | — (TRELLIS) | hooded jacket, crop top, cargo pants | SHIP — venue-gated | on-device, CC0-equiv | credits ped-bodies-dj |
| dj_techno_01 | — (TRELLIS) | dark hooded jacket, cargo pants, boots | SHIP — venue-gated | on-device, CC0-equiv | credits ped-bodies-dj |
| dj_phrtt_01 | — (TRELLIS) | an unclothed body. No garment exists on the asset | REJECT — see §42.3-C | on-device, CC0-equiv | its own 1024² texture |
19 ship (3 of them marginal) · 5 reject. Rejects: 1 luchador, 1 cartoon child, 1 fantasy elder, 1 WW2 soldier, 1 nude.
§42.3-C — THE ONE THE BRIEF DID NOT ASK ABOUT: dj_phrtt_01
Rendering the roster turned up an asset nobody had looked at since R36. dj_phrtt_01.glb renders
as a nude female figure, and it is not a texture-load failure — I extracted its single embedded
image and looked at it. The GLB has one 1024² WebP and that atlas is bare skin end to end.
There is no clothing on the asset to fail to load.
Three things make it worse than a period miss:
- It is in the default crowd.
loadPedFleet'sopts.djsdefaults todance, and the shell passesdance: !CLASSIC— so every boot that is not?classic=1loads it andpickRigcan place it in a venue. - It is on the shared depot (
pipeline/_normalized/dj_phrtt_01.glb, published, in_published.json), so it is not only in this repo. - It is the R41 deny-list's failure class arriving through a different door. That list bans
character_kit_modular/exports/bodies/**and its 30-nsfwbodies by name. This one was never on the list because it came from a different pipeline (on-device TRELLIS, R36), and a name deny-list cannot catch an asset whose name is innocent. The control the deny-list needs is a look, and this round is the first time anyone looked.
Replacement staged (man_smart_01, below). I did not delete it — see §42.3-F.
§42.3-D — RULING 2 vs THE CONTRACT: Quaternius measured, and it cannot ship
Ruling 2 says prefer CC0, and names Quaternius. I measured it before using it, and it fails the ped contract outright:
| shipped ped | Quaternius Casual_Male.gltf |
|
|---|---|---|
| nodes | 67 | 25 |
| skin joints | 65 | 23 |
| bone naming | mixamorig12:LeftForeArm |
LowerArm.L, Torso, Abdomen, Fist.L |
rigs.js _action() filters an incoming clip's tracks to bones that exist on the target and builds
a clip from what survives. Against a Quaternius rig zero of the 64 canonical tracks survive —
so the model would load, report no error, and stand in bind pose forever. That is precisely the
silent break the brief warned about, and it would take D's 46 clips and the impostor bake with it.
Making it work means a retarget, which is a new system and ruling 5 forbids one this round.
So the CC0 route is refused on measurement, and the replacements come from the other pool the
ruling names — ultra's Mixamo people-normal FBX. This costs nothing on the licence side, because
§42.3-A established that pool's licence is the roster's licence: same vendor, same regime,
already credited, required: false. CC0's advantage was "fixes the look and the licence in one
move"; the licence half of that was already fixed by then.
A second contract filter did real work inside the Mixamo pool. Converting 20 candidates showed
bone counts of 65, 67, 68, 69, 83, 86, 87, 88 and 99 — the named Mixamo characters (Aj,
Remy, The Boss, Sporty Granny, claire, kaya, …) carry extra facial/hair/cloth bones and
would break the 65-joint contract exactly like Quaternius does, only less obviously. Only the
65-bone Ch## family is contract-clean, which is the same family the shipped roster came from.
That cut 20 candidates to 9 before a single aesthetic judgement was made.
§42.3-E — THE REPLACEMENTS (5 for 5, roster size unchanged)
Of the 9 contract-clean candidates, 4 failed ruling 1 on sight and are recorded as rejected
sources: woman_raver_01 (Ch03 — cyber visor, reflective-banded rave pants), comical_girl_skater_01
(Ch29 — magenta helmet, stylised), man_jumpsuit_01 (Ch32 — orange coverall and an untextured
faceless head), man_footballer_01 (Ch38 — full football kit with a club crest, which is the
quarantine rule's own trigger). The remaining 5 are exactly the 5 slots to fill.
| slot (pool) | out | in | Mixamo | tris | nodes/joints | boneset | clip bind | sha1 |
|---|---|---|---|---|---|---|---|---|
| normal | man_elder_01 | man_casual_04 | Ch31 | 2 533 | 67 / 65 | MATCH | 3456/3460 | d855c4cf… |
| normal | man_soldier_ww2_01 | person_casual_01 | Ch21 | 2 956 | 67 / 65 | MATCH | 3456/3460 | 2c3b7ff1… |
| comical | comical_boy_01 | person_youth_01 | Ch22 | 2 738 | 67 / 65 | MATCH | 3456/3460 | 94c936c1… |
| comical | comical_luchador_01 | woman_smart_02 | Ch41 | 2 494 | 67 / 65 | MATCH | 3456/3460 | 0c3ddf21… |
| djs | dj_phrtt_01 | man_smart_01 | Ch23 | 2 617 | 67 / 65 | MATCH | 3456/3460 | 68aae4db… |
Depicting, in order: a bloke in a navy jumper · a bloke in a teal shirt and tan chinos · a teenager in a white tee and jeans · a woman in a white cardigan and black trousers · a bloke in a waistcoat over a blue shirt and jeans. With the 19 kept, the roster reads tradie / suit / polo-and-jeans / grunge kid / teenager / chef / office workers / shop staff.
Pipeline, now scripted end to end. The FBX→GLB front end had never existed in this repo (it
lived in 90sDJsim), so recasting had no reproducible first stage. New pipeline/ped_from_fbx.py
is it; stages 2 and 3 are the unchanged R4/R7 recipes:
ped_from_fbx.py # Blender: FBX -> GLB, T-pose action stripped
gltf-transform weld -> simplify --ratio 0.05 --error 0.12 # 50–59k -> 2.5–3.0k tris
merge_ped.py --atlas 1024 # -> 1 primitive / 1 material / 1 WebP atlas
Contract verification — measured, not assumed. Every number below is against the reference ped
man_casual_01.glb:
- 67 nodes / 65 joints / 1 primitive / 1 material / 1 image / 1024² WebP / no Draco — all 5.
- Bone-name set identical to the reference, all 65, after
_canon(mixamorig\d+→mixamorig). - Clip binding, the test that matters: I replayed
rigs.js's own filter (canonicalise, keep.quaternion, dropHips.quaternion) over 54 clips — the 8 base clips plus all 46 of D's R41 library — against each replacement. Result 3 456 / 3 460 bindable tracks (99.9 %), worst single clip 64 bound — byte-identical to what the shipped reference ped scores. The 4 unbound are the knownmixamorigReferencenode (AUDIT R36 w1), not a new loss. - Facing and handedness, the R13 trap: toe-minus-heel ΔZ / span is +0.100 … +0.136 on all
five, against +0.130 … +0.136 on the shipped peds — front is local +Z, which is what
buildFigure'sinner.rotation.y = πcompensates for. A flipped model would have walked backwards and no gate would have said so. - Tri band 2 494–2 956, inside the shipped 2 067–4 000 band and on the shipped mean (~2 550).
- Scale shape: the raw conversion lands metre-geometry under a 0.01 armature root (the DJ
five's shape).
ped_from_fbx.pynow applies that scale so the output matches the base 17 it is joining rather than being the one member with a 10⁴ vertex-magnitude difference feeding the same impostor bake. Verified the fix changed nothing that matters: bone extents identical to 4 dp before and after (y = 1.7611, head bone1.7761).
Draw cost: zero, structurally. One primitive, one material each — the same 1 draw/ped the R7 merge bought. Five in, five out: draw-neutral by construction (ruling 4).
§42.3-F — WHAT I DID NOT DO, AND WHY
The five rejects are still in the tree and still in PED_NAMES. Deleting them was within my
editable set and I chose not to: rigs.js loads them by literal name, so removing the files
shortens the fleet, renumbers every pickRig index, changes crowd identity and moves Lane F's
goldens — during a wave where §42.4 (the wiring) has not run. The swap is D's, in one commit,
with the determinism gate watching. Nothing is wired; the five replacements are additive.
Lane D — the swap map is the table in §42.3-E. Both replacement files for the comical pool
are ordinary people, so that pool stops being a curveball pool; that is ruling 1 winning over
flavour and is deliberate. Keep filling slots by index, never by load order (the R2 bug).
Lane F — your §42.6 period-law name gate has its list. Red on any of:
comical_luchador_01, comical_boy_01, man_elder_01, man_soldier_ww2_01, dj_phrtt_01.
Control demonstrated: the same gate must go green on the five replacements above, all of which
are present in the tree now.
A gap in the transmission sweep, for F. validate_manifest.py sweeps pipeline/_normalized/
and web/assets/models/ for non-manifest GLBs — but not web/models/peds/ or
web/models/clips/, which rigs.js and clipbank.js load from by literal path. It happens to be
covered today only because staged copies exist in _normalized/. I swept all four surfaces by
hand this round (below); worth making the loop's dir list match the game's load paths.
§42.3-G — GATES
four-surface transmission sweep pipeline/_normalized 174 · web/assets/models 169 ·
web/models/peds 37 · web/models/clips 6
= 386 GLB, transmissive materials: 0
depot copies of the 5 new peds materials=1, transmissive=0, ext=[EXT_texture_webp] (fetched back)
publish.py --files 5/5 OK glb:200 thumb:200 → _published.json 169 → 174
drift check: all 174 recorded assets live
sha1 fetch-back (depot /a/<f>) 5/5 MATCH
man_casual_04 d855c4cfd032f72f7576632b33d194d7cb0975f9 283 204 B
man_smart_01 68aae4db344864f7e90e1b7d2d4820dbec15fa50 262 168 B
person_casual_01 2c3b7ff181c84116fb4f31eca78a9b26f8711caf 454 980 B
person_youth_01 94c936c1e20f9742cd3cfb3ec913e60601e4c3f5 424 724 B
woman_smart_02 0c3ddf21e7f64ed1551535c64899becc9d4261b3 262 184 B
validate_manifest.py OK — 0 errors, 0 warnings
validate_manifest.py --depot OK — 0 errors, 0 warnings
credits.json parses, 15 entries, 0 missing required keys,
ped roster no longer "unverified"
Files I touched, and only these: web/models/peds/{man_casual_04,man_smart_01,person_casual_01,person_youth_01,woman_smart_02}.glb
(new) · the same five in pipeline/_normalized/ (publish staging) · web/assets/thumbs/ (same
five PNGs) · pipeline/{ped_from_fbx.py,ped_sheet.py} (new) · pipeline/_published.json (by
publish.py) · web/assets/credits.json (rows only — schema untouched, credits.js untouched) ·
docs/shots/laneE/{r42_roster_contact.jpg,r42_roster_rejects.jpg} (new) · E-progress.md ·
docs/LANES/LANE_E_NOTES.md. No git command was run. I did not touch web/assets/manifest.json,
web/assets/models/**, web/js/**, tools/qa.sh, merge_prims.py, rig_join.py or
check_contract.py.