MOTION (§41.1). The clip library goes 8 -> 46: ten idles, eight browse, eight sit/lean, eight social, six locomotion, six venue, in SIX grouped GLBs (one fetch each), 3.35 MB — LESS than the 4.29 MB the old eight cost, via lossless dedup + int16 rotations (worst error 0.0034 deg). All six verified skeleton-only (tris 0, meshes 0, nodes 66): ZERO DRAW, which is what makes this round affordable. No retarget was run and none was wanted — the bank and the peds are the same mixamorig skeleton, so retargeting would add error AND bake the ped mesh into the clip, ending zero-draw. MIRPAMO 'make smoke' green to prove the tool, then deliberately unused. Three seeds in the brief were duds and got substituted: the whole *_Degree_Turn set is RIFLE-AIMING, and two 'examine' clips are 262 KB static poses, not motion. R16 flat-body trap re-checked on all 46 (spine tilt 24 samples/clip, 0 frames >75 deg); turn_in_place auto-demoted from loopable on a 36.7 deg seam. RULING 1 — the Bandai hazard, closed non-destructively and better than specified. 3,077 CC BY-NC clips sat unzoned in a neutrally-named path. Renamed PER FILE (3,077/3,077) not just the directory, because mirpamo names output <rig>@<clip>.glb — the _NC-research marker now PROPAGATES into any retargeted GLB automatically. Nothing deleted; ultra's red/bandai re-verified as canonical. Ledger corrected: the manifest claimed CC-BY-NC-ND, the bundled licence text says CC BY-NC, no ND. PROPS (§41.2). 110 assets from three libraries, published, sha1-verified, 0 validator errors both modes. THE FINDING: every handover number was a TRIANGLE count, and triangles were never the binding constraint — DRAW CALLS were. As handed over this cargo cost 14,140 draws against a 162-draw margin; it ships at 117. '3dstore passes as-is' was true on tris/metres/ Draco/textures and FALSE on draws — 30 of 46 files carried up to 16 materials on one mesh (one per record sleeve), so a 1,020-tri tub cost 16 draws; baked to COLOR_0, 288 -> 40, zero tri drift, A/B identical. dj-gear's own manifest claimed median 6,288 tris / 18 under budget; measured 51,528 and 12, with draws to 2,145. Pub props were NOT metre-correct (every source unit-normalised to max dim 1.00 m) and 40x decimation was impossible as specified (loungeChair is 94% non-manifold). Ruling 5 vs the draw budget was a real conflict (one mixer = 4.6x a whole room); resolved by joining SCENERY selectively while every control node keeps its object/name/pivot — verified by parsing the SHIPPED GLB, not the tool that wrote it: PASS 4 / STATIC 10 / N/A 17 / FAIL 0. deck_1200_rigged = 5 draws with Platter_SPIN, Arm_YAW, Fader_PITCH, Btn_STARTSTOP intact. The R40 four-surface transmission gate FIRED ON LIVE CARGO: 5 genuinely transmissive materials (cartridge dust-covers, a mixer meter window) that would each have doubled every opaque draw in the room. 9 assets rejected on eyeball after decimation rather than shipping mush. Two existing-tooling bugs fixed: normalize.py's yaw/up were silent no-ops (the jukebox exported 0.40 m instead of 0.97 m), and footprints now measure the shipped GLB. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
72 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 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.