Lane E R41 (§41.1+§41.2): 46 clips and 110 fittings — and every handover number was the wrong unit
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>
582
E-progress.md
@ -7,9 +7,302 @@
|
||||
> lanes need from this one, newest first) and **[`pipeline/AUDIT.md`](pipeline/AUDIT.md)**
|
||||
> (provenance, licence zone, measurements and method, per round). Recorded in R39 rather than
|
||||
> quietly resurrected: a progress file whose last entry is 27 rounds old is a trap for the next
|
||||
> reader, and the honest fix is a signpost, not a backfill. *(One exception below: the R40 tasking
|
||||
> explicitly asked for its control-arm transcripts in THIS file, so §40 lives here AND is
|
||||
> cross-referenced from the notes.)*
|
||||
> 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)*
|
||||
|
||||
@ -432,3 +725,286 @@ All fixed and re-verified:
|
||||
step before that path is free too.)*
|
||||
3. A few library assets were near-misses (garage_shelves, radiogram, market_stall — see AUDIT
|
||||
rejects). Worth a manual Blender re-origin pass later, or leave to primitives? I left them out.
|
||||
|
||||
---
|
||||
|
||||
# Round 41 (§41.1) — THE MOTION LIBRARY *(Lane E, motion half)*
|
||||
|
||||
**Done, green, and it is the round's critical path — Lane D can start.** 46 clips, 6 grouped GLBs,
|
||||
**3 498 124 B (3.34 MB)**, zero-draw, plus a new `web/assets/motion_manifest.json`.
|
||||
Full provenance + the licence lines are in `pipeline/AUDIT.md` under "Round 41 §41.1".
|
||||
|
||||
## 0. Ruling 1 first — the Bandai hazard is closed (paths, exactly)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **was** | `~/Documents/MIRPAMO/library/downloads/bandai_namco_1/` — 3,077 **CC BY-NC 4.0** BVH, 725 MB, unzoned, neutrally named, **nothing in any filename marking them**. One `mirpamo batch` from a shipped build. |
|
||||
| **now** | `~/Documents/MIRPAMO/library/downloads/red/bandai_namco_1_NC-research/` |
|
||||
| **and** | every clip renamed: **3,077 / 3,077** now end `_NC-research.bvh`; `find … ! -name '*_NC-research.bvh'` returns **0** |
|
||||
| **new** | `~/Documents/MIRPAMO/library/downloads/red/README.md` — the zone law, mirrored from ultra's `MOTIONLIB/red/README.md` |
|
||||
| **fixed** | `~/Documents/MIRPAMO/library/manifest.json`: `CC-BY-NC-ND 4.0` → **`CC BY-NC 4.0`** (+ `zone:"RED"`, `verified:true`, the renamed path as `name`, and the correction recorded in `notes`) |
|
||||
|
||||
**NOTHING WAS DELETED.** 552 MB of clip data is still on disk byte-for-byte; the instruction
|
||||
preferred non-destructive and ultra holds the canonical zoned copy anyway
|
||||
(`~/Documents/MOTIONLIB/red/bandai/`, re-verified live: **3,077 BVH / 552 MB**).
|
||||
|
||||
Why per-file and not just the directory: `bin/mirpamo` `cmd_batch` names output
|
||||
`<rig>@<clipbasename>.glb`, so **the marker now propagates into any retargeted GLB automatically**.
|
||||
`downloads/` is gitignored in MIRPAMO, so `red/` inherits that.
|
||||
|
||||
The ledger fix is real, not cosmetic: the bundled `dataset-1/LICENSE` and `dataset-2/LICENSE` both
|
||||
open *"Attribution-NonCommercial 4.0 International"* — **no ND clause**, as ultra's `SOURCES.md`
|
||||
already said. Also recorded while I was in there: `downloads/lafan1_ubisoft/` (the other
|
||||
non-commercial source) is **scaffold only on this box** — 332 KB of 134-byte git-lfs pointers,
|
||||
**0 BVH**, so it cannot leak; the real payload is ultra's `red/lafan1/`.
|
||||
|
||||
## 1. `mirpamo make smoke` — the tool ground truth, run BEFORE the batch
|
||||
|
||||
**PASSED**, Blender 5.1.2, exit 0 — procedural mesh → autorig → retarget → verify, `[mirpamo] verify OK`.
|
||||
|
||||
## 2. What ships
|
||||
|
||||
`web/models/clips/` (new dir), one fetch per group — ruling 6 is **6 files, not 46**:
|
||||
|
||||
| group file | category | clips | bytes | motion |
|
||||
|---|---|---:|---:|---:|
|
||||
| `idles.glb` | idle | 10 | 1 029 496 | 78.3 s |
|
||||
| `social.glb` | social | 8 | 895 648 | 73.9 s |
|
||||
| `venue.glb` | venue | 6 | 526 036 | 39.5 s |
|
||||
| `browse.glb` | browse | 8 | 476 056 | 30.8 s |
|
||||
| `sitlean.glb` | sitlean | 8 | 358 112 | 27.5 s |
|
||||
| `locomotion.glb` | locomotion | 6 | 212 776 | 12.1 s |
|
||||
| **TOTAL ADDED** | | **46** | **3 498 124 B = 3.34 MB** | **262 s** |
|
||||
|
||||
`web/assets/motion_manifest.json` — **16 KB** (new file; I did not touch `manifest.json`, which is
|
||||
the props half's this round). **Total added bytes: 3 514 636 (3.35 MB).**
|
||||
|
||||
The 8 clips PROCITY already ships cost **4.30 MB**. This is **5.75× the clips for 78% of that**.
|
||||
|
||||
## 3. ZERO-DRAW — proved with `glb_stat.py`, not asserted
|
||||
|
||||
```
|
||||
browse.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
|
||||
idles.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
|
||||
locomotion.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
|
||||
sitlean.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
|
||||
social.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
|
||||
venue.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0]
|
||||
```
|
||||
|
||||
66 nodes = 65 mixamorig joints + `Armature`, i.e. the **byte-shape of the existing `idle.glb`**
|
||||
(which is 66 nodes / 1 skin / 0 meshes / 0 tris). Confirmed again after loading in three.js:
|
||||
0 meshes, 0 triangles added to the scene by any group file.
|
||||
|
||||
## 4. THE MANIFEST SCHEMA — `web/assets/motion_manifest.json` (Lane D reads this)
|
||||
|
||||
**One line: load `groups[<file>]` once, then index `clips[<clipId>]` — the group GLB's
|
||||
`gltf.animations[]` are NAMED by `clipId`, so `animations.find(a => a.name === clipId)` is the whole
|
||||
lookup.**
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"_schema": "…", "_licence": "…", // provenance travels with the data
|
||||
"_tracks": "rotation-only", // see §5 — this is what rigs.js keeps anyway
|
||||
"_encoding": "rotation outputs are int16 NORMALIZED …; all 65 joints animated incl. fingers",
|
||||
"_note": "skeleton-only, 0 tris, 66 nodes — same byte-shape as models/peds/idle.glb",
|
||||
"fps": 30,
|
||||
"groups": { // ONE ENTRY = ONE FETCH
|
||||
"idles.glb": { "category":"idle", "desc":"…", "clips":[<clipId>,…],
|
||||
"bytes":1029496, "nodes":66, "durationSum":78.3 }
|
||||
},
|
||||
"clips": { // 46 entries
|
||||
"sit_bench_swing": {
|
||||
"group": "sitlean.glb", // which file to fetch
|
||||
"category": "sitlean",
|
||||
"duration": 2.1333, // seconds, MEASURED from the keyframe times
|
||||
"loopable": true, // safe to play on repeat
|
||||
"loopSeamDeg": 0.04, // MEASURED: max per-bone angle, first key vs last key
|
||||
"source": "Little_Girl_Sitting_On_A_Bench_Swinging_Her_Legs__c9c89165.fbx",
|
||||
"mixamo": "Sitting", // the Mixamo catalogue title
|
||||
"channels": 65, "rotBones": 65,
|
||||
"pair": "<clipId>", // OPTIONAL — a matched two-person / open-close partner
|
||||
"loopDemoted": "…" // OPTIONAL — present iff curated-loopable but seam too wide
|
||||
}
|
||||
},
|
||||
"totalBytes": 3498124, "clipCount": 46, "groupCount": 6, "dedupSavedBytes": 1341724
|
||||
}
|
||||
```
|
||||
|
||||
Consumer notes for D: the clips ride the **same `_canon` + `_rotOnly` path as `walk`/`idle`** — 65
|
||||
raw tracks in, **64 out** (Hips.quaternion dropped). `loadPedFleet` today takes `r.anims[0]`; a
|
||||
group file has N, so read them by name. Everything stays behind your gate — I added no loader code
|
||||
and touched no file under `web/js/`.
|
||||
|
||||
## 5. Where the bytes went — three levers, each measured
|
||||
|
||||
Baseline, straight out of Blender as 46 separate GLBs: **8.20 MB**.
|
||||
|
||||
| lever | → | why it costs nothing real |
|
||||
|---|---:|---|
|
||||
| rotation-only tracks | 7.71 MB | `_rotOnly` decodes every translation/scale track and discards it the same frame. Dead bytes in this engine, not fidelity. `--tracks all` restores them |
|
||||
| accessor dedup by content | 5.98 MB | lossless — **1 341 724 B** of byte-identical keyframe payloads collapsed |
|
||||
| int16-normalized rotations | **3.34 MB** | glTF-legal; three r175 de-normalises in `GLTFLoader._getArrayFromAccessor` (checked in `web/vendor/`). **Worst angular error across all 46 clips: 0.0034°** |
|
||||
|
||||
**The lever I did NOT pull, for D:** finger channels are **49.7% of the payload** (measured — 3.37 MB
|
||||
of the pre-quantisation total). `python3 pipeline/clips_pack.py --drop-fingers` takes the whole set
|
||||
to **1.71 MB**. I kept fidelity; say the word and it is one flag and one minute.
|
||||
|
||||
## 6. Why there is NO retarget — and why that is right
|
||||
|
||||
The Mixamo bank and PROCITY's ped fleet are **the same mixamorig skeleton**. That is the premise of
|
||||
the house one-canonical-clip-bank law (`rigs.js` `_canon`). Running mixamorig→mixamorig through
|
||||
MIRPAMO's retargeter would (a) add transfer error for nothing and (b) **emit the ped MESH into the
|
||||
clip file**, killing the zero-draw property that makes this round affordable. MIRPAMO's retargeter
|
||||
is for foreign skeletons (its CMU bonemap) and for rigging raw meshes — it was still run (`make
|
||||
smoke`, green) as the tool check. The Blender-5 slotted-action gotcha is avoided **by construction**:
|
||||
nothing goes out through FBX at any stage — FBX in, glTF out.
|
||||
|
||||
## 7. The clips, by category (46) — `clipId | Mixamo title | duration | loopable | seam° | source FBX`
|
||||
|
||||
### idles.glb — idle (10) — the clone-army killer
|
||||
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|
||||
|---|---|---:|:---:|---:|---|
|
||||
| `idle_breathing` | Breathing Idle | 9.97s | yes | 0.0 | `Breathing_Idle__c9c6d0d5.fbx` |
|
||||
| `idle_standing` | Standing Idle | 6.03s | yes | 0.0 | `Standing_Idle__c9c831cf.fbx` |
|
||||
| `idle_look_around` | Looking Around | 6.37s | yes | 0.0 | `Idle_Stand_Looking_Around__c9c6cdf7.fbx` |
|
||||
| `idle_weight_shift` | Weight Shift | 9.47s | yes | 0.0 | `Shifting_Weight_From_Side_To_Side__c9c908f7.fbx` |
|
||||
| `idle_cocky_lean` | Being Cocky | 2.93s | no | 0.0 | `Cocky_Lean_Back__c9c90e3c.fbx` |
|
||||
| `idle_yawn` | Yawn | 8.37s | no | 0.0 | `Big_Yawn_While_Standing__c9c98361.fbx` |
|
||||
| `idle_happy_1` | Happy Idle | 2.03s | yes | 0.3 | `Happy_Idle_Variation_1__c9ccf37e.fbx` |
|
||||
| `idle_happy_2` | Happy Idle | 2.97s | yes | 0.0 | `Happy_Idle_Variation_2__c9ccf445.fbx` |
|
||||
| `idle_old_man` | Old Man Idle | 12.20s | yes | 0.0 | `Old_Man_Standing_Idle__c9c6c004.fbx` |
|
||||
| `idle_smoking` | Smoking | 17.93s | yes | 0.4 | `Idle_Smoking__c9c603fd.fbx` |
|
||||
### browse.glb — browse (8)
|
||||
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|
||||
|---|---|---:|:---:|---:|---|
|
||||
| `browse_hold_idle` | Holding Idle | 6.03s | yes | 0.1 | `Holding_An_Object_Idle__c9cda11a.fbx` |
|
||||
| `browse_hold_walk` | Holding Walk | 1.40s | yes | 6.3 | `Holding_An_Object_While_Walking__c9cda1e7.fbx` |
|
||||
| `browse_hold_turn_l` | Holding Turn Left | 1.23s | no | 172.5 | `Holding_An_Object_While_Turning_Left__c9cda2af.fbx` |
|
||||
| `browse_hold_turn_r` | Holding Turn Right | 1.33s | no | 147.3 | `Holding_An_Object_While_Turning_Right__c9cda378.fbx` |
|
||||
| `browse_open_cabinet` | Opening | 4.83s | no | 0.0 | `Opening_Cabinet_Door__c9c6d49e.fbx` |
|
||||
| `browse_close_cabinet` | Closing | 6.20s | no | 0.0 | `Closing_Cabinet_Door__c9c6d55d.fbx` |
|
||||
| `browse_carry_box` | Box Idle | 6.33s | yes | 0.0 | `Carrying_A_Box__c9cd8d90.fbx` |
|
||||
| `browse_pick_up` | Picking Up Object | 3.47s | no | 92.2 | `Picking_Up_An_Object_With_One_Hand__c9c6c5eb.fbx` |
|
||||
### sitlean.glb — sitlean (8)
|
||||
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|
||||
|---|---|---:|:---:|---:|---|
|
||||
| `sit_bench_swing` | Sitting | 2.13s | yes | 0.0 | `Little_Girl_Sitting_On_A_Bench_Swinging_Her_Legs__c9c89165.fbx` |
|
||||
| `sit_hands_thighs` | Sitting Idle | 6.43s | yes | 0.1 | `Sitting_In_Chair_Hands_Resting_On_Thighs__c9cb9783.fbx` |
|
||||
| `sit_impatient` | Sitting | 7.03s | yes | 0.0 | `Sitting_And_Impatiently_Waiting__c9ccad56.fbx` |
|
||||
| `sit_fidget_feet` | Sitting | 1.20s | yes | 0.2 | `Sit_With_Fidgeting_Feet__c9c699a5.fbx` |
|
||||
| `lean_wall_postures` | One Shoulder Lean | 2.03s | yes | 0.0 | `Leaning_Against_A_Wall_With_Various_Postures__c9c695f0.fbx` |
|
||||
| `lean_wall_leg_up` | Leaning | 3.37s | yes | 0.0 | `Leaning_Square_Against_A_Wall_With_One_Leg_Up__c9c696b0.fbx` |
|
||||
| `lean_wall_shoulders` | Leaning | 3.37s | yes | 0.0 | `Leaning_Square_With_Shoulders_Against_A_Wall__c9c69770.fbx` |
|
||||
| `lean_wall_walkman` | Leaning On A Wall | 1.97s | yes | 0.0 | `Little_Girl_Leaning_On_A_Wall_Listening_To_Music__c9c8bace.fbx` |
|
||||
### social.glb — social (8)
|
||||
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|
||||
|---|---|---:|:---:|---:|---|
|
||||
| `social_conversation` | Talking | 10.30s | yes | 0.3 | `General_Conversation__c9c88301.fbx` |
|
||||
| `social_chat_water` | Talking At Watercooler | 44.60s | yes | 0.8 | `Having_A_Chat_At_The_Watercooler__c9c9f1c9.fbx` |
|
||||
| `social_greeting` | Standing Greeting | 5.13s | no | 0.0 | `Greeting_While_Standing__c9c7996a.fbx` |
|
||||
| `social_shake_1` | Shaking Hands 1 | 4.40s | no | 0.0 | `2_People_Shaking_Hands_Part_1_-_Female__c9c9f343.fbx` |
|
||||
| `social_shake_2` | Shaking Hands 2 | 4.40s | no | 0.1 | `2_People_Shaking_Hands_Part_2_-_Male__c9c9f403.fbx` |
|
||||
| `social_nod_yes` | Head Nod Yes | 2.63s | no | 0.0 | `Nodding_Head_Yes__c9c90d7b.fbx` |
|
||||
| `social_shake_no` | Shaking Head No | 1.83s | no | 0.0 | `Shaking_Head_No_Dismissively__c9c90b3c.fbx` |
|
||||
| `social_wave` | Waving | 0.57s | no | 0.0 | `Waving__c9c5ed32.fbx` |
|
||||
### locomotion.glb — locomotion (6)
|
||||
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|
||||
|---|---|---:|:---:|---:|---|
|
||||
| `walk_shopping_bag` | Walking With Shopping Bag | 1.30s | yes | 0.0 | `Male_Walking_With_Shopping_Bag__c9c64027.fbx` |
|
||||
| `turn_left_90` | Unarmed Turn Left 90 | 1.53s | no | 84.6 | `Turning_90_Degrees_Left__c9ceef5f.fbx` |
|
||||
| `turn_right_90` | Unarmed Turn Right 90 | 1.53s | no | 83.2 | `Turning_90_Degrees_Right__c9cef01d.fbx` |
|
||||
| `turn_walk_180` | Walking Turn 180 | 1.03s | no | 180.0 | `Turning_180_Degrees_While_Walking__c9c9c73f.fbx` |
|
||||
| `turn_in_place` | Turning | 3.70s | no | 36.7 | `Turning_In_Place__c9c70ef0.fbx` |
|
||||
| `walk_to_stand` | Stop Walking | 3.03s | no | 55.3 | `Walking_To_Standing_Idle__c9c8d966.fbx` |
|
||||
### venue.glb — venue (6)
|
||||
| clip id | Mixamo title | dur | loop | seam° | source FBX |
|
||||
|---|---|---:|:---:|---:|---|
|
||||
| `venue_clap_stand` | Clapping | 1.20s | yes | 0.1 | `Clap_While_Standing__c9c60ef2.fbx` |
|
||||
| `venue_clap_seated` | Clapping | 1.20s | yes | 0.1 | `Clap_While_Seated__c9c60fce.fbx` |
|
||||
| `venue_cheer` | Cheering | 2.93s | yes | 0.0 | `Male_Cheering_With_Two_Fists_Pump__c9c66dfd.fbx` |
|
||||
| `venue_northern_soul` | Northern Soul Dance | 16.10s | yes | 0.4 | `Basic_Northern_Soul_Step__c9cbe56d.fbx` |
|
||||
| `venue_bartending` | Bartending | 13.60s | yes | 0.0 | `Pouring_And_Serving_Drinks__c9c78799.fbx` |
|
||||
| `venue_headphones` | Listening To Music | 4.50s | yes | 0.0 | `Male_With_Headphones_Listening_To_Music__c9c63329.fbx` |
|
||||
|
||||
## 8. Curation — three substitutions, because the named seed was wrong for this town
|
||||
|
||||
Every §41.1 seed was checked against `index.json` by name. Three resolved to something the brief
|
||||
did not intend:
|
||||
|
||||
| §41.1 named | what it actually is in the bank | shipped instead |
|
||||
|---|---|---|
|
||||
| the whole `45/90/135/180_Degree_{L,R}_Turn` set | **rifle-aiming.** Every single one is Mixamo "Rifle Turn" or "Rifle Crouch Turn". A shopping town does not want citizens sweeping the street down a barrel | the civilian set: `Turning_90_Degrees_Left/Right` (Mixamo **"Unarmed Turn 90"**), `Turning_180_Degrees_While_Walking`, `Turning_In_Place`, `Walking_To_Standing_Idle` |
|
||||
| `Examine_Forwards` / `_Backwards` | **static poses, not motion** — 262 KB single-frame entries titled "Female Crouch Pose" / "Female Action Pose" | `Opening_Cabinet_Door` + `Picking_Up_An_Object_With_One_Hand` |
|
||||
| `Legs_Crossed_Leaning_On_Object` | static pose, "Male Standing Pose", 262 KB | `Leaning_Square_Against_A_Wall_With_One_Leg_Up` + `…_With_Shoulders_Against_A_Wall` |
|
||||
|
||||
`2_People_Shaking_Hands_Part_1/2` **does** exist (`…_-_Female` / `…_-_Male`) and **ships as a pair**,
|
||||
flagged `"pair"` in the manifest — as do the two holding-turns and the cabinet open/close.
|
||||
Two picks are deliberately for *this* game rather than generic: `Idle_Smoking` and
|
||||
`Male_Walking_With_Shopping_Bag` (1990s Australian high street), plus `Pouring_And_Serving_Drinks`
|
||||
for the pub and `Male_With_Headphones_Listening_To_Music` for the record shop.
|
||||
|
||||
## 9. Verification — every claim above is a measurement
|
||||
|
||||
**`pipeline/clips_verify.py` → GREEN, 0 errors, 0 warnings** (new, re-runnable; **F may gate on it**
|
||||
— it is the "every referenced clip resolves" check). It asserts: file sizes match the manifest ·
|
||||
every clip id resolves to a named animation and nothing extra rides along · every group is zero-draw
|
||||
and Draco-free · all six carry the identical 66-node skeleton · every channel targets a real node ·
|
||||
every declared duration equals the keyframe times in the bytes · every clip survives `_rotOnly` with
|
||||
tracks left · quantisation error is bounded.
|
||||
|
||||
**In the repo's own vendored three.js r175**, through `GLTFLoader` + `SkeletonUtils.clone` of a real
|
||||
`man_casual_01.glb`, all 46 clips: **65 raw tracks → 64 after `_rotOnly`**, durations match to 3
|
||||
decimals, every sampled bone world-position finite and moving, 0 meshes / 0 tris added.
|
||||
|
||||
**The R16 trap was checked for on purpose.** R16 found that keeping `Hips.quaternion` on a seated
|
||||
clip lays the body flat (head folded to hip height). Spine tilt (Hips→Head vs +Y) sampled at 24
|
||||
points across every clip: **0 frames over 75°, on all 46.** Worst is 36.5° (`social_chat_water` —
|
||||
a lean on the watercooler); the seated family sits at 27–28°, a natural seated forward lean.
|
||||
**Nothing folds.**
|
||||
|
||||
**Loopability is measured.** `loopSeamDeg` = max per-bone angle between first and last rotation key.
|
||||
28 of 46 loopable. One curated-loopable clip was **auto-demoted by the packer** — `turn_in_place`,
|
||||
36.7° seam — and carries `loopDemoted` saying so. The one-shots read exactly as expected
|
||||
(`browse_hold_turn_l` 172.5°, `turn_left_90` 180°): they are turns, they are not supposed to close.
|
||||
|
||||
## 10. Re-running it (all $0, all on-device, ~2 minutes)
|
||||
|
||||
Added to `pipeline/`: `clips_r41.json` (the checked-in selection — **every asset by name, no
|
||||
wildcard, ruling 3**), `clips_export.py` (Blender), `clips_pack.py` (pure stdlib), `clips_verify.py`.
|
||||
|
||||
```bash
|
||||
# 1. fetch the 46 named FBX from ultra's GREEN Mixamo bank (BY NAME — never a wildcard)
|
||||
python3 - <<'EOF' > /tmp/fbx_list.txt
|
||||
import json;print('\n'.join(c['file'] for g in json.load(open('pipeline/clips_r41.json'))['groups'].values() for c in g['clips']))
|
||||
EOF
|
||||
ssh johnking@100.91.239.7 "cd ~/Documents/MOTIONLIB/green/mixamo_full && tar czf - $(tr '\n' ' ' </tmp/fbx_list.txt)" | tar xzf - -C /tmp/fbx
|
||||
# 2. FBX -> per-clip skeleton-only GLB (local Blender 5.1.2; FBX in, glTF out — never FBX out)
|
||||
/Applications/Blender.app/Contents/MacOS/Blender --background --python pipeline/clips_export.py -- \
|
||||
--list pipeline/clips_r41.json --fbx /tmp/fbx --out /tmp/clip_glb
|
||||
# 3. merge into the <=6 grouped GLBs + write the manifest
|
||||
python3 pipeline/clips_pack.py --stage /tmp/clip_glb --out web/models/clips \
|
||||
--manifest web/assets/motion_manifest.json # [--drop-fingers] [--no-quantize] [--tracks all]
|
||||
# 4. gate
|
||||
python3 pipeline/clips_verify.py --stage /tmp/clip_glb # 0 errors = green
|
||||
python3 pipeline/glb_stat.py --json web/models/clips # tris must be 0 on all six
|
||||
```
|
||||
|
||||
## 11. Handoff
|
||||
|
||||
**Lane D (41.3) — you are unblocked.** `web/assets/motion_manifest.json` is the contract; §4 above is
|
||||
the schema. `web/models/clips/*.glb` are plain uncompressed GLBs, no Draco, no decoder. Nothing in
|
||||
`web/js/**` was touched — no loader code, no gate, no default changed, so `?noassets=1` and
|
||||
`?classic=1` are byte-identical to R40 until you wire it. The per-group `bytes` are in the manifest
|
||||
so you can pick what boots: `sitlean` + `locomotion` together are **571 KB**; the whole set is
|
||||
**3.34 MB in 6 fetches**. If that is too much, `--drop-fingers` halves it in one minute.
|
||||
|
||||
**Lane F (41.6):** `python3 pipeline/clips_verify.py` is a ready-made clip-manifest-integrity gate —
|
||||
exit 0 = green, and it fails loudly on a missing file, a renamed clip, a size drift, a non-zero-draw
|
||||
group or a skeleton mismatch. `pipeline/glb_stat.py --json web/models/clips` is the zero-draw proof.
|
||||
|
||||
**Files I touched, and only these:** `web/models/clips/{idles,browse,sitlean,social,locomotion,venue}.glb`
|
||||
(new) · `web/assets/motion_manifest.json` (new) · `pipeline/{clips_r41.json,clips_export.py,clips_pack.py,clips_verify.py}`
|
||||
(new) · `pipeline/AUDIT.md` · `pipeline/README.md` · `E-progress.md` · `docs/LANES/LANE_E_NOTES.md`.
|
||||
Outside the repo, for ruling 1 only: `~/Documents/MIRPAMO/library/**`. **No git command was run.**
|
||||
I did not touch `web/assets/manifest.json` (props half's), `web/js/**`, or `tools/qa.sh`.
|
||||
|
||||
@ -1,5 +1,80 @@
|
||||
# LANE E — cross-lane notes
|
||||
|
||||
## Round 41 — 110 interior fittings land, and the number that mattered was DRAWS ⟨41.2⟩
|
||||
|
||||
Shots: `docs/shots/laneE/r41_3dstore_40.png` · `r41_pub_42.png` · `r41_djgear_31.png` ·
|
||||
`r41_pub_rejects.png` · `r41_3dstore_merge_ab.png`. Full tables: `E-progress.md` § Round 41.
|
||||
|
||||
### → LANE C: §41.4 — your stock is in `manifest.json` `fittings` (23 → 133). Place on the numbers untouched.
|
||||
|
||||
**Read this first, it is the whole hand-off:** every asset below is **1 draw** except four DJ heroes
|
||||
(2–5). That was not free — as handed over, this cargo cost **14,140 draws**; it ships at **117**.
|
||||
The interior margin is 162, so the naive import would have been 87× over budget on draws while
|
||||
passing every triangle check. Per-entry `tris` and `draws` are now IN the manifest so you can spend
|
||||
the margin without guessing.
|
||||
|
||||
Each entry: `file` · `footprint` [w,d] m · `height` m · `thumb` · `kind` · `tris` · `draws` ·
|
||||
`source`. DJ entries add `mode` (`static`|`rigged`), `control_nodes`, `source_controls`, `drivable`.
|
||||
|
||||
**`kind` tells you how to place it:**
|
||||
|
||||
| kind | n | placement | ids |
|
||||
|---|---:|---|---|
|
||||
| `bin` | 14 | floor-standing record bins, browsable | `bin_endoffunk`, `chiu_bin`, `chiu_bin_half`, `chiu_bin_low`, `crate_blue`, `mos_bin_bottom`, `mos_bin_lower_mid`, `mos_bin_top`, `mos_bin_upper_mid`, `rack_bin_wooden`, `tub_black`, `tub_black_long`, `tub_white`, `tub_white_long` |
|
||||
| `rack` | 12 | floor-standing multi-tier racks | `bin_big_4x4`, `chiu_rack_long`, `chiu_rack_short`, `mos_stack`, `tubs_3x3`, `tubs_3x3_long`, `tubs_4x2_vertical`, `tubs_4x2_wide`, `tubs_shelf_long`, `tubs_shelf_wide`, `wooden_rack_long`, `wooden_rack_wide` |
|
||||
| `counter` | 2 | service counters | `counter_station`, `counter_station_side` |
|
||||
| `trolley` | 1 | wheeled | `library_trolley` |
|
||||
| `prop` | 2 | surface dressing | `vinyl_sleeve`, `window_booth` |
|
||||
| `wall_mount` | 5 | **needs a wall + a mount height — these have no legs** | `aircon_unit`, `shelf_board_long`, `shelf_board_short`, `wall_shelves_big`, `wall_shelves_long` |
|
||||
| `arch` | 4 | structure (door/columns/beam) | `beam_roof`, `column_square`, `column_wide`, `door_shop` |
|
||||
| `pub` | 39 | pub/cafe/milkbar/venue, floor-standing | see below |
|
||||
| `djgear` | 31 | record shop + gig venue hero props | see below |
|
||||
|
||||
**Pub/venue (39):** `ash_urn`, `ashtray_post`, `bain_marie`, `banquette`, `beer_umbrella`, `bin_round`, `bistro_table`, `broken_basin`, `cable_snake`, `can_stack`, `cdj_deck`, `champ_bucket`, `concrete_plinth`, `crowd_barrier`, `distro_box`, `fire_ext`, `firepit`, `flight_case`, `jukebox`, `keg`, `lounge_chair`, `marble_sink`, `old_couch`, `pallet`, `picnic_table`, `pie_warmer`, `planter`, `plywood_bar`, `pokie`, `raffle_drum`, `rope_post`, `sandwich_board`, `smoke_machine`, `speaker_stack`, `strobe_light`, `sub_bass`, `topiary`, `urinal_trough`, `wheelie_bin`.
|
||||
|
||||
**DJ gear — static, 1 draw each (27):** `cart_case`, `cart_concorde`, `cart_m447`, `cart_stanton`, `deck_1200`, `drum_808`, `drum_tr1000`, `headphones_hd25`, `headshell_technics`, `mixer_mx2200`, `mixer_pmc05pro`, `mixer_pmc06pro`, `mixer_pmc07pro`, `mixer_pmc08pro`, `mixer_pmc10`, `mixer_pmc20`, `mixer_pmx9000`, `mixer_shdj1200`, `mixer_ttm54i`, `sampler_mpc3000`, `sampler_s900`, `sampler_sp1200`, `sl_deck_1`, `sl_deck_2`, `sl_deck_3`, `sl_deck_4`, `zip_drive`.
|
||||
|
||||
**DJ gear — rigged heroes (the only drivable ones):**
|
||||
|
||||
| id | draws | control nodes |
|
||||
|---|---:|---|
|
||||
| `deck_1200_rigged` | 5 | `Platter_SPIN`, `Arm_YAW`, `Fader_PITCH`, `Btn_STARTSTOP` |
|
||||
| `mixer_ttm54i_rigged` | 2 | `Fader_CROSS` |
|
||||
| `mixer_pmc05pro_rigged` | 2 | `Fader_CROSS` |
|
||||
| `mixer_shdj1200_rigged` | 2 | `Fader_CROSS` |
|
||||
|
||||
Drive them by their **bare node name** (`dj-gear/README.md`: the cap/stem/rib are parented under
|
||||
it, so moving the node moves the whole control). glTF drops the source's limit constraints, so
|
||||
**clamp the travel yourself**. Place at most one or two — a static copy is 1 draw and looks the
|
||||
same standing still.
|
||||
|
||||
**Three traps:**
|
||||
1. The 3dstore `-long`/`-wide` pairs (`tubs_4x2_wide`/`_vertical`, `tubs_3x3`/`_long`,
|
||||
`tubs_shelf_long`/`_wide`, `wooden_rack_long`/`_wide`, `chiu_rack_long`/`_short`,
|
||||
`counter_station`/`_side`) are **the same mesh rotated 90°**. Pick one per placement; shipping
|
||||
both into one room is paying twice for one prop.
|
||||
2. `vinyl_sleeve` is a 3 mm record sleeve — its footprint depth is floored to 0.01 m so the
|
||||
validator accepts it. It is a surface/wall dressing, not something to stand on the floor.
|
||||
3. `beam_roof` is 6.5 m long and `shelf_board_long` 4.6 m — both are longer than most interiors are
|
||||
wide. Check the room before placing.
|
||||
|
||||
### → LANE F: §41.6 — the R40 four-surface gate held over 110 new assets, and it caught real cargo
|
||||
|
||||
`validate_manifest.py` is **rc 0, 0 errors, 0 warnings** with `GLBs 149 (+40 non-manifest GLBs
|
||||
transmission-swept)`, and also rc 0 under `--depot`. Nothing about the gate changed this round; it
|
||||
was exercised.
|
||||
|
||||
**It fired on live imports, not a fixture.** The sweep caught **5 real
|
||||
`KHR_materials_transmission` materials** in the DJ gear — three cartridge dust-covers (`cc_clear`,
|
||||
`n44_clear`, factor 0.85–0.9) and a mixer meter window (`meter_window`, 0.9). Those are *plausible*
|
||||
glass, which is exactly the R39 failure mode (nobody looks at a material that has a reason to be
|
||||
transmissive). Each would have made three.js issue every opaque draw in that room twice.
|
||||
`strip_transmission.py` removed all five with the **BIN chunk byte-identical**. R40 demonstrated
|
||||
this control with a synthetic fixture; R41 is the same control catching real cargo.
|
||||
|
||||
All 110 are published to the tailnet depot and **sha1-verified byte-identical on fetch-back**
|
||||
(`pipeline/_r41_sha1.json`); `_published.json` 59 → 169 and matches the live depot.
|
||||
|
||||
## Round 40 — the transmission gate grows eyes on every load path, and the arcade kit is a rule, not an asset ⟨40.1 + 40.4⟩
|
||||
|
||||
Shots: `docs/shots/laneE/r40_arcade_posts_before.png` · `r40_arcade_posts_after.png` ·
|
||||
@ -1687,3 +1762,27 @@ gate 3 after your sign-off.
|
||||
- **E2a record_crate: DONE + verified** (loads in vendored GLTFLoader). Re-published.
|
||||
- **E2b counter: in progress** (local, on-device — no fal spend per decision #3).
|
||||
- Not doing: fal.ai spend (glass-case/bus-shelter stay parked), hero-prop bake-to-lowpoly.
|
||||
|
||||
---
|
||||
|
||||
## Round 41 §41.1 — the motion library (E, motion half)
|
||||
|
||||
- **Ruling 1 closed first.** 3,077 CC BY-NC Bandai-Namco BVH on m3ultra were unzoned and unmarked;
|
||||
now `~/Documents/MIRPAMO/library/downloads/red/bandai_namco_1_NC-research/` with `_NC-research`
|
||||
on **every** file (3,077/3,077, 0 unmarked) — `mirpamo batch` names output `<rig>@<clip>.glb`, so
|
||||
the marker propagates automatically. Nothing deleted (552 MB intact; ultra's `red/bandai/` is the
|
||||
canonical zoned copy). MIRPAMO's `library/manifest.json` licence corrected `CC-BY-NC-ND 4.0` →
|
||||
**`CC BY-NC 4.0`** (the bundled LICENSE files say Attribution-NonCommercial, no ND), matching
|
||||
ultra's `MOTIONLIB/SOURCES.md`.
|
||||
- **46 clips, 6 grouped GLBs, 3 498 124 B, 0 tris** → `web/models/clips/` + new
|
||||
`web/assets/motion_manifest.json` (16 KB). 5.75× the clips the game shipped, for 78% of their bytes.
|
||||
- **Pipeline:** `pipeline/clips_r41.json` (named selection) → `clips_export.py` (Blender) →
|
||||
`clips_pack.py` (stdlib merge) → `clips_verify.py` (gate, GREEN). See `pipeline/README.md` §10.
|
||||
- **No retarget:** the bank and the peds are the same mixamorig skeleton. Retargeting would add
|
||||
error and bake the ped mesh into the clip, ending zero-draw. `mirpamo make smoke` green anyway.
|
||||
- **Curation note for future rounds:** the `*_Degree_{L,R}_Turn` clips in the Mixamo bank are all
|
||||
**rifle-aiming**; the civilian equivalents are `Turning_90_Degrees_{Left,Right}` ("Unarmed Turn").
|
||||
Bank entries at ~262 KB whose Mixamo title ends "Pose" are **single-frame static poses**, not
|
||||
motion — check `kb` before picking.
|
||||
- **Standing lever for D/F:** finger channels are 49.7% of the clip payload;
|
||||
`clips_pack.py --drop-fingers` takes the set 3.34 MB → 1.71 MB in one minute.
|
||||
|
||||
BIN
docs/shots/laneE/r41_3dstore_40.png
Normal file
|
After Width: | Height: | Size: 4.8 MiB |
BIN
docs/shots/laneE/r41_3dstore_merge_ab.png
Normal file
|
After Width: | Height: | Size: 619 KiB |
BIN
docs/shots/laneE/r41_djgear_31.png
Normal file
|
After Width: | Height: | Size: 822 KiB |
BIN
docs/shots/laneE/r41_djgear_merge_ab.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
docs/shots/laneE/r41_djgear_rigged.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
docs/shots/laneE/r41_pub_42.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/shots/laneE/r41_pub_before_after.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/shots/laneE/r41_pub_rejects.png
Normal file
|
After Width: | Height: | Size: 885 KiB |
BIN
docs/shots/laneE/r41_shellsoup_solidify.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
@ -5,6 +5,36 @@
|
||||
`pipeline/glb_stat.py` (dependency-free GLB parser); orientation & final scale verified by
|
||||
rendering every candidate through Blender and looking at the thumbnails (never "should look right").*
|
||||
|
||||
## R41 §41.2 — provenance for the three imported libraries (2026-08-07)
|
||||
|
||||
All three are John's own local libraries; none is a vendor pack and none touches the ruling-3
|
||||
deny-list. Every asset entered **by name** through a committed spec, never a wildcard copy.
|
||||
|
||||
| library | path | licence zone | spec | shipped |
|
||||
|---|---|---|---|---|
|
||||
| shop fittings | `~/Documents/3dstore/` (this box) | in-house — built in Blender by John (`*.blend` + `build_booth_room_v*.py` sit beside the GLBs) | `pipeline/r41_3dstore.json` | 40 of 46 |
|
||||
| pub/venue props | `~/Documents/not-tonight/art_incoming/` (this box) | in-house — `trimesh` generator output from John's `not-tonight` project, each with its own `*_shot.png` reference render | `pipeline/r41_pub_batch.json` | 39 of 48 |
|
||||
| DJ gear | ultra `~/Documents/3D=models/dj-gear/glb/` | in-house — exported 2026-08-01 from John's `~/Documents/opusblender` blends | `pipeline/r41_djgear_batch.json` | 31 of 60 |
|
||||
|
||||
**Trademark note, carried forward from the R4 Akai-MPC flag:** the DJ-gear ids name real hardware
|
||||
(`drum_808`, `sampler_mpc3000`, `sampler_sp1200`, `sampler_s900`, `mixer_pmc*`, `deck_1200`,
|
||||
`headphones_hd25`, `cart_m447`). The meshes are John's own reconstructions, not vendor assets, and
|
||||
the source `README` records that several are already wired into 90sDJsim. Same standing caveat as
|
||||
R4: **fine in-house; flag before any public/commercial release**, where in-world text should use
|
||||
generic names.
|
||||
|
||||
**Method (measured, not quoted).** Tri counts, bounding boxes, texture sizes, Draco and
|
||||
transmission flags from `pipeline/glb_stat.py`; **draw counts** from a direct node→mesh→primitive
|
||||
walk of each GLB, which is the number `GLTFLoader` turns into `Mesh` objects and the one the
|
||||
handover notes omitted. Footprints and heights are re-measured **from the shipped GLB** after
|
||||
export, not from the Blender scene that wrote it — `rig_join.py`'s in-scene bounds mis-reported a
|
||||
0.114 m mixer as 0.629 m, and Lane C places on these numbers. Every asset was eyeballed on a
|
||||
contact sheet against its source (`docs/shots/laneE/r41_*.png`); the 9 rejects are listed with
|
||||
reasons in `E-progress.md` § Round 41.
|
||||
|
||||
**Documented budget exceptions: none.** All 110 land ≤5k tris, WebP ≤1024, no Draco, base-origin.
|
||||
|
||||
|
||||
## TL;DR
|
||||
|
||||
- The library is a **raw dump**: almost nothing obeys the house GLB law. Scale is arbitrary
|
||||
@ -1100,3 +1130,135 @@ GLBs 36 → 39); **`validate_manifest.py`: 0 errors, 0 warnings.**
|
||||
|
||||
**Licence unchanged 🟢, $0.** No geometry, no texture and no third-party asset was added or altered
|
||||
— one extension object was deleted from three material blocks.
|
||||
|
||||
## Round 41 §41.1 — THE MOTION LIBRARY (Lane E, motion half)
|
||||
|
||||
*Appended by E-motion. Numbers below are measured with `pipeline/glb_stat.py`,
|
||||
`pipeline/clips_verify.py` and an in-shell three.js r175 probe — not quoted from the survey.*
|
||||
|
||||
### 0. Ruling 1 — THE BANDAI HAZARD, CLOSED (done first, before any clip work)
|
||||
|
||||
3,077 **CC BY-NC 4.0** (non-commercial, never shippable) Bandai-Namco Research BVH sat on m3ultra in
|
||||
a neutrally-named, unzoned path with nothing in any filename marking them:
|
||||
`~/Documents/MIRPAMO/library/downloads/bandai_namco_1/` (725 MB incl. the repo's `src/` figures;
|
||||
552 MB of clips: dataset-1 = 175 BVH, dataset-2 = 2,902 BVH).
|
||||
|
||||
**Renamed, not deleted** (non-destructive, per the instruction's preference — 552 MB of data still
|
||||
on disk, byte-for-byte):
|
||||
|
||||
| what | before | after |
|
||||
|---|---|---|
|
||||
| the tree | `MIRPAMO/library/downloads/bandai_namco_1/` | `MIRPAMO/library/downloads/red/bandai_namco_1_NC-research/` |
|
||||
| every clip | `dataset-1_walk_normal_001.bvh` | `dataset-1_walk_normal_001_NC-research.bvh` — **3,077/3,077 renamed, 0 unmarked left** |
|
||||
| the zone | *(none — MIRPAMO had no zone system)* | new `downloads/red/README.md`, mirroring ultra's `MOTIONLIB/red/README.md` |
|
||||
|
||||
Why per-file and not just the directory: `bin/mirpamo` `cmd_batch` names its output
|
||||
`<rig>@<clipbasename>.glb`, so the suffix **propagates into every retargeted GLB automatically** —
|
||||
a non-commercial clip can no longer reach a shipped build without `_NC-research` in its filename.
|
||||
`downloads/` is already gitignored in MIRPAMO, so `red/` inherits that and nothing gets committed.
|
||||
|
||||
**Ledger reconciled.** `MIRPAMO/library/manifest.json` said `CC-BY-NC-ND 4.0`. The bundled
|
||||
`dataset-1/LICENSE` and `dataset-2/LICENSE` both open with *"Attribution-NonCommercial 4.0
|
||||
International"* — **CC BY-NC 4.0, no ND clause**, which is what ultra's corrected
|
||||
`~/Documents/MOTIONLIB/SOURCES.md` says. The entry now carries the correct licence, `"zone": "RED"`,
|
||||
`"verified": true`, the renamed path as its `name` (so a future `fetch.py` run lands in the marked
|
||||
path too), and a note recording the correction. The `lafan1_ubisoft` entry — also non-commercial —
|
||||
got the same zone field plus the measured fact that **this box's copy is scaffold only**: 0 BVH,
|
||||
332 KB of 134-byte git-lfs pointer files.
|
||||
|
||||
Canonical correctly-zoned copy verified live on ultra: `~/Documents/MOTIONLIB/red/bandai/`,
|
||||
**3,077 BVH / 552 MB**. Nothing was lost by not deleting; nothing can now leak by accident.
|
||||
|
||||
### 1. Provenance and licence — 🟢 ONE REGIME, MIXAMO ONLY (ruling 2)
|
||||
|
||||
| field | value |
|
||||
|---|---|
|
||||
| source | ultra `~/Documents/MOTIONLIB/green/mixamo_full/` — 2,394 anim-only skinless FBX + `index.json` |
|
||||
| licence | **Mixamo (Adobe)** — free to use inside games/projects, **no attribution requirement** |
|
||||
| the one rule | **never redistribute or resell as clip packs.** Retargeted per-character clips inside the game are explicitly fine — which is exactly what ships here |
|
||||
| attribution debt | **none.** R41 adds nothing to the 41.5 credits surface |
|
||||
| cost | **$0** — local Blender 5.1.2, no cloud, no key |
|
||||
| deny-list (ruling 3) | untouched. Nothing entered `web/` from `character_kit_modular/exports/bodies/**`, any `anatomy/`, `elsa_coronation_hair_wig_kh3.glb`, `daphne_sexy.glb` or `~/Documents/mocaponline/**`. **No wildcard copy was used at any step** — the 46 source FBX were fetched by explicit name from a checked-in list (`pipeline/clips_r41.json`), each one verified present in `index.json` with a matching Mixamo title before the copy ran |
|
||||
| CMU / 100STYLE / ACCAD | **not touched.** All attribution-bearing; they wait on 41.5 |
|
||||
|
||||
### 2. What shipped — 46 clips, 6 grouped GLBs, 3 498 124 B
|
||||
|
||||
`web/models/clips/` — **zero-draw, verified, not asserted** (`pipeline/glb_stat.py`):
|
||||
every group file reports `tris=0 meshes=0 materials=0 images=0 nodes=66 draco=false`.
|
||||
|
||||
| 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** | | **46** | **3 498 124 (3.34 MB)** | **262 s** |
|
||||
|
||||
For scale: the 8 clips PROCITY already ships (`web/models/peds/{idle,walk,sit,look,dance_*}.glb`)
|
||||
cost **4.30 MB**. R41 adds **5.75× the clips for 78% of that** — and in **6 fetches, not 46**
|
||||
(ruling 6). `web/assets/motion_manifest.json` is 16 KB.
|
||||
|
||||
### 3. Where the bytes went — three levers, all measured
|
||||
|
||||
Baseline: the 46 clips exported straight out of Blender as separate GLBs = **8.20 MB**.
|
||||
|
||||
| step | result | why it is safe |
|
||||
|---|---|---:|
|
||||
| rotation-only tracks | 8.08 → **7.71 MB** | `rigs.js` `_rotOnly` decodes every translation/scale track and throws it away on the same frame. They were dead bytes in this engine, not fidelity |
|
||||
| accessor dedup by content | 7.71 → **5.98 MB** | lossless. 1 341 724 B of byte-identical keyframe payloads collapsed inside the group files |
|
||||
| int16-normalized rotations | 5.98 → **3.34 MB** | glTF-legal animation output. three.js r175 de-normalises it in `GLTFLoader._getArrayFromAccessor` (verified in `web/vendor/`). **Worst measured angular error over all 46 clips: 0.0034°** |
|
||||
|
||||
Documented but **not applied**: `--drop-fingers` removes the 40 finger-bone channels, which are
|
||||
**49.7% of the payload** (measured), taking the set to **1.71 MB**. Fidelity was kept instead —
|
||||
the lever is one flag away if D's boot budget ever bites.
|
||||
|
||||
### 4. No retarget was run, and that is the correct call
|
||||
|
||||
The Mixamo bank and PROCITY's ped fleet are **the same mixamorig skeleton** — that is the entire
|
||||
premise of the house "one canonical clip bank" law (`rigs.js` `_canon` folds `mixamorig1/4/…` to
|
||||
`mixamorig`). Pushing a mixamorig→mixamorig pair through MIRPAMO's retargeter would add
|
||||
transfer error for nothing **and** would emit the ped MESH into the clip file, destroying the
|
||||
zero-draw property this round is affordable because of. MIRPAMO's retargeter is for foreign
|
||||
skeletons (its CMU bonemap) and for rigging raw meshes.
|
||||
|
||||
MIRPAMO was still exercised as the tool ground-truth check the instruction asked for:
|
||||
**`make smoke` PASSED** (Blender 5.1.2, exit 0, procedural mesh → autorig → retarget → verify).
|
||||
The documented Blender-5 gotcha was respected by construction: nothing is exported through FBX at
|
||||
any stage — Mixamo FBX goes **in**, glTF comes **out**, so slotted actions are never flattened.
|
||||
|
||||
### 5. Curation — three substitutions, all because the named clip was wrong for this town
|
||||
|
||||
§41.1's seed list was checked line by line against `index.json`. Three seeds resolved to something
|
||||
the brief did not intend, and were substituted:
|
||||
|
||||
| seed named in §41.1 | what it actually is in the bank | substituted with |
|
||||
|---|---|---|
|
||||
| `45/90/135/180_Degree_{Left,Right}_Turn` (the whole set) | **rifle-aiming**: every one is "Rifle Turn" or "Rifle Crouch Turn". A shopping town does not need a citizen 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` / `Examine_Backwards` | **static poses**, not motions — 262 KB single-frame entries titled "Female Crouch Pose" / "Female Action Pose" | `Opening_Cabinet_Door` + `Picking_Up_An_Object_With_One_Hand` (real shop-floor motion) |
|
||||
| `Legs_Crossed_Leaning_On_Object` | static pose, "Male Standing Pose", 262 KB | `Leaning_Square_Against_A_Wall_With_One_Leg_Up` + `Leaning_Square_With_Shoulders_Against_A_Wall` |
|
||||
|
||||
`2_People_Shaking_Hands_Part_1/2` **does** exist (as `…_-_Female` / `…_-_Male`) and the pair ships
|
||||
together, flagged `"pair"` in the manifest — as do the two holding-turns and the cabinet open/close.
|
||||
|
||||
### 6. The proof, in the game's own three.js
|
||||
|
||||
`pipeline/clips_verify.py` (new, re-runnable, F may gate on it): **GREEN, 0 errors, 0 warnings** —
|
||||
sizes match the manifest, every clip id resolves to a named animation, nothing extra rides along,
|
||||
every group is zero-draw and carries the same 66-node skeleton, every declared duration matches the
|
||||
keyframe times in the bytes, and every clip survives `_rotOnly` with tracks left.
|
||||
|
||||
Loaded through **the repo's own vendored three.js r175 + GLTFLoader + SkeletonUtils**, bound to a
|
||||
real `SkeletonUtils.clone` of `man_casual_01.glb`, all 46 clips: 65 raw tracks → **64 after
|
||||
`_rotOnly`** (Hips.quaternion dropped, exactly as walk/idle behave), durations match to 3 decimals,
|
||||
every sampled bone position finite and non-degenerate, 0 meshes and 0 triangles added to the scene.
|
||||
|
||||
**The R16 trap was checked for explicitly.** R16 found that keeping `Hips.quaternion` on a seated
|
||||
clip lays the body flat. Spine tilt (Hips→Head vs +Y) was sampled at 24 points across every clip:
|
||||
**0 frames over 75° on all 46**, worst clip 36.5° (`social_chat_water`, a lean on the watercooler),
|
||||
seated family 27–28° (a natural seated forward lean). Nothing folds.
|
||||
|
||||
**Loopability is measured, not asserted.** Every clip carries `loopSeamDeg` = the max per-bone angle
|
||||
between its first and last rotation key. 28 of 46 are loopable; one curated-loopable clip was
|
||||
**auto-demoted** by the packer — `turn_in_place`, 36.7° seam — and says so in the manifest.
|
||||
|
||||
@ -166,3 +166,44 @@ to D's primitive (round-3 thin-wire lesson — spec allows "primitives when abse
|
||||
`pipeline/_normalized/` (staging GLBs, live on depot), `pipeline/.genraw/` (raw skin gen).
|
||||
Committed: the scripts, `*.md`, `batch.json`, `meshgod_batch.json`, `_audit_stats.json`,
|
||||
`_normalize_results.json`, `web/assets/manifest.json`, `web/assets/thumbs/`, `docs/shots/laneE/`.
|
||||
|
||||
### 10. Motion library — `clips_export.py` → `clips_pack.py` → `clips_verify.py` *(round-41 §41.1: DONE, free, on-device)*
|
||||
|
||||
46 Mixamo clips → **6 grouped skeleton-only GLBs** in `web/models/clips/` (3 498 124 B, 0 tris) + a
|
||||
**new** `web/assets/motion_manifest.json`. Source is ultra's `~/Documents/MOTIONLIB/green/mixamo_full/`
|
||||
**only** (Mixamo = GREEN: usable in games, **no attribution**, never redistribute as clip packs).
|
||||
The selection is checked in at `pipeline/clips_r41.json` — **every asset by name, no wildcard**.
|
||||
|
||||
```bash
|
||||
python3 - <<'EOF' > /tmp/fbx_list.txt # 1. the 46 names
|
||||
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
|
||||
mkdir -p /tmp/fbx && 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
|
||||
BL=/Applications/Blender.app/Contents/MacOS/Blender # 2. FBX -> per-clip skeleton-only GLB
|
||||
"$BL" --background --python pipeline/clips_export.py -- --list pipeline/clips_r41.json --fbx /tmp/fbx --out /tmp/clip_glb
|
||||
python3 pipeline/clips_pack.py --stage /tmp/clip_glb \ # 3. merge to <=6 groups + manifest
|
||||
--out web/models/clips --manifest web/assets/motion_manifest.json
|
||||
python3 pipeline/clips_verify.py --stage /tmp/clip_glb # 4. gate: 0 errors = green
|
||||
```
|
||||
|
||||
**No retarget is run and none is wanted:** the Mixamo bank and PROCITY's peds are the *same*
|
||||
mixamorig skeleton (`rigs.js` `_canon` folds `mixamorig1/4/…`). A mixamorig→mixamorig pass through
|
||||
MIRPAMO would add error **and** bake the ped mesh into the clip file, destroying the zero-draw
|
||||
property. MIRPAMO (`~/Documents/MIRPAMO`, `make smoke` = its ground truth, green on Blender 5.1.2)
|
||||
is for foreign skeletons (CMU/BVH) and for rigging raw meshes. **Blender-5 gotcha avoided by
|
||||
construction: FBX goes in, glTF comes out — nothing is ever exported through FBX.**
|
||||
|
||||
Packer flags (each measured in `AUDIT.md` R41): `--tracks all` restores translation+scale (the
|
||||
engine's `_rotOnly` discards them, so they ship off by default) · `--no-quantize` keeps float32
|
||||
rotations (int16-normalized is ~2× smaller at a worst measured error of **0.0034°**) ·
|
||||
`--drop-fingers` removes the 40 finger channels = **49.7% of the payload**, taking the set to
|
||||
1.71 MB. Accessor dedup is lossless and always on (saved 1 341 724 B).
|
||||
|
||||
⚠️ **Licence zoning, ruling 1 (closed R41):** MIRPAMO's `library/downloads/` also held 3,077
|
||||
**CC BY-NC** (never-shippable) Bandai-Namco BVH, unzoned and unmarked. They now live at
|
||||
`library/downloads/red/bandai_namco_1_NC-research/` with the `_NC-research` suffix on **every** file
|
||||
(so `mirpamo batch`'s `<rig>@<clip>.glb` naming propagates the marker), and MIRPAMO's
|
||||
`library/manifest.json` licence label is reconciled to ultra's `MOTIONLIB/SOURCES.md`.
|
||||
**R41 ships one licence regime: Mixamo.** CMU/100STYLE/ACCAD are CC-BY — they wait on 41.5's credits
|
||||
surface.
|
||||
|
||||
@ -6,33 +6,143 @@
|
||||
"dj_phrtt_01.glb",
|
||||
"dj_techno_01.glb",
|
||||
"look.glb",
|
||||
"procity_fit_aircon_unit_01.glb",
|
||||
"procity_fit_arcade_cabinet_01.glb",
|
||||
"procity_fit_ash_urn_01.glb",
|
||||
"procity_fit_ashtray_post_01.glb",
|
||||
"procity_fit_bain_marie_01.glb",
|
||||
"procity_fit_banquette_01.glb",
|
||||
"procity_fit_bass_guitar_01.glb",
|
||||
"procity_fit_bass_guitar_01_hi.glb",
|
||||
"procity_fit_beam_roof_01.glb",
|
||||
"procity_fit_beer_umbrella_01.glb",
|
||||
"procity_fit_bin_big_4x4_01.glb",
|
||||
"procity_fit_bin_endoffunk_01.glb",
|
||||
"procity_fit_bin_round_01.glb",
|
||||
"procity_fit_bistro_table_01.glb",
|
||||
"procity_fit_bookshelf_01.glb",
|
||||
"procity_fit_box_crate_01.glb",
|
||||
"procity_fit_broken_basin_01.glb",
|
||||
"procity_fit_cable_snake_01.glb",
|
||||
"procity_fit_can_stack_01.glb",
|
||||
"procity_fit_cart_case_01.glb",
|
||||
"procity_fit_cart_concorde_01.glb",
|
||||
"procity_fit_cart_m447_01.glb",
|
||||
"procity_fit_cart_stanton_01.glb",
|
||||
"procity_fit_cdj_deck_01.glb",
|
||||
"procity_fit_champ_bucket_01.glb",
|
||||
"procity_fit_chiu_bin_01.glb",
|
||||
"procity_fit_chiu_bin_half_01.glb",
|
||||
"procity_fit_chiu_bin_low_01.glb",
|
||||
"procity_fit_chiu_rack_long_01.glb",
|
||||
"procity_fit_chiu_rack_short_01.glb",
|
||||
"procity_fit_clothes_rack_01.glb",
|
||||
"procity_fit_coffee_table_01.glb",
|
||||
"procity_fit_column_square_01.glb",
|
||||
"procity_fit_column_wide_01.glb",
|
||||
"procity_fit_concrete_plinth_01.glb",
|
||||
"procity_fit_counter_01.glb",
|
||||
"procity_fit_counter_station_01.glb",
|
||||
"procity_fit_counter_station_side_01.glb",
|
||||
"procity_fit_counter_till_01.glb",
|
||||
"procity_fit_crate_blue_01.glb",
|
||||
"procity_fit_crate_stack_01.glb",
|
||||
"procity_fit_crowd_barrier_01.glb",
|
||||
"procity_fit_cube_shelf_wide_01.glb",
|
||||
"procity_fit_deck_1200_01.glb",
|
||||
"procity_fit_deck_1200_rigged_01.glb",
|
||||
"procity_fit_distro_box_01.glb",
|
||||
"procity_fit_door_shop_01.glb",
|
||||
"procity_fit_drinks_fridge_01.glb",
|
||||
"procity_fit_drum_808_01.glb",
|
||||
"procity_fit_drum_kit_01.glb",
|
||||
"procity_fit_drum_kit_01_hi.glb",
|
||||
"procity_fit_drum_tr1000_01.glb",
|
||||
"procity_fit_electric_guitar_01.glb",
|
||||
"procity_fit_electric_guitar_01_hi.glb",
|
||||
"procity_fit_fire_ext_01.glb",
|
||||
"procity_fit_firepit_01.glb",
|
||||
"procity_fit_flight_case_01.glb",
|
||||
"procity_fit_glass_case_01.glb",
|
||||
"procity_fit_guitar_amp_01.glb",
|
||||
"procity_fit_guitar_amp_01_hi.glb",
|
||||
"procity_fit_headphones_hd25_01.glb",
|
||||
"procity_fit_headshell_technics_01.glb",
|
||||
"procity_fit_jukebox_01.glb",
|
||||
"procity_fit_keg_01.glb",
|
||||
"procity_fit_library_trolley_01.glb",
|
||||
"procity_fit_listening_booth_01.glb",
|
||||
"procity_fit_lounge_chair_01.glb",
|
||||
"procity_fit_magazine_rack_01.glb",
|
||||
"procity_fit_marble_sink_01.glb",
|
||||
"procity_fit_mic_stand_01.glb",
|
||||
"procity_fit_mic_stand_01_hi.glb",
|
||||
"procity_fit_milkshake_mixer_01.glb",
|
||||
"procity_fit_mixer_mx2200_01.glb",
|
||||
"procity_fit_mixer_pmc05pro_01.glb",
|
||||
"procity_fit_mixer_pmc05pro_rigged_01.glb",
|
||||
"procity_fit_mixer_pmc06pro_01.glb",
|
||||
"procity_fit_mixer_pmc07pro_01.glb",
|
||||
"procity_fit_mixer_pmc08pro_01.glb",
|
||||
"procity_fit_mixer_pmc10_01.glb",
|
||||
"procity_fit_mixer_pmc20_01.glb",
|
||||
"procity_fit_mixer_pmx9000_01.glb",
|
||||
"procity_fit_mixer_shdj1200_01.glb",
|
||||
"procity_fit_mixer_shdj1200_rigged_01.glb",
|
||||
"procity_fit_mixer_ttm54i_01.glb",
|
||||
"procity_fit_mixer_ttm54i_rigged_01.glb",
|
||||
"procity_fit_mos_bin_bottom_01.glb",
|
||||
"procity_fit_mos_bin_lower_mid_01.glb",
|
||||
"procity_fit_mos_bin_top_01.glb",
|
||||
"procity_fit_mos_bin_upper_mid_01.glb",
|
||||
"procity_fit_mos_stack_01.glb",
|
||||
"procity_fit_old_couch_01.glb",
|
||||
"procity_fit_pallet_01.glb",
|
||||
"procity_fit_picnic_table_01.glb",
|
||||
"procity_fit_pie_warmer_01.glb",
|
||||
"procity_fit_planter_01.glb",
|
||||
"procity_fit_plywood_bar_01.glb",
|
||||
"procity_fit_pokie_01.glb",
|
||||
"procity_fit_rack_bin_wooden_01.glb",
|
||||
"procity_fit_raffle_drum_01.glb",
|
||||
"procity_fit_record_crate_01.glb",
|
||||
"procity_fit_rope_post_01.glb",
|
||||
"procity_fit_sampler_mpc3000_01.glb",
|
||||
"procity_fit_sampler_s900_01.glb",
|
||||
"procity_fit_sampler_sp1200_01.glb",
|
||||
"procity_fit_sandwich_board_01.glb",
|
||||
"procity_fit_shelf_board_long_01.glb",
|
||||
"procity_fit_shelf_board_short_01.glb",
|
||||
"procity_fit_sl_deck_1_01.glb",
|
||||
"procity_fit_sl_deck_2_01.glb",
|
||||
"procity_fit_sl_deck_3_01.glb",
|
||||
"procity_fit_sl_deck_4_01.glb",
|
||||
"procity_fit_smoke_machine_01.glb",
|
||||
"procity_fit_speaker_stack_01.glb",
|
||||
"procity_fit_strobe_light_01.glb",
|
||||
"procity_fit_sub_bass_01.glb",
|
||||
"procity_fit_topiary_01.glb",
|
||||
"procity_fit_tub_black_01.glb",
|
||||
"procity_fit_tub_black_long_01.glb",
|
||||
"procity_fit_tub_white_01.glb",
|
||||
"procity_fit_tub_white_long_01.glb",
|
||||
"procity_fit_tubs_3x3_01.glb",
|
||||
"procity_fit_tubs_3x3_long_01.glb",
|
||||
"procity_fit_tubs_4x2_vertical_01.glb",
|
||||
"procity_fit_tubs_4x2_wide_01.glb",
|
||||
"procity_fit_tubs_shelf_long_01.glb",
|
||||
"procity_fit_tubs_shelf_wide_01.glb",
|
||||
"procity_fit_urinal_trough_01.glb",
|
||||
"procity_fit_vinyl_sleeve_01.glb",
|
||||
"procity_fit_wall_shelves_big_01.glb",
|
||||
"procity_fit_wall_shelves_long_01.glb",
|
||||
"procity_fit_wheelie_bin_01.glb",
|
||||
"procity_fit_window_booth_01.glb",
|
||||
"procity_fit_wire_shelf_01.glb",
|
||||
"procity_fit_wooden_rack_long_01.glb",
|
||||
"procity_fit_wooden_rack_wide_01.glb",
|
||||
"procity_fit_work_table_01.glb",
|
||||
"procity_fit_zip_drive_01.glb",
|
||||
"procity_street_aframe_board_01.glb",
|
||||
"procity_street_bench_01.glb",
|
||||
"procity_street_bench_01_hi.glb",
|
||||
|
||||
1113
pipeline/_r41_3dstore_results.json
Normal file
1033
pipeline/_r41_djgear_results.json
Normal file
731
pipeline/_r41_pub_results.json
Normal file
@ -0,0 +1,731 @@
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"src": "ashUrn.glb",
|
||||
"id": "ash_urn",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_ash_urn_01.glb",
|
||||
"tris_before": 198782,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.435,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.32,
|
||||
0.33
|
||||
],
|
||||
"height": 0.75,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_ash_urn_01.png"
|
||||
},
|
||||
{
|
||||
"src": "ashtray.glb",
|
||||
"id": "ashtray_post",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_ashtray_post_01.glb",
|
||||
"tris_before": 198710,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.425,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.41,
|
||||
0.41
|
||||
],
|
||||
"height": 0.95,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_ashtray_post_01.png"
|
||||
},
|
||||
{
|
||||
"src": "bainMarie.glb",
|
||||
"id": "bain_marie",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_bain_marie_01.glb",
|
||||
"tris_before": 184992,
|
||||
"tris": 4958,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.835,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.89,
|
||||
0.76
|
||||
],
|
||||
"height": 0.95,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_bain_marie_01.png"
|
||||
},
|
||||
{
|
||||
"src": "banquette.glb",
|
||||
"id": "banquette",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_banquette_01.glb",
|
||||
"tris_before": 190350,
|
||||
"tris": 4999,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.686,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
1.55,
|
||||
1.02
|
||||
],
|
||||
"height": 0.85,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_banquette_01.png"
|
||||
},
|
||||
{
|
||||
"src": "barrier.glb",
|
||||
"id": "crowd_barrier",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_crowd_barrier_01.glb",
|
||||
"tris_before": 189524,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.603,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
1.97,
|
||||
0.3
|
||||
],
|
||||
"height": 1.1,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_crowd_barrier_01.png"
|
||||
},
|
||||
{
|
||||
"src": "beerUmbrella.glb",
|
||||
"id": "beer_umbrella",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_beer_umbrella_01.glb",
|
||||
"tris_before": 192752,
|
||||
"tris": 4995,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.607,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
2.68,
|
||||
2.65
|
||||
],
|
||||
"height": 2.3,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_beer_umbrella_01.png"
|
||||
},
|
||||
{
|
||||
"src": "binRound.glb",
|
||||
"id": "bin_round",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_bin_round_01.glb",
|
||||
"tris_before": 197576,
|
||||
"tris": 4999,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.4,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.53,
|
||||
0.51
|
||||
],
|
||||
"height": 0.75,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_bin_round_01.png"
|
||||
},
|
||||
{
|
||||
"src": "bistroTable.glb",
|
||||
"id": "bistro_table",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_bistro_table_01.glb",
|
||||
"tris_before": 196995,
|
||||
"tris": 4999,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.382,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
1.18,
|
||||
1.18
|
||||
],
|
||||
"height": 0.92,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_bistro_table_01.png"
|
||||
},
|
||||
{
|
||||
"src": "brokenBasin.glb",
|
||||
"id": "broken_basin",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_broken_basin_01.glb",
|
||||
"tris_before": 197978,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.535,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.59,
|
||||
0.49
|
||||
],
|
||||
"height": 0.6,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_broken_basin_01.png"
|
||||
},
|
||||
{
|
||||
"src": "cableSnake.glb",
|
||||
"id": "cable_snake",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_cable_snake_01.glb",
|
||||
"tris_before": 193623,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.641,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.45,
|
||||
0.47
|
||||
],
|
||||
"height": 0.2,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_cable_snake_01.png"
|
||||
},
|
||||
{
|
||||
"src": "canStack.glb",
|
||||
"id": "can_stack",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_can_stack_01.glb",
|
||||
"tris_before": 196005,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.464,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.38,
|
||||
0.32
|
||||
],
|
||||
"height": 0.45,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_can_stack_01.png"
|
||||
},
|
||||
{
|
||||
"src": "cdj.glb",
|
||||
"id": "cdj_deck",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_cdj_deck_01.glb",
|
||||
"tris_before": 197967,
|
||||
"tris": 4998,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.335,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.63,
|
||||
0.42
|
||||
],
|
||||
"height": 0.15,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_cdj_deck_01.png"
|
||||
},
|
||||
{
|
||||
"src": "champBucket.glb",
|
||||
"id": "champ_bucket",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_champ_bucket_01.glb",
|
||||
"tris_before": 183943,
|
||||
"tris": 4999,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.625,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.24,
|
||||
0.2
|
||||
],
|
||||
"height": 0.35,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_champ_bucket_01.png"
|
||||
},
|
||||
{
|
||||
"src": "distroBox.glb",
|
||||
"id": "distro_box",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_distro_box_01.glb",
|
||||
"tris_before": 193633,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.415,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.63,
|
||||
0.87
|
||||
],
|
||||
"height": 1.2,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_distro_box_01.png"
|
||||
},
|
||||
{
|
||||
"src": "fireExt.glb",
|
||||
"id": "fire_ext",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_fire_ext_01.glb",
|
||||
"tris_before": 198707,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.376,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.34,
|
||||
0.19
|
||||
],
|
||||
"height": 0.6,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_fire_ext_01.png"
|
||||
},
|
||||
{
|
||||
"src": "firepit.glb",
|
||||
"id": "firepit",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_firepit_01.glb",
|
||||
"tris_before": 188041,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.883,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.76,
|
||||
0.56
|
||||
],
|
||||
"height": 0.5,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_firepit_01.png"
|
||||
},
|
||||
{
|
||||
"src": "flightCase.glb",
|
||||
"id": "flight_case",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_flight_case_01.glb",
|
||||
"tris_before": 195452,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.376,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.6,
|
||||
0.36
|
||||
],
|
||||
"height": 0.75,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_flight_case_01.png"
|
||||
},
|
||||
{
|
||||
"src": "jukebox.glb",
|
||||
"id": "jukebox",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_jukebox_01.glb",
|
||||
"tris_before": 184271,
|
||||
"tris": 4999,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.754,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.97,
|
||||
0.41
|
||||
],
|
||||
"height": 1.52,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_jukebox_01.png"
|
||||
},
|
||||
{
|
||||
"src": "keg.glb",
|
||||
"id": "keg",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_keg_01.glb",
|
||||
"tris_before": 441861,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.237,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.4,
|
||||
0.4
|
||||
],
|
||||
"height": 0.6,
|
||||
"max_tex": 1024,
|
||||
"thumb": "thumbs/procity_fit_keg_01.png"
|
||||
},
|
||||
{
|
||||
"src": "loungeChair.glb",
|
||||
"id": "lounge_chair",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_lounge_chair_01.glb",
|
||||
"tris_before": 1319156,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.941,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.69,
|
||||
0.69
|
||||
],
|
||||
"height": 0.8,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_lounge_chair_01.png"
|
||||
},
|
||||
{
|
||||
"src": "marbleSink.glb",
|
||||
"id": "marble_sink",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_marble_sink_01.glb",
|
||||
"tris_before": 199095,
|
||||
"tris": 4998,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.212,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
1.15,
|
||||
0.56
|
||||
],
|
||||
"height": 0.85,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_marble_sink_01.png"
|
||||
},
|
||||
{
|
||||
"src": "oldCouch.glb",
|
||||
"id": "old_couch",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_old_couch_01.glb",
|
||||
"tris_before": 196277,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.468,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
1.8,
|
||||
0.95
|
||||
],
|
||||
"height": 0.85,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_old_couch_01.png"
|
||||
},
|
||||
{
|
||||
"src": "pallet.glb",
|
||||
"id": "pallet",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_pallet_01.glb",
|
||||
"tris_before": 198086,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.307,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.79,
|
||||
0.73
|
||||
],
|
||||
"height": 0.2,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_pallet_01.png"
|
||||
},
|
||||
{
|
||||
"src": "picnicTable.glb",
|
||||
"id": "picnic_table",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_picnic_table_01.glb",
|
||||
"tris_before": 197370,
|
||||
"tris": 4999,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.425,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
1.38,
|
||||
1.41
|
||||
],
|
||||
"height": 0.75,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_picnic_table_01.png"
|
||||
},
|
||||
{
|
||||
"src": "pieWarmer.glb",
|
||||
"id": "pie_warmer",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_pie_warmer_01.glb",
|
||||
"tris_before": 196434,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.317,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.61,
|
||||
0.71
|
||||
],
|
||||
"height": 0.55,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_pie_warmer_01.png"
|
||||
},
|
||||
{
|
||||
"src": "pillar.glb",
|
||||
"id": "concrete_plinth",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_concrete_plinth_01.glb",
|
||||
"tris_before": 190528,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.538,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.76,
|
||||
0.74
|
||||
],
|
||||
"height": 1.2,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_concrete_plinth_01.png"
|
||||
},
|
||||
{
|
||||
"src": "planter.glb",
|
||||
"id": "planter",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_planter_01.glb",
|
||||
"tris_before": 239862,
|
||||
"tris": 4897,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.877,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.98,
|
||||
0.96
|
||||
],
|
||||
"height": 0.55,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_planter_01.png"
|
||||
},
|
||||
{
|
||||
"src": "plywoodBar.glb",
|
||||
"id": "plywood_bar",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_plywood_bar_01.glb",
|
||||
"tris_before": 192907,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.631,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
1.51,
|
||||
0.82
|
||||
],
|
||||
"height": 1.05,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_plywood_bar_01.png"
|
||||
},
|
||||
{
|
||||
"src": "pokie.glb",
|
||||
"id": "pokie",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_pokie_01.glb",
|
||||
"tris_before": 198030,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.291,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.75,
|
||||
0.78
|
||||
],
|
||||
"height": 1.7,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_pokie_01.png"
|
||||
},
|
||||
{
|
||||
"src": "raffleDrum.glb",
|
||||
"id": "raffle_drum",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_raffle_drum_01.glb",
|
||||
"tris_before": 193830,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.685,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.37,
|
||||
0.37
|
||||
],
|
||||
"height": 0.9,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_raffle_drum_01.png"
|
||||
},
|
||||
{
|
||||
"src": "ropePost.glb",
|
||||
"id": "rope_post",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_rope_post_01.glb",
|
||||
"tris_before": 199748,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.276,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.32,
|
||||
0.32
|
||||
],
|
||||
"height": 1.0,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_rope_post_01.png"
|
||||
},
|
||||
{
|
||||
"src": "sandwichBoard.glb",
|
||||
"id": "sandwich_board",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_sandwich_board_01.glb",
|
||||
"tris_before": 198359,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.257,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.58,
|
||||
0.56
|
||||
],
|
||||
"height": 1.0,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_sandwich_board_01.png"
|
||||
},
|
||||
{
|
||||
"src": "smokeMachine.glb",
|
||||
"id": "smoke_machine",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_smoke_machine_01.glb",
|
||||
"tris_before": 194423,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.528,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.17,
|
||||
0.24
|
||||
],
|
||||
"height": 0.38,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_smoke_machine_01.png"
|
||||
},
|
||||
{
|
||||
"src": "stackF1.glb",
|
||||
"id": "speaker_stack",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_speaker_stack_01.glb",
|
||||
"tris_before": 186357,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.599,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.52,
|
||||
0.52
|
||||
],
|
||||
"height": 1.8,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_speaker_stack_01.png"
|
||||
},
|
||||
{
|
||||
"src": "strobe.glb",
|
||||
"id": "strobe_light",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_strobe_light_01.glb",
|
||||
"tris_before": 193392,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.572,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.22,
|
||||
0.24
|
||||
],
|
||||
"height": 0.4,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_strobe_light_01.png"
|
||||
},
|
||||
{
|
||||
"src": "subBass.glb",
|
||||
"id": "sub_bass",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_sub_bass_01.glb",
|
||||
"tris_before": 193581,
|
||||
"tris": 4998,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.531,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.87,
|
||||
0.63
|
||||
],
|
||||
"height": 0.7,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_sub_bass_01.png"
|
||||
},
|
||||
{
|
||||
"src": "topiary.glb",
|
||||
"id": "topiary",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_topiary_01.glb",
|
||||
"tris_before": 415872,
|
||||
"tris": 4999,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.937,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.61,
|
||||
0.61
|
||||
],
|
||||
"height": 1.2,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_topiary_01.png"
|
||||
},
|
||||
{
|
||||
"src": "trough.glb",
|
||||
"id": "urinal_trough",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_urinal_trough_01.glb",
|
||||
"tris_before": 488808,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.065,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.31,
|
||||
0.26
|
||||
],
|
||||
"height": 0.7,
|
||||
"max_tex": 1024,
|
||||
"thumb": "thumbs/procity_fit_urinal_trough_01.png"
|
||||
},
|
||||
{
|
||||
"src": "wheelieBin.glb",
|
||||
"id": "wheelie_bin",
|
||||
"kind": "pub",
|
||||
"file": "procity_fit_wheelie_bin_01.glb",
|
||||
"tris_before": 195084,
|
||||
"tris": 5000,
|
||||
"draws": 1,
|
||||
"nonmanifold": 0.429,
|
||||
"vertex_colour": true,
|
||||
"footprint": [
|
||||
0.72,
|
||||
0.92
|
||||
],
|
||||
"height": 1.1,
|
||||
"max_tex": 0,
|
||||
"thumb": "thumbs/procity_fit_wheelie_bin_01.png"
|
||||
}
|
||||
],
|
||||
"rejects": [
|
||||
{
|
||||
"id": "balustrade",
|
||||
"why": "glass panel + handrail. The remesh lost the handrail entirely and left a lumpy slab with a detached lump \u2014 a transparent thin panel is the worst case for a signed-distance remesh. Eyeballed against balustrade_shot.png; unusable.",
|
||||
"tris_src": 198229
|
||||
},
|
||||
{
|
||||
"id": "cocktail_bar",
|
||||
"why": "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.",
|
||||
"tris_src": 194298
|
||||
},
|
||||
{
|
||||
"id": "dj_plinth",
|
||||
"why": "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.",
|
||||
"tris_src": 197159
|
||||
}
|
||||
],
|
||||
"skipped_staff": [
|
||||
"staffBartender",
|
||||
"staffChef",
|
||||
"staffCleaner",
|
||||
"staffDj",
|
||||
"staffGlassie",
|
||||
"staffSecurity"
|
||||
]
|
||||
}
|
||||
110
pipeline/_r41_publish_list.txt
Normal file
@ -0,0 +1,110 @@
|
||||
procity_fit_aircon_unit_01.glb
|
||||
procity_fit_ash_urn_01.glb
|
||||
procity_fit_ashtray_post_01.glb
|
||||
procity_fit_bain_marie_01.glb
|
||||
procity_fit_banquette_01.glb
|
||||
procity_fit_beam_roof_01.glb
|
||||
procity_fit_beer_umbrella_01.glb
|
||||
procity_fit_bin_big_4x4_01.glb
|
||||
procity_fit_bin_endoffunk_01.glb
|
||||
procity_fit_bin_round_01.glb
|
||||
procity_fit_bistro_table_01.glb
|
||||
procity_fit_broken_basin_01.glb
|
||||
procity_fit_cable_snake_01.glb
|
||||
procity_fit_can_stack_01.glb
|
||||
procity_fit_cart_case_01.glb
|
||||
procity_fit_cart_concorde_01.glb
|
||||
procity_fit_cart_m447_01.glb
|
||||
procity_fit_cart_stanton_01.glb
|
||||
procity_fit_cdj_deck_01.glb
|
||||
procity_fit_champ_bucket_01.glb
|
||||
procity_fit_chiu_bin_01.glb
|
||||
procity_fit_chiu_bin_half_01.glb
|
||||
procity_fit_chiu_bin_low_01.glb
|
||||
procity_fit_chiu_rack_long_01.glb
|
||||
procity_fit_chiu_rack_short_01.glb
|
||||
procity_fit_column_square_01.glb
|
||||
procity_fit_column_wide_01.glb
|
||||
procity_fit_concrete_plinth_01.glb
|
||||
procity_fit_counter_station_01.glb
|
||||
procity_fit_counter_station_side_01.glb
|
||||
procity_fit_crate_blue_01.glb
|
||||
procity_fit_crowd_barrier_01.glb
|
||||
procity_fit_deck_1200_01.glb
|
||||
procity_fit_deck_1200_rigged_01.glb
|
||||
procity_fit_distro_box_01.glb
|
||||
procity_fit_door_shop_01.glb
|
||||
procity_fit_drum_808_01.glb
|
||||
procity_fit_drum_tr1000_01.glb
|
||||
procity_fit_fire_ext_01.glb
|
||||
procity_fit_firepit_01.glb
|
||||
procity_fit_flight_case_01.glb
|
||||
procity_fit_headphones_hd25_01.glb
|
||||
procity_fit_headshell_technics_01.glb
|
||||
procity_fit_jukebox_01.glb
|
||||
procity_fit_keg_01.glb
|
||||
procity_fit_library_trolley_01.glb
|
||||
procity_fit_lounge_chair_01.glb
|
||||
procity_fit_marble_sink_01.glb
|
||||
procity_fit_mixer_mx2200_01.glb
|
||||
procity_fit_mixer_pmc05pro_01.glb
|
||||
procity_fit_mixer_pmc05pro_rigged_01.glb
|
||||
procity_fit_mixer_pmc06pro_01.glb
|
||||
procity_fit_mixer_pmc07pro_01.glb
|
||||
procity_fit_mixer_pmc08pro_01.glb
|
||||
procity_fit_mixer_pmc10_01.glb
|
||||
procity_fit_mixer_pmc20_01.glb
|
||||
procity_fit_mixer_pmx9000_01.glb
|
||||
procity_fit_mixer_shdj1200_01.glb
|
||||
procity_fit_mixer_shdj1200_rigged_01.glb
|
||||
procity_fit_mixer_ttm54i_01.glb
|
||||
procity_fit_mixer_ttm54i_rigged_01.glb
|
||||
procity_fit_mos_bin_bottom_01.glb
|
||||
procity_fit_mos_bin_lower_mid_01.glb
|
||||
procity_fit_mos_bin_top_01.glb
|
||||
procity_fit_mos_bin_upper_mid_01.glb
|
||||
procity_fit_mos_stack_01.glb
|
||||
procity_fit_old_couch_01.glb
|
||||
procity_fit_pallet_01.glb
|
||||
procity_fit_picnic_table_01.glb
|
||||
procity_fit_pie_warmer_01.glb
|
||||
procity_fit_planter_01.glb
|
||||
procity_fit_plywood_bar_01.glb
|
||||
procity_fit_pokie_01.glb
|
||||
procity_fit_rack_bin_wooden_01.glb
|
||||
procity_fit_raffle_drum_01.glb
|
||||
procity_fit_rope_post_01.glb
|
||||
procity_fit_sampler_mpc3000_01.glb
|
||||
procity_fit_sampler_s900_01.glb
|
||||
procity_fit_sampler_sp1200_01.glb
|
||||
procity_fit_sandwich_board_01.glb
|
||||
procity_fit_shelf_board_long_01.glb
|
||||
procity_fit_shelf_board_short_01.glb
|
||||
procity_fit_sl_deck_1_01.glb
|
||||
procity_fit_sl_deck_2_01.glb
|
||||
procity_fit_sl_deck_3_01.glb
|
||||
procity_fit_sl_deck_4_01.glb
|
||||
procity_fit_smoke_machine_01.glb
|
||||
procity_fit_speaker_stack_01.glb
|
||||
procity_fit_strobe_light_01.glb
|
||||
procity_fit_sub_bass_01.glb
|
||||
procity_fit_topiary_01.glb
|
||||
procity_fit_tub_black_01.glb
|
||||
procity_fit_tub_black_long_01.glb
|
||||
procity_fit_tub_white_01.glb
|
||||
procity_fit_tub_white_long_01.glb
|
||||
procity_fit_tubs_3x3_01.glb
|
||||
procity_fit_tubs_3x3_long_01.glb
|
||||
procity_fit_tubs_4x2_vertical_01.glb
|
||||
procity_fit_tubs_4x2_wide_01.glb
|
||||
procity_fit_tubs_shelf_long_01.glb
|
||||
procity_fit_tubs_shelf_wide_01.glb
|
||||
procity_fit_urinal_trough_01.glb
|
||||
procity_fit_vinyl_sleeve_01.glb
|
||||
procity_fit_wall_shelves_big_01.glb
|
||||
procity_fit_wall_shelves_long_01.glb
|
||||
procity_fit_wheelie_bin_01.glb
|
||||
procity_fit_window_booth_01.glb
|
||||
procity_fit_wooden_rack_long_01.glb
|
||||
procity_fit_wooden_rack_wide_01.glb
|
||||
procity_fit_zip_drive_01.glb
|
||||
112
pipeline/_r41_sha1.json
Normal file
@ -0,0 +1,112 @@
|
||||
{
|
||||
"procity_fit_aircon_unit_01.glb": "ce874f7467673f91b81a14ecc60da23411517760",
|
||||
"procity_fit_ash_urn_01.glb": "90205f27ff38968679384f4d04e18eb4315ede1d",
|
||||
"procity_fit_ashtray_post_01.glb": "1bddccaf298394c68f5dae535c11532bab834ca9",
|
||||
"procity_fit_bain_marie_01.glb": "d3482c49cc30dd47e7715e6d9c5340ab579d8f77",
|
||||
"procity_fit_banquette_01.glb": "878ca932e557bc1a2201b315b8dc52d52a6a72b0",
|
||||
"procity_fit_beam_roof_01.glb": "b856a9657443d765a0089cb3754464b09589a62f",
|
||||
"procity_fit_beer_umbrella_01.glb": "c95b6106d5247d931d5c6895f30b135529135d3f",
|
||||
"procity_fit_bin_big_4x4_01.glb": "7f1bfd7708bf6dfadcc67e31e056283d16c3916a",
|
||||
"procity_fit_bin_endoffunk_01.glb": "ffac5eef693afb67ae51d29eed87f8279b68375c",
|
||||
"procity_fit_bin_round_01.glb": "77bec352d4a8a0a90dfc070befcba2cb958b84ed",
|
||||
"procity_fit_bistro_table_01.glb": "b68368b646c58c72520779e1f1b42206dba40282",
|
||||
"procity_fit_broken_basin_01.glb": "bea1a0af949b0b847fe8849ec095f16ab241e8d1",
|
||||
"procity_fit_cable_snake_01.glb": "aa6d2ba9374b141046038e1f35bed3d78fc3fd2d",
|
||||
"procity_fit_can_stack_01.glb": "613b4edfde03bc23d4b0ded4be229883fd2536f3",
|
||||
"procity_fit_cart_case_01.glb": "3aabbc174bc20bd81e9bd303b149e7ef4ba9e88a",
|
||||
"procity_fit_cart_concorde_01.glb": "7d7420387b68045d5b2e1daf708a30af9300c54a",
|
||||
"procity_fit_cart_m447_01.glb": "196f2fb283bce31d4170e006524e21c9a8c69c82",
|
||||
"procity_fit_cart_stanton_01.glb": "19dfb2eaeee522215693bd11282eae6763b09f8c",
|
||||
"procity_fit_cdj_deck_01.glb": "2f99eac26a4020a9906120c44a63d508b31a22fd",
|
||||
"procity_fit_champ_bucket_01.glb": "e0526054c3bfed01b317ee4c6ce2b54e25cc54c6",
|
||||
"procity_fit_chiu_bin_01.glb": "5d46826d073a864167fdfc8137527afc53962244",
|
||||
"procity_fit_chiu_bin_half_01.glb": "ef0f25a0e335962ca82a691b2d54bed78061cfb9",
|
||||
"procity_fit_chiu_bin_low_01.glb": "0d764172e91d81125a3c099e4deaf4cd44353d7b",
|
||||
"procity_fit_chiu_rack_long_01.glb": "fc545f1f54475f51ef4bb15ca26857aab7442fe6",
|
||||
"procity_fit_chiu_rack_short_01.glb": "7a891a7d3fa861dfd3e43784b2b51ea02d57adef",
|
||||
"procity_fit_column_square_01.glb": "4e6d3e0e268aede54b09843696f8be714f3c0a75",
|
||||
"procity_fit_column_wide_01.glb": "c62f08c030b76da90e806691944818e8e2d011ae",
|
||||
"procity_fit_concrete_plinth_01.glb": "6048ad7408377e7ece105c651f8117739cba2c16",
|
||||
"procity_fit_counter_station_01.glb": "f1bd34cb5212759778340e43db18c1d02c653b55",
|
||||
"procity_fit_counter_station_side_01.glb": "78a472e3dd3467091449b9b9d02108f06ca36738",
|
||||
"procity_fit_crate_blue_01.glb": "d0d0315df8b95fe0a455010415075b707c0cd959",
|
||||
"procity_fit_crowd_barrier_01.glb": "363a0f80ce5239e616c5060634ff1bf8f03197d7",
|
||||
"procity_fit_deck_1200_01.glb": "5070c5a85838164a8464a6c46a2ab08eb3fee70e",
|
||||
"procity_fit_deck_1200_rigged_01.glb": "b2175d53dd8413d3aeca748ba6f036dbfb13e638",
|
||||
"procity_fit_distro_box_01.glb": "69b0d169711702d21329dcb9641b7e395f6b7abf",
|
||||
"procity_fit_door_shop_01.glb": "9abed4dda41f27252e35a150afcfb35d162e4418",
|
||||
"procity_fit_drum_808_01.glb": "ee817b3e152b70afea1019917bcee09c9178eb76",
|
||||
"procity_fit_drum_tr1000_01.glb": "4b6868395265325e0d9d3574e101313ff9823140",
|
||||
"procity_fit_fire_ext_01.glb": "4471601a7ec882a93f6742afc1389a1e66652d8f",
|
||||
"procity_fit_firepit_01.glb": "96bfd21bb94b4478f4c3cc18d0d8a99e328b09b2",
|
||||
"procity_fit_flight_case_01.glb": "744378cd2cde5a3fad6b074b82f1a3fb0a2aa020",
|
||||
"procity_fit_headphones_hd25_01.glb": "99c4c8e63fe59aac36bf6f9dd8907c7781b8a0d1",
|
||||
"procity_fit_headshell_technics_01.glb": "1db6e8b04b44e9cb47c61b0bc49867d7f104ca5a",
|
||||
"procity_fit_jukebox_01.glb": "c6efb9277dac136ce75e36c9c2f9b9085228c545",
|
||||
"procity_fit_keg_01.glb": "7a05c10cfe335b3f14047d115f7e64b0518ab580",
|
||||
"procity_fit_library_trolley_01.glb": "892f604c1660daa84bf10c699478c83ca3072473",
|
||||
"procity_fit_lounge_chair_01.glb": "358e0f0624f0d211ece6afa2af1cad395770aa8b",
|
||||
"procity_fit_marble_sink_01.glb": "c38245fc60e991499b59772cbb8ec519a8fa5c49",
|
||||
"procity_fit_mixer_mx2200_01.glb": "32b231ec9fb305bfb1c5b82cc57cfdace32933d6",
|
||||
"procity_fit_mixer_pmc05pro_01.glb": "f115b9caab36fd7401d76c5971c33ce27466364c",
|
||||
"procity_fit_mixer_pmc05pro_rigged_01.glb": "2d3fe7173e7ecbfc99b7c4b9d7bd1840c3a2b006",
|
||||
"procity_fit_mixer_pmc06pro_01.glb": "bb482cf1f4d09b62767f50bf65d9b39d595c6ebe",
|
||||
"procity_fit_mixer_pmc07pro_01.glb": "a48aa36207fe3c1c124f19ba9f616193f3a79609",
|
||||
"procity_fit_mixer_pmc08pro_01.glb": "b17d5c026d6c8450115177eaa9009400c1996f29",
|
||||
"procity_fit_mixer_pmc10_01.glb": "f58e6b5d36cba3001a922ffee0f2b3f44a19e36f",
|
||||
"procity_fit_mixer_pmc20_01.glb": "610876db83ef03ffe0441284cd7788ae17468836",
|
||||
"procity_fit_mixer_pmx9000_01.glb": "0fca9258da288390861a10ac06ba613f8865cb44",
|
||||
"procity_fit_mixer_shdj1200_01.glb": "0ecb07aab2616081e625dba2bc537348a544b837",
|
||||
"procity_fit_mixer_shdj1200_rigged_01.glb": "daa49b9d6be0dc0b100bfdc5eddce47e49564275",
|
||||
"procity_fit_mixer_ttm54i_01.glb": "c7f81962ce3702223ddbe3a117df8a1205b0bde5",
|
||||
"procity_fit_mixer_ttm54i_rigged_01.glb": "181e6ec5f7edefb6b9f71616ce0bdf7eb064fd05",
|
||||
"procity_fit_mos_bin_bottom_01.glb": "b3d032bc63296359424779091a6ddb64d25dd76c",
|
||||
"procity_fit_mos_bin_lower_mid_01.glb": "e2714d078e56e8a6f19a956954ad534c20f67ee9",
|
||||
"procity_fit_mos_bin_top_01.glb": "d92b4cf931d597a453dec68f800e50463ea21ea4",
|
||||
"procity_fit_mos_bin_upper_mid_01.glb": "68a6533f072145bf28bec5ced388a09bec6e716d",
|
||||
"procity_fit_mos_stack_01.glb": "64e9baba429910c504df14b94dd3acfeacccbf34",
|
||||
"procity_fit_old_couch_01.glb": "549663b12ed46f3cec96d942a360d453667bdde9",
|
||||
"procity_fit_pallet_01.glb": "fa4bd1c7e8ff3a7afee43afbf92983eed8040987",
|
||||
"procity_fit_picnic_table_01.glb": "cea78b9c302a1df16d96cbe080a69e3f9b378984",
|
||||
"procity_fit_pie_warmer_01.glb": "1aad547a6115f39c509389046be7a31855b0c710",
|
||||
"procity_fit_planter_01.glb": "a00293155b8bf533aa0f70030f52f00e5d4bb6a5",
|
||||
"procity_fit_plywood_bar_01.glb": "bccb7d15a3f61603e6b37a4a8633adacaeb5a611",
|
||||
"procity_fit_pokie_01.glb": "90c2841ad1935a762929850edbdc4a4a5a9e7da6",
|
||||
"procity_fit_rack_bin_wooden_01.glb": "c410601f91a32272172ec8e79c541f6373dbcaeb",
|
||||
"procity_fit_raffle_drum_01.glb": "42c0b1d6a8c6c5c48a2ccc3d246e7504139eadf0",
|
||||
"procity_fit_rope_post_01.glb": "ba4a0f6abc4058ca4632d4c23b99aa65cd4d99f5",
|
||||
"procity_fit_sampler_mpc3000_01.glb": "8b3def767602846c9056a76d780db3732a48d845",
|
||||
"procity_fit_sampler_s900_01.glb": "9714875be9af91f05c3ecf82631bbaf2fadf15f3",
|
||||
"procity_fit_sampler_sp1200_01.glb": "370c9c3c177b38e194bfe5842e3d61febc36b91e",
|
||||
"procity_fit_sandwich_board_01.glb": "cdf8467eb7f24ae2352ddfa6b68b17c2d34763b6",
|
||||
"procity_fit_shelf_board_long_01.glb": "e46b3556a0be0648908e3297eb3ffdf3e1a66fc0",
|
||||
"procity_fit_shelf_board_short_01.glb": "13826ee78d491d1072aa7a73d4e2f275ac2fe390",
|
||||
"procity_fit_sl_deck_1_01.glb": "4d1e4e23f75711168d479784579d79c2349700bb",
|
||||
"procity_fit_sl_deck_2_01.glb": "cd782ad484627ef2ed560c1564e9c409bb8a5d06",
|
||||
"procity_fit_sl_deck_3_01.glb": "53391d94e52e6a4ddf84096dbbf8f54b6cc92bc3",
|
||||
"procity_fit_sl_deck_4_01.glb": "52b7a1d28a76b5dda3d6eecb1f70eb92c59a6bc3",
|
||||
"procity_fit_smoke_machine_01.glb": "ee952a30c9410ae10fc83cc4388b9f09770b6d2e",
|
||||
"procity_fit_speaker_stack_01.glb": "e08303b63dc8a672d3dd32f6ef4d988a69a3e779",
|
||||
"procity_fit_strobe_light_01.glb": "df345e87313b499bda5dec441aa553fb4ff39732",
|
||||
"procity_fit_sub_bass_01.glb": "aa7508bf758e3e240ed51f5091082ce52cecbc96",
|
||||
"procity_fit_topiary_01.glb": "99ae95024c6a01d01bab56e6151fbd0ef3a88e8b",
|
||||
"procity_fit_tub_black_01.glb": "1e55e597e063aa454e8c359f0ab6bec5a08d7f09",
|
||||
"procity_fit_tub_black_long_01.glb": "3f6c2935d267b5046f0517519e584f5e8eca3e41",
|
||||
"procity_fit_tub_white_01.glb": "32fc23c3bcc35da595f3ccba997c50c1bed1b2ac",
|
||||
"procity_fit_tub_white_long_01.glb": "380c9fed59141d95a7889ccdbabc7b4805a7fc88",
|
||||
"procity_fit_tubs_3x3_01.glb": "72bec67884cf82dc8340382e8a538068691953ba",
|
||||
"procity_fit_tubs_3x3_long_01.glb": "22efd2ba780593adc4bdeaf58ac6e1eae308f24e",
|
||||
"procity_fit_tubs_4x2_vertical_01.glb": "be8d41e153dc57ec187bcd6b244f47ed7c78f99a",
|
||||
"procity_fit_tubs_4x2_wide_01.glb": "770d6a6bc73953b3710a98821794bd3c74145e93",
|
||||
"procity_fit_tubs_shelf_long_01.glb": "d15effbd8d3098c813cd024986002077c370aa0c",
|
||||
"procity_fit_tubs_shelf_wide_01.glb": "e2cf1d1e633ccf4ee172a8c19a92531eacbe959c",
|
||||
"procity_fit_urinal_trough_01.glb": "a857cbdaf157993dd822aa19042a0fb83c0d81b2",
|
||||
"procity_fit_vinyl_sleeve_01.glb": "348c94af5f8363050ba8d516aa009cdcddb237d3",
|
||||
"procity_fit_wall_shelves_big_01.glb": "6dda25529f7095b93e51f760b19a217d0b319432",
|
||||
"procity_fit_wall_shelves_long_01.glb": "c77e3ce595746a07025dd2a8003552de527e8705",
|
||||
"procity_fit_wheelie_bin_01.glb": "5826de87d818ecbce34190fa60829f6ef6ce937b",
|
||||
"procity_fit_window_booth_01.glb": "9fe80b098955b5de24abfb6555fa86da80bf0a24",
|
||||
"procity_fit_wooden_rack_long_01.glb": "4bef5d1069037c2267d04441d83b2680fd16d3a9",
|
||||
"procity_fit_wooden_rack_wide_01.glb": "6757e16e33956ea430d3e29c1206ed940d1861fd",
|
||||
"procity_fit_zip_drive_01.glb": "6b7fece8a4f646e693ec21ae6a1b94c792767da4"
|
||||
}
|
||||
@ -28,6 +28,11 @@ R38_RESULTS = os.path.join(ROOT, "pipeline", "_r38_results.json") # roun
|
||||
# sha1-verified off the public depot, so they are catalogued the same round they were generated
|
||||
# (contrast R38, which held its results file at `.PENDING_PUBLISH` until the push landed).
|
||||
R39_RESULTS = os.path.join(ROOT, "pipeline", "_r39_results.json") # round-39 arcade kit
|
||||
# [R41 §41.2] the three imported libraries: ~/Documents/3dstore (shop fittings),
|
||||
# ~/Documents/not-tonight/art_incoming (pub/venue props), ultra's 3D=models/dj-gear (DJ gear).
|
||||
R41_3DSTORE = os.path.join(ROOT, "pipeline", "_r41_3dstore_results.json")
|
||||
R41_PUB = os.path.join(ROOT, "pipeline", "_r41_pub_results.json")
|
||||
R41_DJGEAR = os.path.join(ROOT, "pipeline", "_r41_djgear_results.json")
|
||||
OUT = os.path.join(ROOT, "web", "assets", "manifest.json")
|
||||
AUDIO = os.path.join(ROOT, "web", "assets", "audio") # round-11 audio pack
|
||||
DEPOT = "https://digalot.fyi/3god"
|
||||
@ -312,6 +317,35 @@ def build():
|
||||
"thumb": "thumbs/" + r["file"].replace(".glb", ".png"),
|
||||
}
|
||||
(furniture if r["file"].startswith("procity_street_") else fittings)[fid] = entry
|
||||
# ---- R41 §41.2: the three imported libraries ------------------------------------------------
|
||||
# These carry their own measured footprint/height (taken 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, and Lane C places on these numbers). They also carry the two facts C needs to spend
|
||||
# the 162-draw interior margin deliberately: `draws` and `tris` per asset.
|
||||
for results_file in (R41_3DSTORE, R41_PUB, R41_DJGEAR):
|
||||
if not os.path.exists(results_file):
|
||||
continue
|
||||
for r in json.load(open(results_file)).get("results", []):
|
||||
fid = r["file"].replace("procity_fit_", "").replace("_01.glb", "")
|
||||
entry = {
|
||||
"file": r["file"],
|
||||
"footprint": r["footprint"],
|
||||
"height": r["height"],
|
||||
"thumb": r["thumb"],
|
||||
"kind": r.get("kind"),
|
||||
"tris": r.get("tris"),
|
||||
"draws": r.get("draws", r.get("draws_after", 1)),
|
||||
"source": r.get("src"),
|
||||
}
|
||||
if r.get("kind") == "djgear":
|
||||
# what C/D need to drive it later: which contract nodes actually ship, and the
|
||||
# full control-name list the source carried (dj-gear/manifest.json).
|
||||
entry["mode"] = r.get("mode")
|
||||
entry["control_nodes"] = r.get("contract_shipped", [])
|
||||
entry["source_controls"] = r.get("named_controls", [])
|
||||
entry["drivable"] = bool(r.get("contract_shipped"))
|
||||
fittings[fid] = entry
|
||||
|
||||
# depot-reuse assets (no local normalize) — e.g. the reused vintage cash register
|
||||
for fid, meta in DEPOT_REUSE.items():
|
||||
fittings.setdefault(fid, dict(meta))
|
||||
|
||||
96
pipeline/check_contract.py
Normal file
@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Assert the DJ-gear named-node contract survives normalization — read from the GLB, not Blender.
|
||||
|
||||
python3 pipeline/check_contract.py SRC_DIR OUT_DIR MAP.json
|
||||
python3 pipeline/check_contract.py --list FILE.glb [FILE.glb ...]
|
||||
|
||||
The contract 90sDJsim's GEAR driver reads (dj-gear/README.md) is five bare node names:
|
||||
|
||||
Platter_SPIN · Arm_YAW · Fader_PITCH · Fader_CROSS · Btn_STARTSTOP
|
||||
|
||||
Integrator ruling 5 exists because `gltf-transform optimize` silently deleted `Fader_CROSS` from a
|
||||
696-node mixer. "Silently" is the operative word, so this check parses the **shipped GLB's** node
|
||||
list rather than asking the tool that produced it — a scene-graph scan inside Blender still sees
|
||||
control EMPTIES that were never exported, and would have reported a contract that the file does
|
||||
not actually carry.
|
||||
|
||||
Verdict per model: PASS = every contract node present in the source is present in the output.
|
||||
FAIL = one or more were lost (the ruling-5 failure mode).
|
||||
N/A = the source carried none (a cartridge has no crossfader).
|
||||
Exit 1 if any FAIL.
|
||||
"""
|
||||
import json, os, struct, sys
|
||||
|
||||
CONTRACT = ["Platter_SPIN", "Arm_YAW", "Fader_PITCH", "Fader_CROSS", "Btn_STARTSTOP"]
|
||||
|
||||
|
||||
def glb_nodes(path):
|
||||
"""Node names from a GLB's JSON chunk. Dependency-free — no Blender, no pygltflib."""
|
||||
d = open(path, "rb").read()
|
||||
if d[:4] != b"glTF":
|
||||
raise ValueError("not a GLB")
|
||||
off = 12
|
||||
while off + 8 <= len(d):
|
||||
clen, ctype = struct.unpack("<II", d[off:off + 8])
|
||||
if ctype == 0x4E4F534A:
|
||||
g = json.loads(d[off + 8: off + 8 + clen].decode("utf-8"))
|
||||
return [n.get("name", "") for n in g.get("nodes", [])]
|
||||
off += 8 + clen
|
||||
raise ValueError("no JSON chunk")
|
||||
|
||||
|
||||
def contract_of(path):
|
||||
names = set(glb_nodes(path))
|
||||
return [c for c in CONTRACT if c in names]
|
||||
|
||||
|
||||
def main():
|
||||
if "--list" in sys.argv:
|
||||
for f in sys.argv[sys.argv.index("--list") + 1:]:
|
||||
print(f"{os.path.basename(f):46} {','.join(contract_of(f)) or '-'}")
|
||||
return 0
|
||||
if len(sys.argv) < 4:
|
||||
print(__doc__)
|
||||
return 2
|
||||
src_dir, out_dir, mapfile = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
pairs = json.load(open(mapfile)) # [{"src":"TTM54i.glb","file":"procity_...glb","mode":...}]
|
||||
|
||||
rows, fails = [], 0
|
||||
for p in pairs:
|
||||
s = os.path.join(src_dir, p["src"])
|
||||
o = os.path.join(out_dir, p["file"])
|
||||
if not (os.path.isfile(s) and os.path.isfile(o)):
|
||||
rows.append({**p, "verdict": "MISSING"})
|
||||
fails += 1
|
||||
continue
|
||||
before, after = contract_of(s), contract_of(o)
|
||||
if not before:
|
||||
verdict = "N/A"
|
||||
elif p.get("mode") == "static":
|
||||
# a static dressing copy is one joined mesh BY DESIGN — it is not claimed to be
|
||||
# drivable, so a missing contract here is expected, not a failure.
|
||||
verdict = "STATIC (contract intentionally dropped)" if not after else "STATIC"
|
||||
elif set(before) <= set(after):
|
||||
verdict = "PASS"
|
||||
else:
|
||||
verdict = "FAIL"
|
||||
fails += 1
|
||||
rows.append({**p, "contract_before": before, "contract_after": after, "verdict": verdict})
|
||||
|
||||
w = max(len(r["file"]) for r in rows) if rows else 40
|
||||
print(f"{'shipped file':{w}} {'mode':7} {'source contract':46} {'shipped contract':46} verdict")
|
||||
print("-" * (w + 110))
|
||||
for r in rows:
|
||||
print(f"{r['file']:{w}} {r.get('mode',''):7} "
|
||||
f"{','.join(r.get('contract_before', [])) or '-':46} "
|
||||
f"{','.join(r.get('contract_after', [])) or '-':46} {r['verdict']}")
|
||||
n_pass = sum(1 for r in rows if r["verdict"] == "PASS")
|
||||
n_na = sum(1 for r in rows if r["verdict"] == "N/A")
|
||||
n_st = sum(1 for r in rows if str(r["verdict"]).startswith("STATIC"))
|
||||
print(f"\nPASS {n_pass} · STATIC {n_st} · N/A {n_na} · FAIL {fails}")
|
||||
json.dump(rows, open(os.path.join(out_dir, "_contract_check.json"), "w"), indent=2)
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
110
pipeline/clips_export.py
Normal file
@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PROCITY R41 §41.1 — stage 1 of 2: Mixamo anim-only FBX -> skeleton-only per-clip GLB.
|
||||
|
||||
Runs INSIDE Blender:
|
||||
|
||||
BL=/Applications/Blender.app/Contents/MacOS/Blender
|
||||
"$BL" --background --python pipeline/clips_export.py -- \
|
||||
--list pipeline/clips_r41.json --fbx <dir of the named FBX> --out <staging dir>
|
||||
|
||||
Stage 2 is `pipeline/clips_pack.py` (pure python, no Blender): it merges the per-clip GLBs into
|
||||
the <=6 grouped GLBs under web/models/clips/ and writes web/assets/motion_manifest.json.
|
||||
|
||||
WHY TWO STAGES. MIRPAMO's roadmap item "multi-clip single-GLB export" is not built yet, and the
|
||||
documented Blender-5 slotted-action hazard (MIRPAMO/CLAUDE.md) bites exactly when you try to move
|
||||
actions between armatures inside one .blend. Exporting one clip per GLB is the proven path — it is
|
||||
how web/models/peds/sit.glb and look.glb were made (generator "Khronos glTF Blender I/O v5.1.20",
|
||||
66 nodes, 0 meshes) — and the merge is then a deterministic glTF-level operation with no Blender
|
||||
version risk at all.
|
||||
|
||||
NO RETARGET IS NEEDED and none is performed: the Mixamo bank and PROCITY's ped fleet are the SAME
|
||||
mixamorig skeleton (rigs.js `_canon` folds mixamorig1/4/... -> mixamorig), which is the whole point
|
||||
of the house "one canonical clip bank" law. MIRPAMO's retargeter is for foreign skeletons (CMU/BVH)
|
||||
and for rigging raw meshes; pointing it at a mixamorig->mixamorig pair would only add error, and it
|
||||
would emit the ped MESH into the clip file, breaking the zero-draw property that makes this round
|
||||
affordable. `make smoke` is still run as the tool ground-truth check (see E-progress.md).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def argv_after_ddash():
|
||||
return sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
|
||||
|
||||
def parse_args(a):
|
||||
out = {}
|
||||
i = 0
|
||||
while i < len(a):
|
||||
out[a[i].lstrip("-")] = a[i + 1]
|
||||
i += 2
|
||||
return out
|
||||
|
||||
|
||||
def reset():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
|
||||
def import_clip(path):
|
||||
bpy.ops.import_scene.fbx(filepath=path, automatic_bone_orientation=False,
|
||||
ignore_leaf_bones=False)
|
||||
arms = [o for o in bpy.data.objects if o.type == "ARMATURE"]
|
||||
if len(arms) != 1:
|
||||
raise RuntimeError("expected exactly 1 armature, got %d in %s" % (len(arms), path))
|
||||
return arms[0]
|
||||
|
||||
|
||||
def export_glb(arm, out_path, anim_name):
|
||||
# name the action == the clip id so the exported animation carries it
|
||||
if arm.animation_data and arm.animation_data.action:
|
||||
arm.animation_data.action.name = anim_name
|
||||
for o in bpy.data.objects:
|
||||
o.select_set(o is arm)
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
kw = dict(filepath=out_path, export_format="GLB",
|
||||
export_animations=True, export_animation_mode="ACTIVE_ACTIONS",
|
||||
export_skins=True, export_yup=True,
|
||||
export_materials="NONE", export_texcoords=False, export_normals=False,
|
||||
export_apply=False, use_selection=True)
|
||||
try:
|
||||
bpy.ops.export_scene.gltf(**kw)
|
||||
except TypeError: # older/newer exporter arg drift — retry minimal
|
||||
bpy.ops.export_scene.gltf(filepath=out_path, export_format="GLB",
|
||||
export_animations=True, use_selection=True)
|
||||
|
||||
|
||||
def main():
|
||||
a = parse_args(argv_after_ddash())
|
||||
sel = json.load(open(a["list"]))
|
||||
fbx_dir, out_dir = a["fbx"], a["out"]
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
results, fails = [], []
|
||||
for gname, g in sel["groups"].items():
|
||||
for c in g["clips"]:
|
||||
src = os.path.join(fbx_dir, c["file"])
|
||||
dst = os.path.join(out_dir, "%s.glb" % c["id"])
|
||||
try:
|
||||
reset()
|
||||
arm = import_clip(src)
|
||||
nbones = len(arm.data.bones)
|
||||
act = arm.animation_data.action if arm.animation_data else None
|
||||
fs, fe = (act.frame_range if act else (0, 0))
|
||||
export_glb(arm, dst, c["id"])
|
||||
results.append(dict(id=c["id"], group=gname, file=c["file"], bones=nbones,
|
||||
frame_start=float(fs), frame_end=float(fe),
|
||||
fps=bpy.context.scene.render.fps,
|
||||
bytes=os.path.getsize(dst)))
|
||||
print("[ok ] %-22s %-64s bones=%d frames=%.0f..%.0f -> %d B"
|
||||
% (c["id"], c["file"], nbones, fs, fe, os.path.getsize(dst)))
|
||||
except Exception as e: # noqa: BLE001
|
||||
fails.append(dict(id=c["id"], file=c["file"], err=repr(e)))
|
||||
print("[FAIL] %-22s %s: %r" % (c["id"], c["file"], e))
|
||||
with open(os.path.join(out_dir, "_export_results.json"), "w") as f:
|
||||
json.dump(dict(ok=results, fail=fails), f, indent=1)
|
||||
print("\n%d exported / %d failed" % (len(results), len(fails)))
|
||||
|
||||
|
||||
main()
|
||||
329
pipeline/clips_pack.py
Normal file
@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PROCITY R41 §41.1 — stage 2 of 2: per-clip skeleton-only GLBs -> <=6 GROUPED clip GLBs.
|
||||
|
||||
python3 pipeline/clips_pack.py --stage <dir from clips_export.py> \
|
||||
--list pipeline/clips_r41.json --out web/models/clips \
|
||||
--manifest web/assets/motion_manifest.json
|
||||
|
||||
Pure python3 stdlib — no Blender, no deps. Operates at the glTF level, which is safe here because
|
||||
every per-clip GLB in the staging dir carries the IDENTICAL 66-node mixamorig skeleton (asserted,
|
||||
not assumed: `--check` fails loudly on any node-name mismatch). Merging is therefore: keep group
|
||||
clip #1's node graph + skin verbatim, then append every other clip's animation with its channel
|
||||
targets remapped by node NAME.
|
||||
|
||||
TRACK FILTER (`--tracks`, default `rot`).
|
||||
PROCITY's consumer is `web/js/citizens/rigs.js` `_rotOnly`, which keeps ONLY `.quaternion` tracks and
|
||||
additionally drops `Hips.quaternion` — every translation and scale track a clip GLB ships is decoded
|
||||
by three.js and then thrown away on the same frame. Shipping them is pure boot cost. So the default
|
||||
packs rotation channels only. `--tracks all` reproduces the unfiltered T/R/S file if a future round
|
||||
needs root motion back (the pipeline is re-runnable from `clips_r41.json` in ~1 minute).
|
||||
|
||||
LOOP SEAM is measured, not asserted: for every clip we compute the maximum per-bone angle between
|
||||
the first and last rotation keyframe (`loopSeamDeg`). Small = the clip cycles cleanly and can be
|
||||
played on repeat; large = it is a one-shot and should be played once and blended out. The manifest
|
||||
carries both the curated `loopable` flag and the measured number so Lane D can second-guess it.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
|
||||
COMP = {5120: ("b", 1), 5121: ("B", 1), 5122: ("h", 2), 5123: ("H", 2),
|
||||
5125: ("I", 4), 5126: ("f", 4)}
|
||||
NCOMP = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- GLB read/write
|
||||
def glb_read(path):
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
if data[:4] != b"glTF":
|
||||
raise ValueError("%s: not a GLB" % path)
|
||||
js, bin_ = None, b""
|
||||
off = 12
|
||||
while off < len(data):
|
||||
ln, ty = struct.unpack_from("<II", data, off)
|
||||
off += 8
|
||||
if ty == 0x4E4F534A:
|
||||
js = json.loads(data[off:off + ln])
|
||||
elif ty == 0x004E4942:
|
||||
bin_ = data[off:off + ln]
|
||||
off += ln
|
||||
return js, bin_
|
||||
|
||||
|
||||
def glb_write(path, js, bin_):
|
||||
j = json.dumps(js, separators=(",", ":")).encode()
|
||||
j += b" " * ((4 - len(j) % 4) % 4)
|
||||
b = bytes(bin_) + b"\0" * ((4 - len(bin_) % 4) % 4)
|
||||
total = 12 + 8 + len(j) + (8 + len(b) if b else 0)
|
||||
out = bytearray()
|
||||
out += struct.pack("<4sII", b"glTF", 2, total)
|
||||
out += struct.pack("<II", len(j), 0x4E4F534A) + j
|
||||
if b:
|
||||
out += struct.pack("<II", len(b), 0x004E4942) + b
|
||||
with open(path, "wb") as f:
|
||||
f.write(out)
|
||||
return total
|
||||
|
||||
|
||||
def acc_bytes(js, bin_, ai):
|
||||
"""Return (tightly packed bytes, count, type, componentType, normalized) for accessor ai."""
|
||||
a = js["accessors"][ai]
|
||||
ct, n = a["componentType"], a["count"]
|
||||
fmt, sz = COMP[ct]
|
||||
nc = NCOMP[a["type"]]
|
||||
elem = sz * nc
|
||||
bv = js["bufferViews"][a["bufferView"]]
|
||||
base = bv.get("byteOffset", 0) + a.get("byteOffset", 0)
|
||||
stride = bv.get("byteStride") or elem
|
||||
if stride == elem:
|
||||
raw = bin_[base:base + elem * n]
|
||||
else:
|
||||
raw = b"".join(bin_[base + i * stride: base + i * stride + elem] for i in range(n))
|
||||
return raw, n, a["type"], ct, a.get("normalized", False), a.get("min"), a.get("max")
|
||||
|
||||
|
||||
def acc_floats(js, bin_, ai):
|
||||
raw, n, ty, ct, _, _, _ = acc_bytes(js, bin_, ai)
|
||||
fmt, sz = COMP[ct]
|
||||
nc = NCOMP[ty]
|
||||
v = struct.unpack("<%d%s" % (n * nc, fmt), raw)
|
||||
return [v[i * nc:(i + 1) * nc] for i in range(n)]
|
||||
|
||||
|
||||
def quantize_rot(raw, count, ct):
|
||||
"""float32 VEC4 quaternions -> int16 normalized VEC4 (glTF-legal animation output).
|
||||
|
||||
Halves the dominant payload. glTF 2.0 dequantises SHORT as max(c/32767, -1.0) and three.js
|
||||
r175 does exactly that in GLTFLoader `_getArrayFromAccessor` (verified in
|
||||
web/vendor/addons/loaders/GLTFLoader.js). Worst-case angular error is ~0.006 deg — three
|
||||
orders of magnitude under anything a 1.7 m citizen can show on screen.
|
||||
"""
|
||||
if ct != 5126:
|
||||
return raw, ct, False
|
||||
v = struct.unpack("<%df" % (count * 4), raw)
|
||||
q = [max(-32767, min(32767, int(round(max(-1.0, min(1.0, x)) * 32767.0)))) for x in v]
|
||||
return struct.pack("<%dh" % len(q), *q), 5122, True
|
||||
|
||||
|
||||
class Sink:
|
||||
"""Accumulates tightly packed accessor payloads into one BIN chunk, deduped by content."""
|
||||
|
||||
def __init__(self):
|
||||
self.bin = bytearray()
|
||||
self.views = []
|
||||
self.accs = []
|
||||
self._seen = {}
|
||||
self.dedup_saved = 0
|
||||
|
||||
def add(self, raw, count, ty, ct, normalized, mn, mx):
|
||||
key = (bytes(raw), count, ty, ct, normalized)
|
||||
if key in self._seen: # identical payload already in this file
|
||||
self.dedup_saved += len(raw)
|
||||
return self._seen[key]
|
||||
while len(self.bin) % 4:
|
||||
self.bin += b"\0"
|
||||
off = len(self.bin)
|
||||
self.bin += raw
|
||||
self.views.append({"buffer": 0, "byteOffset": off, "byteLength": len(raw)})
|
||||
a = {"bufferView": len(self.views) - 1, "componentType": ct,
|
||||
"count": count, "type": ty}
|
||||
if normalized:
|
||||
a["normalized"] = True
|
||||
if mn is not None:
|
||||
a["min"], a["max"] = mn, mx
|
||||
self.accs.append(a)
|
||||
self._seen[key] = len(self.accs) - 1
|
||||
return len(self.accs) - 1
|
||||
|
||||
|
||||
# The bank is not uniform about the mixamorig namespace: 45 of the 46 R41 clips export as
|
||||
# `mixamorig1:Bone`, one (Idle_Smoking) as `mixamorig:Bone` — the exact split the house `_canon`
|
||||
# trick in rigs.js exists to absorb (and the same split already shipped: idle.glb is mixamorig4:,
|
||||
# sit.glb is mixamorig:). Joint NAMES and node ORDER are identical across all 46 (asserted below).
|
||||
# Normalise to the bare `mixamorig:` namespace so the merge is name-exact and every group file
|
||||
# reads the same; three.js sanitises the colon out anyway, so runtime binding is unchanged.
|
||||
_NS = re.compile(r"mixamorig\d*:")
|
||||
|
||||
|
||||
def canon_name(n):
|
||||
return _NS.sub("mixamorig:", n) if n else n
|
||||
|
||||
|
||||
def quat_angle_deg(q0, q1):
|
||||
d = abs(sum(a * b for a, b in zip(q0, q1)))
|
||||
return math.degrees(2.0 * math.acos(max(-1.0, min(1.0, d))))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- pack one group
|
||||
FINGER = re.compile(r"Hand(Thumb|Index|Middle|Ring|Pinky)\d")
|
||||
|
||||
|
||||
def pack_group(gname, clips, stage, out_dir, tracks, quant=True, drop_fingers=False):
|
||||
keep = {"rotation"} if tracks == "rot" else {"rotation", "translation", "scale"}
|
||||
base_js, base_bin = glb_read(os.path.join(stage, "%s.glb" % clips[0]["id"]))
|
||||
for n in base_js["nodes"]:
|
||||
if n.get("name"):
|
||||
n["name"] = canon_name(n["name"])
|
||||
names = [n.get("name") for n in base_js["nodes"]]
|
||||
if len(set(names)) != len(names):
|
||||
raise SystemExit("%s: duplicate node names in base skeleton" % gname)
|
||||
idx_of = {n: i for i, n in enumerate(names)}
|
||||
|
||||
sink = Sink()
|
||||
out = {
|
||||
"asset": {"version": "2.0",
|
||||
"generator": "PROCITY pipeline/clips_pack.py (R41 41.1) from %s"
|
||||
% base_js["asset"].get("generator", "?")},
|
||||
"scene": base_js.get("scene", 0),
|
||||
"scenes": base_js["scenes"],
|
||||
"nodes": base_js["nodes"],
|
||||
"animations": [],
|
||||
"buffers": [{"byteLength": 0}],
|
||||
}
|
||||
if "skins" in base_js:
|
||||
skins = json.loads(json.dumps(base_js["skins"]))
|
||||
for sk in skins:
|
||||
if "inverseBindMatrices" in sk:
|
||||
sk["inverseBindMatrices"] = sink.add(*acc_bytes(base_js, base_bin,
|
||||
sk["inverseBindMatrices"]))
|
||||
out["skins"] = skins
|
||||
|
||||
report = []
|
||||
for c in clips:
|
||||
p = os.path.join(stage, "%s.glb" % c["id"])
|
||||
js, bin_ = glb_read(p)
|
||||
nm = [canon_name(n.get("name")) for n in js["nodes"]]
|
||||
if nm != names:
|
||||
raise SystemExit("%s/%s: skeleton mismatch vs group base (%d vs %d nodes)"
|
||||
% (gname, c["id"], len(nm), len(names)))
|
||||
if len(js.get("animations", [])) != 1:
|
||||
raise SystemExit("%s/%s: expected 1 animation, got %d"
|
||||
% (gname, c["id"], len(js.get("animations", []))))
|
||||
anim = js["animations"][0]
|
||||
smap, chans, samps = {}, [], []
|
||||
dur, seam, nrot = 0.0, 0.0, 0
|
||||
for ch in anim["channels"]:
|
||||
path = ch["target"]["path"]
|
||||
if path not in keep or "node" not in ch["target"]:
|
||||
continue
|
||||
tname = nm[ch["target"]["node"]]
|
||||
if drop_fingers and FINGER.search(tname):
|
||||
continue
|
||||
si = ch["sampler"]
|
||||
if si not in smap:
|
||||
s = anim["samplers"][si]
|
||||
t = acc_floats(js, bin_, s["input"])
|
||||
dur = max(dur, t[-1][0] if t else 0.0)
|
||||
oraw, on, oty, oct, onorm, omn, omx = acc_bytes(js, bin_, s["output"])
|
||||
if path == "rotation":
|
||||
q = acc_floats(js, bin_, s["output"])
|
||||
if len(q) > 1:
|
||||
seam = max(seam, quat_angle_deg(q[0], q[-1]))
|
||||
nrot += 1
|
||||
if quant and s.get("interpolation", "LINEAR") != "CUBICSPLINE":
|
||||
oraw, oct, onorm = quantize_rot(oraw, on, oct)
|
||||
omn = omx = None
|
||||
smap[si] = len(samps)
|
||||
samps.append({"input": sink.add(*acc_bytes(js, bin_, s["input"])),
|
||||
"output": sink.add(oraw, on, oty, oct, onorm, omn, omx),
|
||||
"interpolation": s.get("interpolation", "LINEAR")})
|
||||
chans.append({"sampler": smap[si],
|
||||
"target": {"node": idx_of[tname], "path": path}})
|
||||
out["animations"].append({"name": c["id"], "channels": chans, "samplers": samps})
|
||||
report.append(dict(id=c["id"], duration=round(dur, 4), channels=len(chans),
|
||||
samplers=len(samps), rot_bones=nrot, loopSeamDeg=round(seam, 2)))
|
||||
|
||||
out["bufferViews"] = sink.views
|
||||
out["accessors"] = sink.accs
|
||||
out["buffers"][0]["byteLength"] = len(sink.bin) + ((4 - len(sink.bin) % 4) % 4)
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
path = os.path.join(out_dir, "%s.glb" % gname)
|
||||
size = glb_write(path, out, sink.bin)
|
||||
return path, size, report, len(names), sink.dedup_saved
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--stage", required=True)
|
||||
ap.add_argument("--list", default="pipeline/clips_r41.json")
|
||||
ap.add_argument("--out", default="web/models/clips")
|
||||
ap.add_argument("--manifest", default="web/assets/motion_manifest.json")
|
||||
ap.add_argument("--tracks", choices=["rot", "all"], default="rot")
|
||||
ap.add_argument("--no-quantize", action="store_true",
|
||||
help="keep float32 rotation outputs (default: int16 normalized, ~2x smaller)")
|
||||
ap.add_argument("--drop-fingers", action="store_true",
|
||||
help="drop the 40 finger-bone channels (~half the payload; hands then hold "
|
||||
"their bind pose). Off by default — fidelity kept, lever documented.")
|
||||
ap.add_argument("--loop-seam-max", type=float, default=25.0,
|
||||
help="a clip curated loopable but seaming worse than this is demoted")
|
||||
a = ap.parse_args()
|
||||
|
||||
sel = json.load(open(a.list))
|
||||
man = {
|
||||
"_schema": ("PROCITY motion manifest v1 (R41 41.1). groups{<file>} -> one GLB under "
|
||||
"web/models/clips/, holding N named animations. clips{<clipId>} -> "
|
||||
"{group,category,duration,loopable,source,mixamo,pair?}. Load a group ONCE "
|
||||
"(gltf.animations[] are named by clipId); index by clips[id].group."),
|
||||
"_licence": ("Mixamo (Adobe) via ultra ~/Documents/MOTIONLIB/green/mixamo_full/. GREEN: "
|
||||
"free to use inside games/projects, NO attribution required. Never "
|
||||
"redistribute as a clip pack. R41 ships this licence regime only (ruling 2)."),
|
||||
"_tracks": ("rotation-only" if a.tracks == "rot" else "translation+rotation+scale"),
|
||||
"_encoding": ("rotation outputs are %s; %s"
|
||||
% ("float32" if a.no_quantize else "int16 NORMALIZED (glTF-legal, three.js "
|
||||
"r175 de-normalises in GLTFLoader._getArrayFromAccessor)",
|
||||
"finger channels DROPPED" if a.drop_fingers else
|
||||
"all 65 joints animated incl. fingers")),
|
||||
"_note": ("Skeleton-only, 0 tris, 66 nodes (65 mixamorig joints + Armature) — the same "
|
||||
"byte-shape as web/models/peds/idle.glb. rigs.js `_canon` folds the mixamorigN: "
|
||||
"prefix, `_rotOnly` keeps quaternions and drops Hips.quaternion."),
|
||||
"fps": 30,
|
||||
"groups": {},
|
||||
"clips": {},
|
||||
}
|
||||
total, dedup = 0, 0
|
||||
for gname, g in sel["groups"].items():
|
||||
path, size, rep, nnodes, dd = pack_group(gname, g["clips"], a.stage, a.out, a.tracks,
|
||||
quant=not a.no_quantize,
|
||||
drop_fingers=a.drop_fingers)
|
||||
total += size
|
||||
dedup += dd
|
||||
byid = {r["id"]: r for r in rep}
|
||||
man["groups"]["%s.glb" % gname] = {
|
||||
"category": g["category"], "desc": g["desc"], "clips": [c["id"] for c in g["clips"]],
|
||||
"bytes": size, "nodes": nnodes,
|
||||
"durationSum": round(sum(r["duration"] for r in rep), 3),
|
||||
}
|
||||
for c in g["clips"]:
|
||||
r = byid[c["id"]]
|
||||
loop = bool(c.get("loop")) and r["loopSeamDeg"] <= a.loop_seam_max
|
||||
e = {"group": "%s.glb" % gname, "category": g["category"],
|
||||
"duration": r["duration"], "loopable": loop,
|
||||
"loopSeamDeg": r["loopSeamDeg"],
|
||||
"source": c["file"], "mixamo": c["mixamo"],
|
||||
"channels": r["channels"], "rotBones": r["rot_bones"]}
|
||||
if c.get("loop") and not loop:
|
||||
e["loopDemoted"] = "curated loopable but loop seam %.1f deg > %.1f" % (
|
||||
r["loopSeamDeg"], a.loop_seam_max)
|
||||
if c.get("pair"):
|
||||
e["pair"] = c["pair"]
|
||||
man["clips"][c["id"]] = e
|
||||
print("[pack] %-11s %2d clips %7d B seam max %.1f deg"
|
||||
% (gname, len(rep), size, max(r["loopSeamDeg"] for r in rep)))
|
||||
man["totalBytes"] = total
|
||||
man["clipCount"] = len(man["clips"])
|
||||
man["groupCount"] = len(man["groups"])
|
||||
man["dedupSavedBytes"] = dedup
|
||||
os.makedirs(os.path.dirname(a.manifest), exist_ok=True)
|
||||
with open(a.manifest, "w") as f:
|
||||
json.dump(man, f, indent=1)
|
||||
f.write("\n")
|
||||
print("\n%d clips in %d group GLBs, %d B total (%.2f MB) -> %s"
|
||||
% (man["clipCount"], man["groupCount"], total, total / 1048576.0, a.out))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
89
pipeline/clips_r41.json
Normal file
@ -0,0 +1,89 @@
|
||||
{
|
||||
"_comment": "PROCITY R41 §41.1 motion-library selection. SOURCE: ultra ~/Documents/MOTIONLIB/green/mixamo_full/ ONLY (Mixamo/Adobe, GREEN zone: free to use inside games, no attribution required, never redistribute as clip packs). Ruling 2: one licence regime this round. Every entry names an exact file in that bank's index.json — no wildcards (ruling 3).",
|
||||
"_source_dir": "johnking@100.91.239.7:~/Documents/MOTIONLIB/green/mixamo_full/",
|
||||
"_group_files": "web/models/clips/<group>.glb — 6 groups, one fetch each (ruling 6)",
|
||||
"groups": {
|
||||
"idles": {
|
||||
"category": "idle",
|
||||
"desc": "the clone-army killer: 10 distinct standing idles",
|
||||
"clips": [
|
||||
{"id": "idle_breathing", "file": "Breathing_Idle__c9c6d0d5.fbx", "mixamo": "Breathing Idle", "loop": true},
|
||||
{"id": "idle_standing", "file": "Standing_Idle__c9c831cf.fbx", "mixamo": "Standing Idle", "loop": true},
|
||||
{"id": "idle_look_around", "file": "Idle_Stand_Looking_Around__c9c6cdf7.fbx", "mixamo": "Looking Around", "loop": true},
|
||||
{"id": "idle_weight_shift", "file": "Shifting_Weight_From_Side_To_Side__c9c908f7.fbx","mixamo": "Weight Shift", "loop": true},
|
||||
{"id": "idle_cocky_lean", "file": "Cocky_Lean_Back__c9c90e3c.fbx", "mixamo": "Being Cocky", "loop": false},
|
||||
{"id": "idle_yawn", "file": "Big_Yawn_While_Standing__c9c98361.fbx", "mixamo": "Yawn", "loop": false},
|
||||
{"id": "idle_happy_1", "file": "Happy_Idle_Variation_1__c9ccf37e.fbx", "mixamo": "Happy Idle", "loop": true},
|
||||
{"id": "idle_happy_2", "file": "Happy_Idle_Variation_2__c9ccf445.fbx", "mixamo": "Happy Idle", "loop": true},
|
||||
{"id": "idle_old_man", "file": "Old_Man_Standing_Idle__c9c6c004.fbx", "mixamo": "Old Man Idle", "loop": true},
|
||||
{"id": "idle_smoking", "file": "Idle_Smoking__c9c603fd.fbx", "mixamo": "Smoking", "loop": true}
|
||||
]
|
||||
},
|
||||
"browse": {
|
||||
"category": "browse",
|
||||
"desc": "shop-floor: hold / examine / open / carry / pick up",
|
||||
"clips": [
|
||||
{"id": "browse_hold_idle", "file": "Holding_An_Object_Idle__c9cda11a.fbx", "mixamo": "Holding Idle", "loop": true},
|
||||
{"id": "browse_hold_walk", "file": "Holding_An_Object_While_Walking__c9cda1e7.fbx", "mixamo": "Holding Walk", "loop": true},
|
||||
{"id": "browse_hold_turn_l", "file": "Holding_An_Object_While_Turning_Left__c9cda2af.fbx", "mixamo": "Holding Turn Left", "loop": false, "pair": "browse_hold_turn_r"},
|
||||
{"id": "browse_hold_turn_r", "file": "Holding_An_Object_While_Turning_Right__c9cda378.fbx", "mixamo": "Holding Turn Right", "loop": false, "pair": "browse_hold_turn_l"},
|
||||
{"id": "browse_open_cabinet", "file": "Opening_Cabinet_Door__c9c6d49e.fbx", "mixamo": "Opening", "loop": false, "pair": "browse_close_cabinet"},
|
||||
{"id": "browse_close_cabinet","file": "Closing_Cabinet_Door__c9c6d55d.fbx", "mixamo": "Closing", "loop": false, "pair": "browse_open_cabinet"},
|
||||
{"id": "browse_carry_box", "file": "Carrying_A_Box__c9cd8d90.fbx", "mixamo": "Box Idle", "loop": true},
|
||||
{"id": "browse_pick_up", "file": "Picking_Up_An_Object_With_One_Hand__c9c6c5eb.fbx","mixamo": "Picking Up Object","loop": false}
|
||||
]
|
||||
},
|
||||
"sitlean": {
|
||||
"category": "sitlean",
|
||||
"desc": "benches and shopfronts: 4 seated + 4 leaning",
|
||||
"clips": [
|
||||
{"id": "sit_bench_swing", "file": "Little_Girl_Sitting_On_A_Bench_Swinging_Her_Legs__c9c89165.fbx", "mixamo": "Sitting", "loop": true},
|
||||
{"id": "sit_hands_thighs", "file": "Sitting_In_Chair_Hands_Resting_On_Thighs__c9cb9783.fbx", "mixamo": "Sitting Idle", "loop": true},
|
||||
{"id": "sit_impatient", "file": "Sitting_And_Impatiently_Waiting__c9ccad56.fbx", "mixamo": "Sitting", "loop": true},
|
||||
{"id": "sit_fidget_feet", "file": "Sit_With_Fidgeting_Feet__c9c699a5.fbx", "mixamo": "Sitting", "loop": true},
|
||||
{"id": "lean_wall_postures", "file": "Leaning_Against_A_Wall_With_Various_Postures__c9c695f0.fbx", "mixamo": "One Shoulder Lean", "loop": true},
|
||||
{"id": "lean_wall_leg_up", "file": "Leaning_Square_Against_A_Wall_With_One_Leg_Up__c9c696b0.fbx", "mixamo": "Leaning", "loop": true},
|
||||
{"id": "lean_wall_shoulders", "file": "Leaning_Square_With_Shoulders_Against_A_Wall__c9c69770.fbx", "mixamo": "Leaning", "loop": true},
|
||||
{"id": "lean_wall_walkman", "file": "Little_Girl_Leaning_On_A_Wall_Listening_To_Music__c9c8bace.fbx", "mixamo": "Leaning On A Wall", "loop": true}
|
||||
]
|
||||
},
|
||||
"social": {
|
||||
"category": "social",
|
||||
"desc": "two-person street/shop talk; the handshake pair stays paired",
|
||||
"clips": [
|
||||
{"id": "social_conversation", "file": "General_Conversation__c9c88301.fbx", "mixamo": "Talking", "loop": true},
|
||||
{"id": "social_chat_water", "file": "Having_A_Chat_At_The_Watercooler__c9c9f1c9.fbx", "mixamo": "Talking At Watercooler","loop": true},
|
||||
{"id": "social_greeting", "file": "Greeting_While_Standing__c9c7996a.fbx", "mixamo": "Standing Greeting", "loop": false},
|
||||
{"id": "social_shake_1", "file": "2_People_Shaking_Hands_Part_1_-_Female__c9c9f343.fbx", "mixamo": "Shaking Hands 1", "loop": false, "pair": "social_shake_2"},
|
||||
{"id": "social_shake_2", "file": "2_People_Shaking_Hands_Part_2_-_Male__c9c9f403.fbx", "mixamo": "Shaking Hands 2", "loop": false, "pair": "social_shake_1"},
|
||||
{"id": "social_nod_yes", "file": "Nodding_Head_Yes__c9c90d7b.fbx", "mixamo": "Head Nod Yes", "loop": false},
|
||||
{"id": "social_shake_no", "file": "Shaking_Head_No_Dismissively__c9c90b3c.fbx", "mixamo": "Shaking Head No", "loop": false},
|
||||
{"id": "social_wave", "file": "Waving__c9c5ed32.fbx", "mixamo": "Waving", "loop": false}
|
||||
]
|
||||
},
|
||||
"locomotion": {
|
||||
"category": "locomotion",
|
||||
"desc": "civilian walk + turn set (the named Degree_Turn set is rifle-aiming — substituted, see AUDIT)",
|
||||
"clips": [
|
||||
{"id": "walk_shopping_bag", "file": "Male_Walking_With_Shopping_Bag__c9c64027.fbx", "mixamo": "Walking With Shopping Bag", "loop": true},
|
||||
{"id": "turn_left_90", "file": "Turning_90_Degrees_Left__c9ceef5f.fbx", "mixamo": "Unarmed Turn Left 90", "loop": false, "pair": "turn_right_90"},
|
||||
{"id": "turn_right_90", "file": "Turning_90_Degrees_Right__c9cef01d.fbx", "mixamo": "Unarmed Turn Right 90", "loop": false, "pair": "turn_left_90"},
|
||||
{"id": "turn_walk_180", "file": "Turning_180_Degrees_While_Walking__c9c9c73f.fbx", "mixamo": "Walking Turn 180", "loop": false},
|
||||
{"id": "turn_in_place", "file": "Turning_In_Place__c9c70ef0.fbx", "mixamo": "Turning", "loop": true},
|
||||
{"id": "walk_to_stand", "file": "Walking_To_Standing_Idle__c9c8d966.fbx", "mixamo": "Stop Walking", "loop": false}
|
||||
]
|
||||
},
|
||||
"venue": {
|
||||
"category": "venue",
|
||||
"desc": "pub / gig / record shop",
|
||||
"clips": [
|
||||
{"id": "venue_clap_stand", "file": "Clap_While_Standing__c9c60ef2.fbx", "mixamo": "Clapping", "loop": true},
|
||||
{"id": "venue_clap_seated", "file": "Clap_While_Seated__c9c60fce.fbx", "mixamo": "Clapping", "loop": true},
|
||||
{"id": "venue_cheer", "file": "Male_Cheering_With_Two_Fists_Pump__c9c66dfd.fbx", "mixamo": "Cheering", "loop": true},
|
||||
{"id": "venue_northern_soul", "file": "Basic_Northern_Soul_Step__c9cbe56d.fbx", "mixamo": "Northern Soul Dance","loop": true},
|
||||
{"id": "venue_bartending", "file": "Pouring_And_Serving_Drinks__c9c78799.fbx", "mixamo": "Bartending", "loop": true},
|
||||
{"id": "venue_headphones", "file": "Male_With_Headphones_Listening_To_Music__c9c63329.fbx", "mixamo": "Listening To Music", "loop": true}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
155
pipeline/clips_verify.py
Normal file
@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PROCITY R41 §41.1 — gate: motion_manifest.json <-> web/models/clips/*.glb integrity.
|
||||
|
||||
python3 pipeline/clips_verify.py # 0 errors = green
|
||||
python3 pipeline/clips_verify.py --stage <dir> # + quantisation fidelity vs the float source
|
||||
|
||||
Checks, all measured from the bytes (Lane F: this is the "every referenced clip resolves" gate):
|
||||
1. every group file named in the manifest exists, and its byte size matches the recorded one
|
||||
2. every clip id resolves to a NAMED animation inside its group GLB, and nothing extra rides along
|
||||
3. every group GLB is ZERO-DRAW — 0 meshes, 0 materials, 0 images, no Draco
|
||||
4. every group carries the same 66-node mixamorig skeleton (65 joints + Armature)
|
||||
5. every animation channel targets a node that exists, on a path the manifest declares
|
||||
6. recorded duration matches the max keyframe time in the file
|
||||
7. `_rotOnly` survivability: every clip keeps >=1 non-Hips quaternion track, i.e. the clip still
|
||||
says something after web/js/citizens/rigs.js filters it
|
||||
8. with --stage: max angular error of the int16 rotation quantisation vs the float32 source
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from clips_pack import acc_floats, canon_name, glb_read # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--manifest", default="web/assets/motion_manifest.json")
|
||||
ap.add_argument("--clips", default="web/models/clips")
|
||||
ap.add_argument("--stage", help="staging dir of per-clip float32 GLBs (fidelity check)")
|
||||
a = ap.parse_args()
|
||||
man = json.load(open(a.manifest))
|
||||
err, warn = [], []
|
||||
total = 0
|
||||
skel = None
|
||||
|
||||
for gfile, g in man["groups"].items():
|
||||
p = os.path.join(a.clips, gfile)
|
||||
if not os.path.exists(p):
|
||||
err.append("%s: MISSING" % gfile)
|
||||
continue
|
||||
size = os.path.getsize(p)
|
||||
total += size
|
||||
if size != g["bytes"]:
|
||||
err.append("%s: size %d != manifest %d" % (gfile, size, g["bytes"]))
|
||||
js, bin_ = glb_read(p)
|
||||
if js.get("meshes") or js.get("materials") or js.get("images"):
|
||||
err.append("%s: NOT zero-draw (meshes=%d materials=%d images=%d)"
|
||||
% (gfile, len(js.get("meshes", [])), len(js.get("materials", [])),
|
||||
len(js.get("images", []))))
|
||||
if any("draco" in e.lower() for e in js.get("extensionsUsed", [])):
|
||||
err.append("%s: Draco present" % gfile)
|
||||
names = [canon_name(n.get("name")) for n in js["nodes"]]
|
||||
if skel is None:
|
||||
skel = names
|
||||
elif names != skel:
|
||||
err.append("%s: skeleton differs from %s" % (gfile, list(man["groups"])[0]))
|
||||
joints = [n for n in names if n.startswith("mixamorig:")]
|
||||
if len(names) != 66 or len(joints) != 65:
|
||||
err.append("%s: expected 66 nodes / 65 mixamorig joints, got %d / %d"
|
||||
% (gfile, len(names), len(joints)))
|
||||
anims = {an.get("name"): an for an in js.get("animations", [])}
|
||||
if sorted(anims) != sorted(g["clips"]):
|
||||
err.append("%s: animations %s != manifest clips %s"
|
||||
% (gfile, sorted(anims), sorted(g["clips"])))
|
||||
for cid in g["clips"]:
|
||||
if cid not in anims:
|
||||
continue
|
||||
m = man["clips"][cid]
|
||||
if m["group"] != gfile:
|
||||
err.append("%s: clips[%s].group=%s" % (gfile, cid, m["group"]))
|
||||
an = anims[cid]
|
||||
dur, keeps = 0.0, 0
|
||||
for ch in an["channels"]:
|
||||
ni = ch["target"]["node"]
|
||||
if not (0 <= ni < len(names)):
|
||||
err.append("%s/%s: channel targets node %d out of range" % (gfile, cid, ni))
|
||||
continue
|
||||
if ch["target"]["path"] != "rotation" and man["_tracks"] == "rotation-only":
|
||||
err.append("%s/%s: %s channel in a rotation-only build"
|
||||
% (gfile, cid, ch["target"]["path"]))
|
||||
if ch["target"]["path"] == "rotation" and not names[ni].endswith(":Hips"):
|
||||
keeps += 1
|
||||
t = acc_floats(js, bin_, an["samplers"][ch["sampler"]]["input"])
|
||||
if t:
|
||||
dur = max(dur, t[-1][0])
|
||||
if abs(dur - m["duration"]) > 1e-3:
|
||||
err.append("%s/%s: duration %.4f != manifest %.4f" % (gfile, cid, dur, m["duration"]))
|
||||
if keeps == 0:
|
||||
err.append("%s/%s: nothing survives rigs.js `_rotOnly`" % (gfile, cid))
|
||||
if len(an["channels"]) != m["channels"]:
|
||||
err.append("%s/%s: %d channels != manifest %d"
|
||||
% (gfile, cid, len(an["channels"]), m["channels"]))
|
||||
|
||||
if total != man["totalBytes"]:
|
||||
err.append("totalBytes %d != measured %d" % (man["totalBytes"], total))
|
||||
if len(man["groups"]) > 6:
|
||||
err.append("ruling 6: %d group files > 6" % len(man["groups"]))
|
||||
if man["clipCount"] != len(man["clips"]):
|
||||
err.append("clipCount mismatch")
|
||||
|
||||
# 8. quantisation fidelity against the float32 staging GLBs
|
||||
if a.stage:
|
||||
worst, worst_id = 0.0, None
|
||||
for cid, m in man["clips"].items():
|
||||
sp = os.path.join(a.stage, "%s.glb" % cid)
|
||||
if not os.path.exists(sp):
|
||||
warn.append("no staged source for %s" % cid)
|
||||
continue
|
||||
sj, sb = glb_read(sp)
|
||||
gj_, gb = glb_read(os.path.join(a.clips, m["group"]))
|
||||
snames = [canon_name(n.get("name")) for n in sj["nodes"]]
|
||||
gnames = [canon_name(n.get("name")) for n in gj_["nodes"]]
|
||||
ga = next(x for x in gj_["animations"] if x.get("name") == cid)
|
||||
gmap = {gnames[c["target"]["node"]]: c for c in ga["channels"]
|
||||
if c["target"]["path"] == "rotation"}
|
||||
for ch in sj["animations"][0]["channels"]:
|
||||
if ch["target"]["path"] != "rotation":
|
||||
continue
|
||||
bone = snames[ch["target"]["node"]]
|
||||
if bone not in gmap:
|
||||
continue
|
||||
q0 = acc_floats(sj, sb, sj["animations"][0]["samplers"][ch["sampler"]]["output"])
|
||||
q1 = acc_floats(gj_, gb, ga["samplers"][gmap[bone]["sampler"]]["output"])
|
||||
if len(q0) != len(q1):
|
||||
err.append("%s/%s: keyframe count %d != %d" % (cid, bone, len(q0), len(q1)))
|
||||
continue
|
||||
for x, y in zip(q0, q1):
|
||||
if man["_encoding"].startswith("rotation outputs are int16"):
|
||||
y = tuple(max(v / 32767.0, -1.0) for v in y)
|
||||
d = abs(sum(p * q for p, q in zip(x, y)))
|
||||
d /= (math.sqrt(sum(p * p for p in x)) * math.sqrt(sum(q * q for q in y)) or 1)
|
||||
ang = math.degrees(2 * math.acos(max(-1.0, min(1.0, d))))
|
||||
if ang > worst:
|
||||
worst, worst_id = ang, "%s/%s" % (cid, bone)
|
||||
print("quantisation: worst angular error %.6f deg (%s)" % (worst, worst_id))
|
||||
if worst > 0.05:
|
||||
err.append("quantisation error %.4f deg exceeds 0.05" % worst)
|
||||
|
||||
print("%d clips / %d groups / %d bytes (%.2f MB)"
|
||||
% (man["clipCount"], len(man["groups"]), total, total / 1048576.0))
|
||||
for w in warn:
|
||||
print("WARN %s" % w)
|
||||
for e in err:
|
||||
print("ERROR %s" % e)
|
||||
print("clips_verify: %s (%d errors, %d warnings)"
|
||||
% ("GREEN" if not err else "RED", len(err), len(warn)))
|
||||
return 1 if err else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
231
pipeline/contact_sheet.py
Normal file
@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render a labelled contact sheet of many GLBs in ONE Blender run — the eyeball tool.
|
||||
|
||||
The house rule is "always eyeball the thumbnails", and R41 §41.2 imports ~150 assets across three
|
||||
libraries. One Blender startup per asset would cost more than the work; this lays every GLB out on
|
||||
a grid in a single scene, one camera, one render, with a Blender text label and a metre reference
|
||||
under each. Blender startup is the slow part, so 48 assets cost about what 2 do.
|
||||
|
||||
Every tile is framed to the SAME world scale by default (`--absolute`), which is the only way a
|
||||
contact sheet can answer "is this metre-correct" — a per-tile auto-zoom makes a 0.2 m stool and a
|
||||
2 m fridge look identical, which is exactly how a wrong scale ships. A 1 m grid square sits under
|
||||
each asset as the ruler.
|
||||
|
||||
BL=/Applications/Blender.app/Contents/MacOS/Blender
|
||||
"$BL" --background --python pipeline/contact_sheet.py -- OUT.png GLB [GLB ...]
|
||||
"$BL" --background --python pipeline/contact_sheet.py -- OUT.png --cols 8 --tile 320 DIR/*.glb
|
||||
|
||||
Options (after OUT.png, in any order among the file list):
|
||||
--cols N tiles per row (default: ceil(sqrt(n)))
|
||||
--tile N pixel size per tile (default 256)
|
||||
--fit per-tile auto-zoom instead of one shared scale (looks prettier, lies about size)
|
||||
--title TEXT caption drawn at the top of the sheet
|
||||
"""
|
||||
import bpy, sys, os, math
|
||||
from mathutils import Vector
|
||||
|
||||
ARGV = sys.argv[sys.argv.index("--") + 1:]
|
||||
OUT = ARGV[0]
|
||||
rest = ARGV[1:]
|
||||
COLS = TILE = None
|
||||
FIT = False
|
||||
TITLE = ""
|
||||
files = []
|
||||
i = 0
|
||||
while i < len(rest):
|
||||
a = rest[i]
|
||||
if a == "--cols":
|
||||
COLS = int(rest[i + 1]); i += 2
|
||||
elif a == "--tile":
|
||||
TILE = int(rest[i + 1]); i += 2
|
||||
elif a == "--title":
|
||||
TITLE = rest[i + 1]; i += 2
|
||||
elif a == "--fit":
|
||||
FIT = True; i += 1
|
||||
else:
|
||||
files.append(a); i += 1
|
||||
TILE = TILE or 256
|
||||
files = [f for f in files if f.lower().endswith(".glb")]
|
||||
COLS = COLS or max(1, int(math.ceil(math.sqrt(len(files)))))
|
||||
ROWS = int(math.ceil(len(files) / COLS))
|
||||
|
||||
|
||||
def wipe():
|
||||
for o in list(bpy.data.objects):
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
for c in list(bpy.data.collections):
|
||||
bpy.data.collections.remove(c)
|
||||
|
||||
|
||||
def bounds(objs):
|
||||
lo = Vector((1e9,) * 3); hi = Vector((-1e9,) * 3)
|
||||
for o in objs:
|
||||
if o.type != 'MESH':
|
||||
continue
|
||||
for c in o.bound_box:
|
||||
w = o.matrix_world @ Vector(c)
|
||||
for k in range(3):
|
||||
lo[k] = min(lo[k], w[k]); hi[k] = max(hi[k], w[k])
|
||||
if lo.x > 1e8:
|
||||
return Vector((0, 0, 0)), Vector((0, 0, 0))
|
||||
return lo, hi
|
||||
|
||||
|
||||
def label(text, x, y, size, colour=(0.1, 0.1, 0.1, 1)):
|
||||
cu = bpy.data.curves.new(type="FONT", name="lbl")
|
||||
cu.body = text
|
||||
cu.align_x = 'CENTER'
|
||||
cu.size = size
|
||||
ob = bpy.data.objects.new("lbl", cu)
|
||||
ob.location = (x, y, 0)
|
||||
m = bpy.data.materials.new("lblmat")
|
||||
m.use_nodes = True
|
||||
bsdf = m.node_tree.nodes.get("Principled BSDF")
|
||||
bsdf.inputs["Base Color"].default_value = colour
|
||||
bsdf.inputs["Roughness"].default_value = 1.0
|
||||
ob.data.materials.append(m)
|
||||
bpy.context.scene.collection.objects.link(ob)
|
||||
return ob
|
||||
|
||||
|
||||
def main():
|
||||
wipe()
|
||||
scn = bpy.context.scene
|
||||
THETA = math.radians(58) # camera tilt from straight-down
|
||||
PITCH = 1.0 # world metres between tile centres, ACROSS (x)
|
||||
# Rows are separated along world Y, which the tilted camera foreshortens by cos(THETA) ~ 0.53,
|
||||
# while object HEIGHT projects at sin(THETA) ~ 0.85. Using one pitch for both axes therefore
|
||||
# stacks every tall asset on top of the row behind it. Space the rows out by the reciprocal.
|
||||
PITCH_Y = PITCH / math.cos(THETA)
|
||||
|
||||
# --- import every GLB, park it on the grid ------------------------------------------------
|
||||
placed = []
|
||||
for n, path in enumerate(files):
|
||||
r, c = divmod(n, COLS)
|
||||
before = set(bpy.data.objects)
|
||||
try:
|
||||
bpy.ops.import_scene.gltf(filepath=path)
|
||||
except Exception as e:
|
||||
print(f"IMPORT FAIL {path}: {e}")
|
||||
continue
|
||||
new = [o for o in bpy.data.objects if o not in before]
|
||||
roots = [o for o in new if o.parent is None]
|
||||
meshes = [o for o in new if o.type == 'MESH']
|
||||
if not meshes:
|
||||
for o in new:
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
print(f"NO MESH {path}")
|
||||
continue
|
||||
lo, hi = bounds(meshes)
|
||||
size = max(hi.x - lo.x, hi.y - lo.y, hi.z - lo.z, 1e-4)
|
||||
cx, cy = (lo.x + hi.x) / 2, (lo.y + hi.y) / 2
|
||||
# scale: shared world scale (honest) or per-tile fit (pretty)
|
||||
s = (PITCH * 0.62) / size if FIT else 1.0
|
||||
tx, ty = c * PITCH, -r * PITCH_Y
|
||||
for o in roots:
|
||||
o.scale = (o.scale.x * s, o.scale.y * s, o.scale.z * s)
|
||||
o.location = (o.location.x * s + tx - cx * s,
|
||||
o.location.y * s + ty - cy * s,
|
||||
o.location.z * s - lo.z * s)
|
||||
placed.append((path, size, hi.z - lo.z, meshes))
|
||||
label(os.path.basename(path)[:-4][:26], tx, ty - PITCH * 0.50, PITCH * 0.052)
|
||||
label(f"{hi.x-lo.x:.2f} x {hi.y-lo.y:.2f} x {hi.z-lo.z:.2f} m",
|
||||
tx, ty - PITCH * 0.60, PITCH * 0.042, (0.30, 0.30, 0.38, 1))
|
||||
|
||||
# --- a 1 m reference square under every tile (the ruler) -----------------------------------
|
||||
if not FIT:
|
||||
for n in range(len(placed)):
|
||||
r, c = divmod(n, COLS)
|
||||
bpy.ops.mesh.primitive_plane_add(size=1.0, location=(c * PITCH, -r * PITCH_Y, -0.002))
|
||||
p = bpy.context.object
|
||||
m = bpy.data.materials.new("grid")
|
||||
m.use_nodes = True
|
||||
b = m.node_tree.nodes.get("Principled BSDF")
|
||||
b.inputs["Base Color"].default_value = (0.80, 0.80, 0.84, 1)
|
||||
b.inputs["Roughness"].default_value = 1.0
|
||||
p.data.materials.append(m)
|
||||
|
||||
# --- backdrop, light, camera ----------------------------------------------------------------
|
||||
W = (COLS - 1) * PITCH
|
||||
H = (ROWS - 1) * PITCH_Y
|
||||
tall = max((t[2] for t in placed), default=1.0)
|
||||
bpy.ops.mesh.primitive_plane_add(size=max(W, H) * 4 + 40, location=(W / 2, -H / 2, -0.004))
|
||||
bg = bpy.context.object
|
||||
m = bpy.data.materials.new("bg")
|
||||
m.use_nodes = True
|
||||
b = m.node_tree.nodes.get("Principled BSDF")
|
||||
b.inputs["Base Color"].default_value = (0.94, 0.94, 0.96, 1)
|
||||
b.inputs["Roughness"].default_value = 1.0
|
||||
bg.data.materials.append(m)
|
||||
|
||||
scn.world.use_nodes = True
|
||||
scn.world.node_tree.nodes["Background"].inputs[0].default_value = (0.9, 0.92, 0.96, 1)
|
||||
scn.world.node_tree.nodes["Background"].inputs[1].default_value = 0.85
|
||||
bpy.ops.object.light_add(type='SUN', location=(W / 2 + 3, -H / 2 - 4, 8 + tall))
|
||||
sun = bpy.context.object
|
||||
sun.data.energy = 1.9
|
||||
sun.rotation_euler = (math.radians(52), 0, math.radians(38))
|
||||
|
||||
# A CONTACT SHEET IS A COLOUR JUDGEMENT, so it must not go through a film emulation.
|
||||
# Blender's default view transform (AgX in 5.x) desaturates hard: the first pass of the
|
||||
# art_incoming props read as bleached white and looked like the vertex-colour transfer had
|
||||
# failed, when measuring COLOR_0 in the exported GLBs showed the means intact to within 3%.
|
||||
# The tool was lying, not the asset. Standard = what you see is what is in the file.
|
||||
scn.view_settings.view_transform = 'Standard'
|
||||
scn.view_settings.look = 'None'
|
||||
|
||||
# Aim the camera AT the grid centre. A camera rotated theta about X looks along
|
||||
# (0, sin theta, -cos theta), so it must sit back along the reverse of that ray from the target
|
||||
# -- an arbitrary offset misses by t*sin(theta) and renders an empty backdrop (this tool's
|
||||
# first take came back blank exactly that way).
|
||||
tgt = Vector((W / 2, -H / 2 - PITCH * 0.15, PITCH * 0.20))
|
||||
cam_d = (max(W, H) + tall) * 3.0 + 10.0
|
||||
bpy.ops.object.camera_add(location=(tgt.x,
|
||||
tgt.y - cam_d * math.sin(THETA),
|
||||
tgt.z + cam_d * math.cos(THETA)))
|
||||
cam = bpy.context.object
|
||||
cam.data.type = 'ORTHO'
|
||||
cam.data.clip_end = cam_d * 4
|
||||
cam.rotation_euler = (THETA, 0, 0)
|
||||
scn.camera = cam
|
||||
|
||||
# World extent the frame must contain. Screen-vertical mixes the ground span BETWEEN row
|
||||
# centres ((ROWS-1) gaps, each PITCH_Y foreshortened by cos = PITCH) with the tallest object's
|
||||
# height (x sin) and one label gutter. Counting a gutter PER ROW instead of between rows
|
||||
# over-estimates badly at ROWS=1 and renders everything as postage stamps.
|
||||
need_w = COLS * PITCH * 1.02
|
||||
need_h = (ROWS - 1) * PITCH + tall * math.sin(THETA) + PITCH * 0.75 \
|
||||
+ (PITCH * 0.42 if TITLE else 0.0)
|
||||
res_x = int(COLS * TILE)
|
||||
res_y = max(TILE // 2, int(res_x * need_h / need_w))
|
||||
# ortho_scale governs the LARGER pixel dimension; solve so BOTH fit.
|
||||
if res_x >= res_y:
|
||||
cam.data.ortho_scale = max(need_w, need_h * res_x / res_y)
|
||||
else:
|
||||
cam.data.ortho_scale = max(need_h, need_w * res_y / res_x)
|
||||
|
||||
if TITLE:
|
||||
# well clear of row 0: tall assets project upward in screen space from y=0, so a title
|
||||
# parked at +0.6 m lands on top of them.
|
||||
label(TITLE, W / 2, PITCH_Y * 0.72 + tall, PITCH * 0.085, (0.05, 0.05, 0.1, 1))
|
||||
|
||||
# Blender 5.x renamed the EEVEE enum back to BLENDER_EEVEE; keep both spellings working.
|
||||
engines = scn.bl_rna.properties["render"].fixed_type.properties["engine"].enum_items.keys() \
|
||||
if False else ('BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT')
|
||||
for e in engines:
|
||||
try:
|
||||
scn.render.engine = e
|
||||
break
|
||||
except TypeError:
|
||||
continue
|
||||
scn.render.film_transparent = False
|
||||
scn.render.resolution_x = res_x
|
||||
scn.render.resolution_y = res_y
|
||||
scn.render.image_settings.file_format = 'PNG'
|
||||
scn.render.filepath = os.path.abspath(OUT)
|
||||
os.makedirs(os.path.dirname(os.path.abspath(OUT)), exist_ok=True)
|
||||
bpy.ops.render.render(write_still=True)
|
||||
print(f"SHEET {OUT} {len(placed)}/{len(files)} rendered {COLS}x{ROWS}")
|
||||
|
||||
|
||||
main()
|
||||
245
pipeline/merge_prims.py
Normal file
@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Collapse a multi-material GLB to ONE primitive per mesh by baking baseColorFactor -> COLOR_0.
|
||||
|
||||
WHY THIS EXISTS (R41 §41.2, the 3dstore import)
|
||||
-----------------------------------------------
|
||||
`~/Documents/3dstore/` was handed over as "45 of 46 pass the house law as-is". On tris, scale,
|
||||
Draco and textures that is true. On the number that actually decides whether Lane C can place them
|
||||
it is not: **14 of the files carry 16 materials on a single mesh**, one flat colour per record
|
||||
sleeve (`Mat_Sleeve_<hex>`). glTF gives each material its own primitive, and three.js
|
||||
`GLTFLoader` instantiates one `Mesh` per primitive — so a 1,020-tri "Black Tub" costs **16 draw
|
||||
calls**, not one. The interior draw margin is 162. Ten tubs would be the entire budget.
|
||||
|
||||
The materials are pure `baseColorFactor` — no textures, no UVs, nothing but a flat colour and a
|
||||
metallic/roughness pair. That is exactly what a vertex colour reproduces. So we merge every
|
||||
primitive of a mesh into one, write each source primitive's `baseColorFactor` into a `COLOR_0`
|
||||
attribute on its own vertices, and emit a single white material with the dominant primitive's
|
||||
PBR values. glTF multiplies `COLOR_0` by `baseColorFactor`, and both live in LINEAR space, so a
|
||||
white factor times the original linear colour is **numerically the original colour**. Byte-exact
|
||||
look, 16 draws -> 1.
|
||||
|
||||
This is deliberately NOT `gltf-transform optimize` (ruling 5) and NOT a Blender re-export: the
|
||||
geometry is already correct and every extra tool is another chance to move it. We rewrite the
|
||||
container and touch nothing but the material binding.
|
||||
|
||||
python3 pipeline/merge_prims.py IN.glb OUT.glb
|
||||
python3 pipeline/merge_prims.py --check OUT.glb # report prims/draws/materials
|
||||
"""
|
||||
import json, os, struct, sys
|
||||
|
||||
COMPONENT = {5120: ("b", 1), 5121: ("B", 1), 5122: ("h", 2),
|
||||
5123: ("H", 2), 5125: ("I", 4), 5126: ("f", 4)}
|
||||
NCOMP = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16}
|
||||
|
||||
|
||||
# ── container io ─────────────────────────────────────────────────────────────────────────────────
|
||||
def read_glb(path):
|
||||
d = open(path, "rb").read()
|
||||
if d[:4] != b"glTF":
|
||||
raise ValueError(f"{path}: not a GLB")
|
||||
off, gltf, binc = 12, None, b""
|
||||
while off + 8 <= len(d):
|
||||
clen, ctype = struct.unpack("<II", d[off:off + 8])
|
||||
chunk = d[off + 8: off + 8 + clen]
|
||||
if ctype == 0x4E4F534A:
|
||||
gltf = json.loads(chunk.decode("utf-8"))
|
||||
elif ctype == 0x004E4942:
|
||||
binc = chunk
|
||||
off += 8 + clen
|
||||
return gltf, binc
|
||||
|
||||
|
||||
def write_glb(path, gltf, binc):
|
||||
js = json.dumps(gltf, separators=(",", ":")).encode("utf-8")
|
||||
js += b" " * ((4 - len(js) % 4) % 4)
|
||||
binc += b"\x00" * ((4 - len(binc) % 4) % 4)
|
||||
total = 12 + 8 + len(js) + (8 + len(binc) if binc else 0)
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"glTF" + struct.pack("<II", 2, total))
|
||||
f.write(struct.pack("<II", len(js), 0x4E4F534A) + js)
|
||||
if binc:
|
||||
f.write(struct.pack("<II", len(binc), 0x004E4942) + binc)
|
||||
|
||||
|
||||
def read_accessor(gltf, binc, idx):
|
||||
"""Return a list of tuples (or scalars) for accessor `idx`, honouring byteStride."""
|
||||
a = gltf["accessors"][idx]
|
||||
fmt, size = COMPONENT[a["componentType"]]
|
||||
n = NCOMP[a["type"]]
|
||||
count = a["count"]
|
||||
if "bufferView" not in a: # all-zero accessor (spec-legal)
|
||||
return [(0,) * n for _ in range(count)] if n > 1 else [0] * count
|
||||
bv = gltf["bufferViews"][a["bufferView"]]
|
||||
base = bv.get("byteOffset", 0) + a.get("byteOffset", 0)
|
||||
stride = bv.get("byteStride") or (size * n)
|
||||
out = []
|
||||
for i in range(count):
|
||||
off = base + i * stride
|
||||
v = struct.unpack_from("<" + fmt * n, binc, off)
|
||||
out.append(v if n > 1 else v[0])
|
||||
return out
|
||||
|
||||
|
||||
def norm_component(v, ctype):
|
||||
"""glTF normalized-integer -> float, per the spec's exact divisors."""
|
||||
if ctype == 5121:
|
||||
return v / 255.0
|
||||
if ctype == 5123:
|
||||
return v / 65535.0
|
||||
if ctype == 5120:
|
||||
return max(v / 127.0, -1.0)
|
||||
if ctype == 5122:
|
||||
return max(v / 32767.0, -1.0)
|
||||
return float(v)
|
||||
|
||||
|
||||
# ── the merge ────────────────────────────────────────────────────────────────────────────────────
|
||||
def merge(gltf, binc):
|
||||
"""Rewrite every multi-primitive mesh as one primitive with baked COLOR_0. Returns a report."""
|
||||
mats = gltf.get("materials", [])
|
||||
new_bin = bytearray()
|
||||
new_views, new_accs = [], []
|
||||
|
||||
def add_accessor(data, fmt, size, ncomp, atype, ctype, minmax=False, target=None):
|
||||
while len(new_bin) % 4:
|
||||
new_bin.append(0)
|
||||
off = len(new_bin)
|
||||
for v in data:
|
||||
new_bin.extend(struct.pack("<" + fmt * ncomp, *(v if ncomp > 1 else (v,))))
|
||||
bv = {"buffer": 0, "byteOffset": off, "byteLength": len(new_bin) - off}
|
||||
if target:
|
||||
bv["target"] = target
|
||||
new_views.append(bv)
|
||||
acc = {"bufferView": len(new_views) - 1, "componentType": ctype,
|
||||
"count": len(data), "type": atype}
|
||||
if minmax and data:
|
||||
cols = list(zip(*data)) if ncomp > 1 else [data]
|
||||
acc["min"] = [min(c) for c in cols]
|
||||
acc["max"] = [max(c) for c in cols]
|
||||
new_accs.append(acc)
|
||||
return len(new_accs) - 1
|
||||
|
||||
report = []
|
||||
for mi, mesh in enumerate(gltf.get("meshes", [])):
|
||||
prims = mesh.get("primitives", [])
|
||||
if not prims:
|
||||
continue
|
||||
pos, nrm, col, idx = [], [], [], []
|
||||
tri_by_prim = []
|
||||
for p in prims:
|
||||
attrs = p["attributes"]
|
||||
P = read_accessor(gltf, binc, attrs["POSITION"])
|
||||
N = (read_accessor(gltf, binc, attrs["NORMAL"])
|
||||
if "NORMAL" in attrs else [(0.0, 1.0, 0.0)] * len(P))
|
||||
# existing COLOR_0 multiplies the factor (spec); normalize ints first
|
||||
if "COLOR_0" in attrs:
|
||||
ca = gltf["accessors"][attrs["COLOR_0"]]
|
||||
raw = read_accessor(gltf, binc, attrs["COLOR_0"])
|
||||
ct, nc = ca["componentType"], NCOMP[ca["type"]]
|
||||
existing = [tuple(norm_component(x, ct) for x in (v if nc > 1 else (v,)))
|
||||
for v in raw]
|
||||
existing = [(e + (1.0,)) if len(e) == 3 else e for e in existing]
|
||||
else:
|
||||
existing = None
|
||||
m = mats[p["material"]] if p.get("material") is not None and mats else {}
|
||||
bcf = (m.get("pbrMetallicRoughness", {}) or {}).get("baseColorFactor", [1, 1, 1, 1])
|
||||
bcf = tuple(float(x) for x in (list(bcf) + [1, 1, 1, 1])[:4])
|
||||
base = len(pos)
|
||||
pos.extend(P)
|
||||
nrm.extend(N)
|
||||
if existing:
|
||||
col.extend(tuple(bcf[k] * existing[i][k] for k in range(4))
|
||||
for i in range(len(P)))
|
||||
else:
|
||||
col.extend([bcf] * len(P))
|
||||
if "indices" in p:
|
||||
I = read_accessor(gltf, binc, p["indices"])
|
||||
else:
|
||||
I = list(range(len(P)))
|
||||
idx.extend(i + base for i in I)
|
||||
tri_by_prim.append((len(I) // 3, p.get("material")))
|
||||
|
||||
# PBR donor = the material covering the most triangles, so the dominant surface keeps its
|
||||
# exact metallic/roughness. (On the 3dstore tubs that is a sleeve: metallic 0, rough 0.6.)
|
||||
donor_mat = max(tri_by_prim, key=lambda t: t[0])[1] if tri_by_prim else None
|
||||
donor = mats[donor_mat] if donor_mat is not None and mats else {}
|
||||
dp = donor.get("pbrMetallicRoughness", {}) or {}
|
||||
|
||||
ctype = 5125 if len(pos) > 65535 else 5123
|
||||
fmt = "I" if ctype == 5125 else "H"
|
||||
i_acc = add_accessor(idx, fmt, 4 if ctype == 5125 else 2, 1, "SCALAR", ctype,
|
||||
target=34963)
|
||||
p_acc = add_accessor(pos, "f", 4, 3, "VEC3", 5126, minmax=True, target=34962)
|
||||
n_acc = add_accessor(nrm, "f", 4, 3, "VEC3", 5126, target=34962)
|
||||
c_acc = add_accessor(col, "f", 4, 4, "VEC4", 5126, target=34962)
|
||||
|
||||
mesh["primitives"] = [{
|
||||
"attributes": {"POSITION": p_acc, "NORMAL": n_acc, "COLOR_0": c_acc},
|
||||
"indices": i_acc, "material": 0, "mode": 4,
|
||||
}]
|
||||
report.append({"mesh": mi, "name": mesh.get("name"), "prims_before": len(prims),
|
||||
"prims_after": 1, "tris": len(idx) // 3, "verts": len(pos)})
|
||||
|
||||
gltf["materials"] = [{
|
||||
"name": "Merged_VertexColor",
|
||||
"doubleSided": any(m.get("doubleSided") for m in mats) if mats else False,
|
||||
"pbrMetallicRoughness": {
|
||||
"baseColorFactor": [1, 1, 1, 1], # identity: COLOR_0 carries it all
|
||||
"metallicFactor": dp.get("metallicFactor", 1.0),
|
||||
"roughnessFactor": dp.get("roughnessFactor", 1.0),
|
||||
},
|
||||
}]
|
||||
gltf["bufferViews"] = new_views
|
||||
gltf["accessors"] = new_accs
|
||||
gltf["buffers"] = [{"byteLength": len(new_bin)}]
|
||||
for k in ("images", "textures", "samplers"): # nothing references them now
|
||||
gltf.pop(k, None)
|
||||
gltf["extensionsUsed"] = [e for e in gltf.get("extensionsUsed", [])
|
||||
if "draco" not in e.lower()] or None
|
||||
if not gltf["extensionsUsed"]:
|
||||
gltf.pop("extensionsUsed")
|
||||
return report, bytes(new_bin)
|
||||
|
||||
|
||||
def check(path):
|
||||
gltf, _ = read_glb(path)
|
||||
meshes = gltf.get("meshes", [])
|
||||
nodes = gltf.get("nodes", [])
|
||||
draws = sum(len(meshes[n["mesh"]].get("primitives", [])) for n in nodes if "mesh" in n)
|
||||
prims = sum(len(m.get("primitives", [])) for m in meshes)
|
||||
tris = 0
|
||||
for m in meshes:
|
||||
for p in m.get("primitives", []):
|
||||
a = gltf["accessors"][p["indices"]] if "indices" in p else \
|
||||
gltf["accessors"][p["attributes"]["POSITION"]]
|
||||
tris += a["count"] // 3
|
||||
return {"file": os.path.basename(path), "draws": draws, "prims": prims,
|
||||
"materials": len(gltf.get("materials", [])), "tris": tris,
|
||||
"images": len(gltf.get("images", []))}
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if args and args[0] == "--check":
|
||||
for f in args[1:]:
|
||||
print(check(f))
|
||||
return 0
|
||||
if len(args) != 2:
|
||||
print(__doc__)
|
||||
return 2
|
||||
src, dst = args
|
||||
gltf, binc = read_glb(src)
|
||||
before = check(src)
|
||||
rep, nb = merge(gltf, binc)
|
||||
os.makedirs(os.path.dirname(os.path.abspath(dst)), exist_ok=True)
|
||||
write_glb(dst, gltf, nb)
|
||||
after = check(dst)
|
||||
print(f"{os.path.basename(src):42} draws {before['draws']:>3} -> {after['draws']:>2} "
|
||||
f"| mats {before['materials']:>2} -> {after['materials']} "
|
||||
f"| tris {before['tris']} -> {after['tris']}"
|
||||
f"{' *** TRI DRIFT ***' if before['tris'] != after['tris'] else ''}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -141,6 +141,150 @@ def clean_mesh(obj):
|
||||
return 0
|
||||
|
||||
|
||||
def nonmanifold_ratio(obj):
|
||||
"""Fraction of vertices on a non-manifold edge — the number that predicts whether COLLAPSE
|
||||
decimation can work at all. Measured, not guessed: see remesh_shellsoup()."""
|
||||
n = len(obj.data.vertices)
|
||||
if not n:
|
||||
return 0.0
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='DESELECT')
|
||||
bpy.ops.mesh.select_non_manifold()
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
return sum(1 for v in obj.data.vertices if v.select) / n
|
||||
|
||||
|
||||
def remesh_shellsoup(obj, budget, voxel=None, thick=0.0):
|
||||
"""Voxel-remesh a shell-soup mesh to a manifold surface, decimate to budget, and carry the
|
||||
source's vertex colours across.
|
||||
|
||||
[R41 §41.2] `art_incoming`'s 48 pub props are trimesh output at ~200k tris that COLLAPSE
|
||||
decimation cannot touch: 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 the same at 93%
|
||||
non-manifold; pokie, at 29%, decimated 198k -> 47k normally. Decimate cannot merge across
|
||||
overlapping shells, so the fix is to stop trying: rebuild the topology with a voxel remesh
|
||||
(OpenVDB, always manifold), then decimate THAT, then transfer the colour back.
|
||||
|
||||
This is the same "throw the topology away" move as bake_lowpoly.py, minus the Cycles bake —
|
||||
these props carry COLOR_0 and no textures, so a vertex-colour transfer keeps them at one
|
||||
material and zero texture bytes instead of adding a 1024px atlas each.
|
||||
"""
|
||||
src = obj
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
src.select_set(True)
|
||||
bpy.context.view_layer.objects.active = src
|
||||
bpy.ops.object.duplicate()
|
||||
lp = bpy.context.view_layer.objects.active # the copy becomes the low-poly
|
||||
lp.name = "LP"
|
||||
|
||||
dims = lp.dimensions
|
||||
v = voxel or max(max(dims) / 150.0, 1e-4)
|
||||
|
||||
# A voxel remesh is a signed-distance field: it needs a VOLUME. Feed it a thin open shell and
|
||||
# it returns a lattice of holes — the keg (a thin-walled scan) came back as a wire cage at
|
||||
# every voxel size tried, while solid props like the couch remeshed cleanly. Giving the shell
|
||||
# a wall thicker than one voxel first is what makes the field well-defined.
|
||||
if thick:
|
||||
sm = lp.modifiers.new("solid", 'SOLIDIFY')
|
||||
sm.thickness = thick if thick > 0 else v * 2.5
|
||||
sm.offset = 0.0
|
||||
sm.use_even_offset = False
|
||||
bpy.ops.object.modifier_apply(modifier=sm.name)
|
||||
|
||||
rm = lp.modifiers.new("remesh", 'REMESH')
|
||||
rm.mode = 'VOXEL'
|
||||
rm.voxel_size = v
|
||||
rm.adaptivity = 0.0
|
||||
bpy.ops.object.modifier_apply(modifier=rm.name)
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.quads_convert_to_tris()
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
n = len(lp.data.polygons)
|
||||
if n > budget:
|
||||
dm = lp.modifiers.new("dec", 'DECIMATE')
|
||||
dm.ratio = max(0.002, budget / n)
|
||||
bpy.ops.object.modifier_apply(modifier=dm.name)
|
||||
|
||||
# Carry colour (and UVs, if the source is textured) across from the dense original by
|
||||
# nearest-face interpolation. The destination layers must be CREATED first with
|
||||
# `datalayout_transfer` — a Data Transfer modifier applied without it silently transfers
|
||||
# nothing and the prop exports colourless, which is exactly how the first run of this came
|
||||
# back "colour LOST" on all six.
|
||||
transferred = False
|
||||
src_cols = list(src.data.color_attributes)
|
||||
src_uvs = list(src.data.uv_layers)
|
||||
if src_cols or src_uvs:
|
||||
try:
|
||||
dt = lp.modifiers.new("xfer", 'DATA_TRANSFER')
|
||||
dt.object = src
|
||||
loops, verts = set(), set()
|
||||
if src_cols:
|
||||
dom = (src.data.color_attributes.active_color or src_cols[0]).domain
|
||||
(loops if dom == 'CORNER' else verts).add(
|
||||
'COLOR_CORNER' if dom == 'CORNER' else 'COLOR_VERTEX')
|
||||
if src_uvs:
|
||||
loops.add('UV')
|
||||
if loops:
|
||||
dt.use_loop_data = True
|
||||
dt.data_types_loops = loops
|
||||
dt.loop_mapping = 'POLYINTERP_NEAREST'
|
||||
if verts:
|
||||
dt.use_vert_data = True
|
||||
dt.data_types_verts = verts
|
||||
dt.vert_mapping = 'POLYINTERP_NEAREST'
|
||||
bpy.ops.object.datalayout_transfer(modifier=dt.name) # create dest layers
|
||||
bpy.ops.object.modifier_apply(modifier=dt.name)
|
||||
transferred = bool(lp.data.color_attributes) if src_cols else bool(lp.data.uv_layers)
|
||||
except Exception as e:
|
||||
print(f" colour/UV transfer failed: {e}")
|
||||
|
||||
# the voxel remesh can leave degenerate faces; Blender warns "mesh is not valid" on export
|
||||
lp.data.validate(verbose=False)
|
||||
|
||||
lp_mesh_name = lp.data.name
|
||||
bpy.data.objects.remove(src, do_unlink=True)
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
lp.select_set(True)
|
||||
bpy.context.view_layer.objects.active = lp
|
||||
print(f" remesh voxel={v:.4f} -> {n} tris -> {len(lp.data.polygons)} "
|
||||
f"(colour {'transferred' if transferred else 'LOST'}) [{lp_mesh_name}]")
|
||||
return lp, transferred
|
||||
|
||||
|
||||
def wire_vertex_colour(obj):
|
||||
"""Route the active colour attribute into Base Color on every material.
|
||||
|
||||
The glTF exporter only writes COLOR_0 when a material actually reads the attribute
|
||||
(`export_vertex_color='MATERIAL'`), and these sources arrive with NO material block at all —
|
||||
Blender invents a plain grey one on import. Without this the remeshed props would export
|
||||
colourless and every thumbnail would be grey.
|
||||
"""
|
||||
if not obj.data.color_attributes:
|
||||
return False
|
||||
cname = obj.data.color_attributes.active_color.name \
|
||||
if obj.data.color_attributes.active_color else obj.data.color_attributes[0].name
|
||||
if not obj.data.materials:
|
||||
m = bpy.data.materials.new("vcol")
|
||||
m.use_nodes = True
|
||||
obj.data.materials.append(m)
|
||||
for m in obj.data.materials:
|
||||
if not m or not m.use_nodes:
|
||||
continue
|
||||
nt = m.node_tree
|
||||
bsdf = nt.nodes.get("Principled BSDF")
|
||||
if not bsdf:
|
||||
continue
|
||||
n = nt.nodes.new("ShaderNodeVertexColor")
|
||||
n.layer_name = cname
|
||||
nt.links.new(n.outputs["Color"], bsdf.inputs["Base Color"])
|
||||
if "Specular IOR Level" in bsdf.inputs:
|
||||
bsdf.inputs["Specular IOR Level"].default_value = 0.2
|
||||
bsdf.inputs["Roughness"].default_value = 0.72
|
||||
return True
|
||||
|
||||
|
||||
def resize_textures(max_px=1024):
|
||||
for img in bpy.data.images:
|
||||
if img.size[0] > max_px or img.size[1] > max_px:
|
||||
@ -154,14 +298,37 @@ def normalize_one(spec):
|
||||
src = os.path.join(ROOT, spec["src"]) if not os.path.isabs(spec["src"]) else spec["src"]
|
||||
obj = import_join(src)
|
||||
clipped = clean_mesh(obj) # welds, drops loose geo, TRIANGULATES
|
||||
tri_before = len(obj.data.polygons) # true source triangle count (post-triangulate)
|
||||
tri_before = len(obj.data.polygons)
|
||||
|
||||
# shell-soup path: rebuild topology before the decimate loop, which cannot touch it
|
||||
nm = 0.0
|
||||
remeshed = vcol = False
|
||||
if spec.get("remesh"):
|
||||
nm = nonmanifold_ratio(obj)
|
||||
obj, vcol = remesh_shellsoup(obj, spec.get("tris", 5000), spec.get("voxel"),
|
||||
spec.get("shell_thickness", 0.0))
|
||||
remeshed = True # true source triangle count (post-triangulate)
|
||||
|
||||
# stand upright (if mis-authored), then face -Z
|
||||
rx, ry, rz = UP_ROT.get(spec.get("up", "none"), (0, 0, 0))
|
||||
obj.rotation_euler = (rx, ry, rz)
|
||||
if spec.get("yaw"):
|
||||
obj.rotation_euler[2] += math.radians(spec["yaw"])
|
||||
bpy.ops.object.transform_apply(rotation=True)
|
||||
if rx or ry or rz or spec.get("yaw"):
|
||||
# Select+activate explicitly: transform_apply acts on the SELECTION, and after clean_mesh
|
||||
# (or a remesh) the object we mean is not guaranteed to still be it. A silently-skipped
|
||||
# yaw is invisible in the log and ships a prop facing the wrong way.
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.rotation_mode = 'XYZ'
|
||||
before = tuple(round(v, 4) for v in obj.dimensions)
|
||||
obj.rotation_euler = (rx, ry, rz)
|
||||
if spec.get("yaw"):
|
||||
obj.rotation_euler[2] += math.radians(spec["yaw"])
|
||||
bpy.ops.object.transform_apply(rotation=True)
|
||||
after = tuple(round(v, 4) for v in obj.dimensions)
|
||||
if before == after and (spec.get("yaw", 0) % 180) not in (0,):
|
||||
print(f" WARNING: yaw/up requested but dimensions unchanged {before} — not applied")
|
||||
else:
|
||||
print(f" oriented {before} -> {after}")
|
||||
|
||||
# solidify: cap open bottoms → watertight, white sole (finish_glb.py:54)
|
||||
if spec.get("solidify"):
|
||||
@ -235,17 +402,22 @@ def normalize_one(spec):
|
||||
mn, mx = bounds(obj)
|
||||
|
||||
resize_textures(1024)
|
||||
if spec.get("remesh") or spec.get("vertex_colour"):
|
||||
vcol = wire_vertex_colour(obj) or vcol
|
||||
|
||||
name = f"procity_{spec['category']}_{spec['id']}_01.glb"
|
||||
out = os.path.join(OUT_DIR, name)
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=out, export_format='GLB', export_yup=True, export_apply=True,
|
||||
export_image_format='WEBP', export_image_quality=85,
|
||||
)
|
||||
exp = dict(filepath=out, export_format='GLB', export_yup=True, export_apply=True,
|
||||
export_image_format='WEBP', export_image_quality=85)
|
||||
try: # Blender >=4.2 gates COLOR_0 behind this enum
|
||||
bpy.ops.export_scene.gltf(**exp, export_vertex_color='ACTIVE')
|
||||
except TypeError:
|
||||
bpy.ops.export_scene.gltf(**exp)
|
||||
thumb = render_thumb(name.replace(".glb", ".png"))
|
||||
return {
|
||||
"src": spec["src"], "file": name, "out": out, "thumb": thumb,
|
||||
"tri_before": tri_before, "tri_after": tri_after,
|
||||
"remeshed": remeshed, "nonmanifold": round(nm, 3), "vertex_colour": vcol,
|
||||
"size_m": [round(mx.x - mn.x, 3), round(mx.z - mn.z, 3), round(mx.y - mn.y, 3)],
|
||||
"footprint": [round(mx.x - mn.x, 2), round(mx.y - mn.y, 2)],
|
||||
}
|
||||
|
||||
@ -67,13 +67,29 @@ def main():
|
||||
drift_check(record)
|
||||
return
|
||||
only = sys.argv[sys.argv.index("--only") + 1] if "--only" in sys.argv else ""
|
||||
# [R41] --files LIST.txt : publish exactly these basenames (one per line). `--only` is a
|
||||
# substring match, which cannot express "these 110 assets from three libraries and nothing
|
||||
# else". It matters because `_normalized/` is shared: other lanes stage rigs and clips in the
|
||||
# same directory, and a blanket run would push whatever they happen to have half-written.
|
||||
explicit = None
|
||||
if "--files" in sys.argv:
|
||||
listfile = sys.argv[sys.argv.index("--files") + 1]
|
||||
explicit = {ln.strip() for ln in open(listfile) if ln.strip()}
|
||||
# Passwordless tailnet path (round-3 decision #4): GOD3_DEPOT overridden to an allowlisted
|
||||
# tailnet endpoint (e.g. http://100.94.195.115:8788) trusts this box — no cookie needed.
|
||||
tailnet = DEPOT != "https://digalot.fyi/3god"
|
||||
glbs = sorted(g for g in glob.glob(NORM + "/*.glb")
|
||||
if os.path.basename(g) not in SKIP and (not only or only in os.path.basename(g)))
|
||||
if os.path.basename(g) not in SKIP
|
||||
and (explicit is None or os.path.basename(g) in explicit)
|
||||
and (not only or only in os.path.basename(g)))
|
||||
if not glbs:
|
||||
print(f"no GLBs in {NORM} matching {only!r} — run normalize.py first"); sys.exit(1)
|
||||
if explicit is not None:
|
||||
missing = explicit - {os.path.basename(g) for g in glbs}
|
||||
if missing:
|
||||
print(f"ERROR: {len(missing)} name(s) in --files are not staged in {NORM}: "
|
||||
f"{sorted(missing)[:5]}")
|
||||
sys.exit(2)
|
||||
print(f"depot: {DEPOT} assets: {len(glbs)} {'(DRY RUN)' if DRY else ''}"
|
||||
+ (" [tailnet passwordless]" if tailnet else ""))
|
||||
if not DRY and not PW and not tailnet:
|
||||
|
||||
70
pipeline/r41_3dstore.json
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
"_comment": [
|
||||
"R41 §41.2 — the 3dstore import, BY NAME (integrator ruling 3: no wildcard copies).",
|
||||
"Source: ~/Documents/3dstore/ (46 GLB). Every file below was measured with glb_stat.py on",
|
||||
"2026-08-07, not taken from the handover note. 40 accepted, 6 rejected (see `reject`).",
|
||||
"These assets need NO Blender pass: they are already metres, +Y up, base-origin, no Draco,",
|
||||
"no textures, <=5k tris. The ONLY change is pipeline/merge_prims.py collapsing the",
|
||||
"per-record-sleeve materials into one vertex-coloured primitive, because 16 materials on one",
|
||||
"mesh is 16 draw calls and the interior margin is 162.",
|
||||
"kind: bin|rack|counter|trolley|prop|arch|wall_mount — tells Lane C what it is standing on."
|
||||
],
|
||||
"accept": [
|
||||
{"src": "Black Tub.glb", "id": "tub_black", "kind": "bin"},
|
||||
{"src": "Black Tub long.glb", "id": "tub_black_long", "kind": "bin"},
|
||||
{"src": "White Tub.glb", "id": "tub_white", "kind": "bin"},
|
||||
{"src": "White Tub long.glb", "id": "tub_white_long", "kind": "bin"},
|
||||
{"src": "Blue Crate.glb", "id": "crate_blue", "kind": "bin"},
|
||||
{"src": "Wooden Rack Bin.glb", "id": "rack_bin_wooden", "kind": "bin"},
|
||||
{"src": "endoffunk1.glb", "id": "bin_endoffunk", "kind": "bin"},
|
||||
{"src": "chiu-long-bin.glb", "id": "chiu_bin", "kind": "bin"},
|
||||
{"src": "chiu-long-bin-low.glb", "id": "chiu_bin_low", "kind": "bin"},
|
||||
{"src": "chiu-half-bin-sideways.glb", "id": "chiu_bin_half", "kind": "bin"},
|
||||
{"src": "chiu-long.glb", "id": "chiu_rack_long", "kind": "rack"},
|
||||
{"src": "chiu-short.glb", "id": "chiu_rack_short", "kind": "rack"},
|
||||
{"src": "mos-japhop.glb", "id": "mos_stack", "kind": "rack"},
|
||||
{"src": "mos-japhop-top.glb", "id": "mos_bin_top", "kind": "bin"},
|
||||
{"src": "mos-japhop-upper-mid.glb", "id": "mos_bin_upper_mid", "kind": "bin"},
|
||||
{"src": "mos-japhop-lower-mid.glb", "id": "mos_bin_lower_mid", "kind": "bin"},
|
||||
{"src": "mos-japhop-bottom.glb", "id": "mos_bin_bottom", "kind": "bin"},
|
||||
{"src": "big-4x4x4-01.glb", "id": "bin_big_4x4", "kind": "rack"},
|
||||
{"src": "tubs-4x2-wide.glb", "id": "tubs_4x2_wide", "kind": "rack"},
|
||||
{"src": "tubs-4x2-vertical.glb", "id": "tubs_4x2_vertical", "kind": "rack"},
|
||||
{"src": "white-tub-3x3.glb", "id": "tubs_3x3", "kind": "rack"},
|
||||
{"src": "white-tub-3x3-long.glb", "id": "tubs_3x3_long", "kind": "rack"},
|
||||
{"src": "3-top-4-low-tubs-on-shelf-long.glb", "id": "tubs_shelf_long", "kind": "rack"},
|
||||
{"src": "3-top-4-low-tubs-on-shelf-wide.glb", "id": "tubs_shelf_wide", "kind": "rack"},
|
||||
{"src": "wooden-rack-type-long.glb", "id": "wooden_rack_long", "kind": "rack"},
|
||||
{"src": "wooden-rack-type-wide.glb", "id": "wooden_rack_wide", "kind": "rack"},
|
||||
{"src": "library trolley.glb", "id": "library_trolley", "kind": "trolley"},
|
||||
{"src": "STATION 1.glb", "id": "counter_station", "kind": "counter"},
|
||||
{"src": "STATION 2.glb", "id": "counter_station_side", "kind": "counter"},
|
||||
{"src": "window-booth.glb", "id": "window_booth", "kind": "prop"},
|
||||
{"src": "vinylsleeve_v5.glb", "id": "vinyl_sleeve", "kind": "prop",
|
||||
"rebase": true,
|
||||
"_why": "the only 3dstore file NOT base-origin: minY -0.160 (centred, not seated)"},
|
||||
{"src": "AIR CON.glb", "id": "aircon_unit", "kind": "wall_mount"},
|
||||
{"src": "long guy.glb", "id": "shelf_board_long", "kind": "wall_mount"},
|
||||
{"src": "short guy.glb", "id": "shelf_board_short", "kind": "wall_mount"},
|
||||
{"src": "wall big.glb", "id": "wall_shelves_big", "kind": "wall_mount"},
|
||||
{"src": "wall long.glb", "id": "wall_shelves_long", "kind": "wall_mount"},
|
||||
{"src": "DOOR-front.glb", "id": "door_shop", "kind": "arch"},
|
||||
{"src": "GREY COLUMN-FRONT.glb", "id": "column_square", "kind": "arch"},
|
||||
{"src": "WHITE COLUMN.glb", "id": "column_wide", "kind": "arch"},
|
||||
{"src": "ROOF COLUMN.glb", "id": "beam_roof", "kind": "arch"}
|
||||
],
|
||||
"reject": [
|
||||
{"src": "3dmrp.glb",
|
||||
"why": "499,948 tris (100x the 5k prop budget) + a 4096px texture. Named in the tasking as the one reject; measured and confirmed."},
|
||||
{"src": "DOOR.glb",
|
||||
"why": "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."},
|
||||
{"src": "GREY COLUMN.glb",
|
||||
"why": "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."},
|
||||
{"src": "Shelf Level.glb",
|
||||
"why": "4.50 x 0.05 x 0.005 m — a 5 mm trim rail from the booth_room build script, not a placeable fitting."},
|
||||
{"src": "Shelf Level long.glb",
|
||||
"why": "2.60 x 0.05 x 0.005 m — same 5 mm trim rail, short version."},
|
||||
{"src": "floor crate.glb",
|
||||
"why": "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."}
|
||||
]
|
||||
}
|
||||
250
pipeline/r41_djgear_batch.json
Normal file
@ -0,0 +1,250 @@
|
||||
[
|
||||
{
|
||||
"src": "TURNTABLE-1200-RIGGED.glb",
|
||||
"id": "deck_1200",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "SL1.glb",
|
||||
"id": "sl_deck_1",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "SL2.glb",
|
||||
"id": "sl_deck_2",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "SL3.glb",
|
||||
"id": "sl_deck_3",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "SL4.glb",
|
||||
"id": "sl_deck_4",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "TTM54i.glb",
|
||||
"id": "mixer_ttm54i",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "PMC-05PRO.glb",
|
||||
"id": "mixer_pmc05pro",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "PMC-06ProA.glb",
|
||||
"id": "mixer_pmc06pro",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "PMC-07Pro.glb",
|
||||
"id": "mixer_pmc07pro",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "PMC-08Pro.glb",
|
||||
"id": "mixer_pmc08pro",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "PMC-10.glb",
|
||||
"id": "mixer_pmc10",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "PMC-20.glb",
|
||||
"id": "mixer_pmc20",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "SH-DJ1200.glb",
|
||||
"id": "mixer_shdj1200",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "MX-2200.glb",
|
||||
"id": "mixer_mx2200",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "PMX-9000.glb",
|
||||
"id": "mixer_pmx9000",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "TR-808.glb",
|
||||
"id": "drum_808",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "TR-1000.glb",
|
||||
"id": "drum_tr1000",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "SP-1200.glb",
|
||||
"id": "sampler_sp1200",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "MPC3000.glb",
|
||||
"id": "sampler_mpc3000",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "S900.glb",
|
||||
"id": "sampler_s900",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "HD25.glb",
|
||||
"id": "headphones_hd25",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "M44-7.glb",
|
||||
"id": "cart_m447",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "CONCORDE-PRO.glb",
|
||||
"id": "cart_concorde",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "CONCORDE-CASE.glb",
|
||||
"id": "cart_case",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "HEADSHELL-TECHNICS.glb",
|
||||
"id": "headshell_technics",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "STANTON-500.glb",
|
||||
"id": "cart_stanton",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "ZIP-100.glb",
|
||||
"id": "zip_drive",
|
||||
"category": "fit",
|
||||
"mode": "static",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "TURNTABLE-1200-RIGGED.glb",
|
||||
"id": "deck_1200_rigged",
|
||||
"category": "fit",
|
||||
"mode": "rigged",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "TTM54i.glb",
|
||||
"id": "mixer_ttm54i_rigged",
|
||||
"category": "fit",
|
||||
"mode": "rigged",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "PMC-05PRO.glb",
|
||||
"id": "mixer_pmc05pro_rigged",
|
||||
"category": "fit",
|
||||
"mode": "rigged",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
},
|
||||
{
|
||||
"src": "SH-DJ1200.glb",
|
||||
"id": "mixer_shdj1200_rigged",
|
||||
"category": "fit",
|
||||
"mode": "rigged",
|
||||
"height_m": 0,
|
||||
"tris": 5000
|
||||
}
|
||||
]
|
||||
423
pipeline/r41_pub_batch.json
Normal file
@ -0,0 +1,423 @@
|
||||
[
|
||||
{
|
||||
"src": "ashUrn.glb",
|
||||
"id": "ash_urn",
|
||||
"category": "fit",
|
||||
"height_m": 0.75,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "ashtray.glb",
|
||||
"id": "ashtray_post",
|
||||
"category": "fit",
|
||||
"height_m": 0.95,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "bainMarie.glb",
|
||||
"id": "bain_marie",
|
||||
"category": "fit",
|
||||
"height_m": 0.95,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "balustrade.glb",
|
||||
"id": "balustrade",
|
||||
"category": "fit",
|
||||
"height_m": 1.05,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "banquette.glb",
|
||||
"id": "banquette",
|
||||
"category": "fit",
|
||||
"height_m": 0.85,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "barrier.glb",
|
||||
"id": "crowd_barrier",
|
||||
"category": "fit",
|
||||
"height_m": 1.1,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "beerUmbrella.glb",
|
||||
"id": "beer_umbrella",
|
||||
"category": "fit",
|
||||
"height_m": 2.3,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "binRound.glb",
|
||||
"id": "bin_round",
|
||||
"category": "fit",
|
||||
"height_m": 0.75,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "bistroTable.glb",
|
||||
"id": "bistro_table",
|
||||
"category": "fit",
|
||||
"height_m": 0.92,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "brokenBasin.glb",
|
||||
"id": "broken_basin",
|
||||
"category": "fit",
|
||||
"height_m": 0.6,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "cableSnake.glb",
|
||||
"id": "cable_snake",
|
||||
"category": "fit",
|
||||
"height_m": 0.2,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "canStack.glb",
|
||||
"id": "can_stack",
|
||||
"category": "fit",
|
||||
"height_m": 0.45,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "cdj.glb",
|
||||
"id": "cdj_deck",
|
||||
"category": "fit",
|
||||
"height_m": 0.15,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "champBucket.glb",
|
||||
"id": "champ_bucket",
|
||||
"category": "fit",
|
||||
"height_m": 0.35,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "cocktailBar.glb",
|
||||
"id": "cocktail_bar",
|
||||
"category": "fit",
|
||||
"height_m": 2.0,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "distroBox.glb",
|
||||
"id": "distro_box",
|
||||
"category": "fit",
|
||||
"height_m": 1.2,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "djPlinth.glb",
|
||||
"id": "dj_plinth",
|
||||
"category": "fit",
|
||||
"height_m": 1.0,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "fireExt.glb",
|
||||
"id": "fire_ext",
|
||||
"category": "fit",
|
||||
"height_m": 0.6,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "firepit.glb",
|
||||
"id": "firepit",
|
||||
"category": "fit",
|
||||
"height_m": 0.5,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "flightCase.glb",
|
||||
"id": "flight_case",
|
||||
"category": "fit",
|
||||
"height_m": 0.75,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "jukebox.glb",
|
||||
"id": "jukebox",
|
||||
"category": "fit",
|
||||
"height_m": 1.52,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1,
|
||||
"yaw": 90
|
||||
},
|
||||
{
|
||||
"src": "keg.glb",
|
||||
"id": "keg",
|
||||
"category": "fit",
|
||||
"height_m": 0.6,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "loungeChair.glb",
|
||||
"id": "lounge_chair",
|
||||
"category": "fit",
|
||||
"height_m": 0.8,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "marbleSink.glb",
|
||||
"id": "marble_sink",
|
||||
"category": "fit",
|
||||
"height_m": 0.85,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "oldCouch.glb",
|
||||
"id": "old_couch",
|
||||
"category": "fit",
|
||||
"height_m": 0.85,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "pallet.glb",
|
||||
"id": "pallet",
|
||||
"category": "fit",
|
||||
"height_m": 0.2,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "picnicTable.glb",
|
||||
"id": "picnic_table",
|
||||
"category": "fit",
|
||||
"height_m": 0.75,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "pieWarmer.glb",
|
||||
"id": "pie_warmer",
|
||||
"category": "fit",
|
||||
"height_m": 0.55,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "pillar.glb",
|
||||
"id": "concrete_plinth",
|
||||
"category": "fit",
|
||||
"height_m": 1.2,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "planter.glb",
|
||||
"id": "planter",
|
||||
"category": "fit",
|
||||
"height_m": 0.55,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "plywoodBar.glb",
|
||||
"id": "plywood_bar",
|
||||
"category": "fit",
|
||||
"height_m": 1.05,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "pokie.glb",
|
||||
"id": "pokie",
|
||||
"category": "fit",
|
||||
"height_m": 1.7,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "raffleDrum.glb",
|
||||
"id": "raffle_drum",
|
||||
"category": "fit",
|
||||
"height_m": 0.9,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "ropePost.glb",
|
||||
"id": "rope_post",
|
||||
"category": "fit",
|
||||
"height_m": 1.0,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "sandwichBoard.glb",
|
||||
"id": "sandwich_board",
|
||||
"category": "fit",
|
||||
"height_m": 1.0,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "smokeMachine.glb",
|
||||
"id": "smoke_machine",
|
||||
"category": "fit",
|
||||
"height_m": 0.38,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "stackF1.glb",
|
||||
"id": "speaker_stack",
|
||||
"category": "fit",
|
||||
"height_m": 1.8,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "strobe.glb",
|
||||
"id": "strobe_light",
|
||||
"category": "fit",
|
||||
"height_m": 0.4,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "subBass.glb",
|
||||
"id": "sub_bass",
|
||||
"category": "fit",
|
||||
"height_m": 0.7,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "topiary.glb",
|
||||
"id": "topiary",
|
||||
"category": "fit",
|
||||
"height_m": 1.2,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "trough.glb",
|
||||
"id": "urinal_trough",
|
||||
"category": "fit",
|
||||
"height_m": 0.7,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
},
|
||||
{
|
||||
"src": "wheelieBin.glb",
|
||||
"id": "wheelie_bin",
|
||||
"category": "fit",
|
||||
"height_m": 1.1,
|
||||
"tris": 5000,
|
||||
"remesh": true,
|
||||
"voxel": 0.011111111111111112,
|
||||
"shell_thickness": -1
|
||||
}
|
||||
]
|
||||
171
pipeline/r41_stage_3dstore.py
Normal file
@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stage the named 3dstore fittings into the repo (R41 §41.2, step 1).
|
||||
|
||||
No Blender. The source assets already satisfy the house GLB law measured end to end — metres,
|
||||
+Y up, base-origin, no Draco, no textures, <=5k tris — so putting them through `normalize.py`
|
||||
would re-join, re-triangulate and re-export geometry that is already correct, which is three
|
||||
chances to move something for no gain. The two things they DO need are done here:
|
||||
|
||||
1. pipeline/merge_prims.py — collapse the per-sleeve materials to one vertex-coloured
|
||||
primitive. 16 materials on one mesh is 16 draw calls; the interior margin is 162.
|
||||
2. base-origin repair for the one file that needs it (vinylsleeve_v5, minY -0.160).
|
||||
|
||||
Reads pipeline/r41_3dstore.json (the by-name allow-list, ruling 3). Writes
|
||||
pipeline/_normalized/procity_fit_<id>_01.glb, mirrors to web/assets/models/, and emits
|
||||
pipeline/_r41_3dstore_results.json with measured footprints/heights for build_manifest.py.
|
||||
|
||||
python3 pipeline/r41_stage_3dstore.py [--src ~/Documents/3dstore] [--dry-run]
|
||||
"""
|
||||
import json, os, shutil, sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(HERE)
|
||||
sys.path.insert(0, HERE)
|
||||
import merge_prims as MP
|
||||
import glb_stat as GS
|
||||
|
||||
SPEC = os.path.join(HERE, "r41_3dstore.json")
|
||||
NORM = os.path.join(ROOT, "pipeline", "_normalized")
|
||||
MODELS = os.path.join(ROOT, "web", "assets", "models")
|
||||
RESULTS = os.path.join(HERE, "_r41_3dstore_results.json")
|
||||
|
||||
|
||||
def world_bbox(path):
|
||||
"""World-space min/max, node transforms respected (glb_stat's own walk)."""
|
||||
g, _ = GS._read_glb(path)
|
||||
acc, meshes, nodes = g.get("accessors", []), g.get("meshes", []), g.get("nodes", [])
|
||||
mn = [float("inf")] * 3
|
||||
mx = [float("-inf")] * 3
|
||||
scenes = g.get("scenes") or [{"nodes": list(range(len(nodes)))}]
|
||||
|
||||
def walk(ni, M):
|
||||
n = nodes[ni]
|
||||
W = GS._mat_mul(M, GS._node_matrix(n))
|
||||
if "mesh" in n:
|
||||
for p in meshes[n["mesh"]].get("primitives", []):
|
||||
pos = p.get("attributes", {}).get("POSITION")
|
||||
if pos is None:
|
||||
continue
|
||||
a = acc[pos]
|
||||
if "min" not in a:
|
||||
continue
|
||||
for cx in (a["min"][0], a["max"][0]):
|
||||
for cy in (a["min"][1], a["max"][1]):
|
||||
for cz in (a["min"][2], a["max"][2]):
|
||||
w = GS._xform(W, [cx, cy, cz])
|
||||
for k in range(3):
|
||||
mn[k] = min(mn[k], w[k]); mx[k] = max(mx[k], w[k])
|
||||
for c in n.get("children", []):
|
||||
walk(c, W)
|
||||
|
||||
I = [[1 if i == j else 0 for j in range(4)] for i in range(4)]
|
||||
for r in scenes[g.get("scene", 0)].get("nodes", []):
|
||||
walk(r, I)
|
||||
return mn, mx
|
||||
|
||||
|
||||
def rebase(path):
|
||||
"""Seat the asset on y=0 and centre it in X/Z by wrapping the scene roots in one node.
|
||||
|
||||
Wrapping rather than editing each root's TRS keeps every original transform intact and is
|
||||
exactly reversible — the asset is correct, only its pivot was authored at the centre.
|
||||
"""
|
||||
g, binc = MP.read_glb(path)
|
||||
mn, mx = world_bbox(path)
|
||||
dx = -(mn[0] + mx[0]) / 2.0
|
||||
dy = -mn[1]
|
||||
dz = -(mn[2] + mx[2]) / 2.0
|
||||
if abs(dx) < 1e-6 and abs(dy) < 1e-6 and abs(dz) < 1e-6:
|
||||
return False, (0, 0, 0)
|
||||
scenes = g.setdefault("scenes", [{"nodes": list(range(len(g.get("nodes", []))))}])
|
||||
si = g.get("scene", 0)
|
||||
roots = scenes[si].get("nodes", [])
|
||||
g.setdefault("nodes", []).append({"name": "procity_base_origin",
|
||||
"translation": [dx, dy, dz], "children": list(roots)})
|
||||
scenes[si]["nodes"] = [len(g["nodes"]) - 1]
|
||||
MP.write_glb(path, g, binc)
|
||||
return True, (round(dx, 4), round(dy, 4), round(dz, 4))
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
dry = "--dry-run" in args
|
||||
src_dir = os.path.expanduser("~/Documents/3dstore")
|
||||
if "--src" in args:
|
||||
src_dir = os.path.expanduser(args[args.index("--src") + 1])
|
||||
spec = json.load(open(SPEC))
|
||||
|
||||
os.makedirs(NORM, exist_ok=True)
|
||||
os.makedirs(MODELS, exist_ok=True)
|
||||
results, errors = [], []
|
||||
|
||||
print(f"{'file':34} {'tris':>6} {'draws':>11} {'w x d x h (m)':>22} kind")
|
||||
print("-" * 92)
|
||||
for e in spec["accept"]:
|
||||
s = os.path.join(src_dir, e["src"])
|
||||
if not os.path.isfile(s):
|
||||
errors.append({"src": e["src"], "error": "source missing"})
|
||||
print(f"MISSING {e['src']}")
|
||||
continue
|
||||
name = f"procity_fit_{e['id']}_01.glb"
|
||||
out = os.path.join(NORM, name)
|
||||
try:
|
||||
before = MP.check(s)
|
||||
if dry:
|
||||
print(f"{name:34} {before['tris']:>6} {before['draws']:>4} -> ? (dry-run)")
|
||||
continue
|
||||
# merge() mutates the glTF dict in place and returns the rebuilt BIN chunk
|
||||
gltf, binc = MP.read_glb(s)
|
||||
_rep, newbin = MP.merge(gltf, binc)
|
||||
MP.write_glb(out, gltf, newbin)
|
||||
moved = (False, (0, 0, 0))
|
||||
if e.get("rebase"):
|
||||
moved = rebase(out)
|
||||
mn, mx = world_bbox(out)
|
||||
after = MP.check(out)
|
||||
if after["tris"] != before["tris"]:
|
||||
errors.append({"src": e["src"], "error":
|
||||
f"tri drift {before['tris']} -> {after['tris']}"})
|
||||
w, h, d = round(mx[0] - mn[0], 3), round(mx[1] - mn[1], 3), round(mx[2] - mn[2], 3)
|
||||
if abs(mn[1]) > 0.01:
|
||||
errors.append({"src": e["src"], "error": f"not base-origin: minY={mn[1]:.3f}"})
|
||||
shutil.copy2(out, os.path.join(MODELS, name))
|
||||
results.append({
|
||||
"src": e["src"], "id": e["id"], "kind": e["kind"], "file": name,
|
||||
"tris": after["tris"], "draws_before": before["draws"],
|
||||
"draws_after": after["draws"], "materials_before": before["materials"],
|
||||
# floor at 1 cm: a 3 mm record sleeve rounds to 0.00 at 2 dp, and the validator
|
||||
# (rightly) calls a zero-width footprint insane. The number Lane C places on must
|
||||
# stay a positive metre value.
|
||||
"footprint": [max(round(w, 2), 0.01), max(round(d, 2), 0.01)],
|
||||
"height": max(round(h, 3), 0.01),
|
||||
"size_m": [w, d, h], "rebased": moved[0], "rebase_delta": moved[1],
|
||||
"thumb": f"thumbs/{name[:-4]}.png",
|
||||
})
|
||||
print(f"{name:34} {after['tris']:>6} {before['draws']:>4} -> {after['draws']:<4} "
|
||||
f"{w:>7.2f} x{d:>6.2f} x{h:>6.2f} {e['kind']}"
|
||||
+ (" [rebased]" if moved[0] else ""))
|
||||
except Exception as ex:
|
||||
errors.append({"src": e["src"], "error": str(ex)})
|
||||
print(f"ERR {e['src']}: {ex}")
|
||||
|
||||
if dry:
|
||||
return 0
|
||||
db = sum(r["draws_before"] for r in results)
|
||||
da = sum(r["draws_after"] for r in results)
|
||||
json.dump({"results": results, "errors": errors, "rejects": spec["reject"],
|
||||
"draws_before_total": db, "draws_after_total": da},
|
||||
open(RESULTS, "w"), indent=2)
|
||||
print("-" * 92)
|
||||
print(f"staged {len(results)}/{len(spec['accept'])} rejected {len(spec['reject'])} "
|
||||
f"errors {len(errors)}")
|
||||
print(f"DRAW COST across the set: {db} -> {da} ({db - da} draws removed, "
|
||||
f"{100 * (db - da) / max(db, 1):.0f}%)")
|
||||
for e in errors:
|
||||
print(f" ERR {e}")
|
||||
print(f"-> {RESULTS}")
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
263
pipeline/rig_join.py
Normal file
@ -0,0 +1,263 @@
|
||||
"""PROCITY DJ-gear normalizer — collapse draws WITHOUT breaking the named-node contract.
|
||||
|
||||
BL=/Applications/Blender.app/Contents/MacOS/Blender
|
||||
"$BL" --background --python pipeline/rig_join.py -- BATCH.json OUT_DIR [THUMB_DIR]
|
||||
|
||||
WHY (R41 §41.2, and the tension nobody had measured)
|
||||
----------------------------------------------------
|
||||
Integrator ruling 5 forbids `gltf-transform optimize` on this library: it collapsed a 696-node
|
||||
mixer to 25 nodes and deleted `Fader_CROSS`. True — and the reason it was reached for is also
|
||||
true. Measured on ultra's 60 exports, these models cost, in DRAW CALLS:
|
||||
|
||||
TR-1000 2145 · MPC-XL 1319 · MP2016 1201 · TR-808 1092 · PMC-08Pro 751 · TTM54i 659
|
||||
|
||||
The interior draw margin is **162**. One PMC-08Pro is 4.6x the entire budget for a room. So the
|
||||
library as shipped is unplaceable, and the fix that makes it placeable is the one that is banned —
|
||||
because both "join everything" and "join nothing" are wrong.
|
||||
|
||||
The third option is to join SELECTIVELY. Everything that is not a control, and everything under a
|
||||
control that is not the control itself, is scenery: it merges into one body mesh. Each node named
|
||||
in the contract keeps its own object, its own name, and its own pivot, so it can still be driven:
|
||||
|
||||
Platter_SPIN · Arm_YAW · Fader_PITCH · Fader_CROSS · Btn_STARTSTOP
|
||||
|
||||
That is 1 body + at most 5 controls = **<=6 draws** for a fully drivable deck, instead of 751 for
|
||||
one that nobody can afford to place. `mode:"static"` drops the controls into the body too (1 draw)
|
||||
for the dressing copies, which is what most of a record shop actually needs.
|
||||
|
||||
The contract is VERIFIED, not assumed: every model is scanned for contract nodes before and after,
|
||||
and `_rig_results.json` records both lists so a deletion can never pass silently.
|
||||
|
||||
BATCH.json: [ {"src":"TTM54i.glb", "id":"mixer_ttm54i", "category":"fit",
|
||||
"mode":"rigged"|"static", "height_m":0.10, "tris":5000, "yaw":0} ]
|
||||
"""
|
||||
import bpy, sys, os, math, json
|
||||
from mathutils import Vector
|
||||
|
||||
ARGV = sys.argv[sys.argv.index("--") + 1:]
|
||||
BATCH = ARGV[0]
|
||||
OUT_DIR = ARGV[1] if len(ARGV) > 1 else "/tmp/procity_rig"
|
||||
THUMB_DIR = ARGV[2] if len(ARGV) > 2 else os.path.join(OUT_DIR, "thumbs")
|
||||
ROOT = os.environ.get("PROCITY_MODELS_ROOT", os.path.expanduser("~/Documents/3D=models/dj-gear/glb"))
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
os.makedirs(THUMB_DIR, exist_ok=True)
|
||||
|
||||
CONTRACT = ["Platter_SPIN", "Arm_YAW", "Fader_PITCH", "Fader_CROSS", "Btn_STARTSTOP"]
|
||||
|
||||
|
||||
def wipe():
|
||||
for o in list(bpy.data.objects):
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
for blk in (bpy.data.meshes, bpy.data.materials, bpy.data.images):
|
||||
for d in list(blk):
|
||||
if d.users == 0:
|
||||
blk.remove(d)
|
||||
|
||||
|
||||
def world_bounds(objs):
|
||||
lo = Vector((1e9,) * 3); hi = Vector((-1e9,) * 3)
|
||||
for o in objs:
|
||||
if o.type != 'MESH':
|
||||
continue
|
||||
for c in o.bound_box:
|
||||
w = o.matrix_world @ Vector(c)
|
||||
for k in range(3):
|
||||
lo[k] = min(lo[k], w[k]); hi[k] = max(hi[k], w[k])
|
||||
return lo, hi
|
||||
|
||||
|
||||
def descendants(o):
|
||||
out = []
|
||||
for c in o.children:
|
||||
out.append(c)
|
||||
out.extend(descendants(c))
|
||||
return out
|
||||
|
||||
|
||||
def join_into(active, others):
|
||||
"""Join `others` into `active`, keeping active's origin (so a control keeps its pivot)."""
|
||||
others = [o for o in others if o.type == 'MESH' and o is not active]
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
active.select_set(True)
|
||||
for o in others:
|
||||
o.select_set(True)
|
||||
bpy.context.view_layer.objects.active = active
|
||||
if others:
|
||||
bpy.ops.object.join()
|
||||
return active
|
||||
|
||||
|
||||
def tris_of(o):
|
||||
return len(o.data.polygons)
|
||||
|
||||
|
||||
def decimate_to(o, budget):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
o.select_set(True)
|
||||
bpy.context.view_layer.objects.active = o
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.quads_convert_to_tris()
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
n = tris_of(o)
|
||||
passes = 0
|
||||
while n > budget * 1.05 and passes < 6:
|
||||
m = o.modifiers.new("dec", 'DECIMATE')
|
||||
m.ratio = max(0.01, budget / n)
|
||||
bpy.ops.object.modifier_apply(modifier=m.name)
|
||||
new = tris_of(o)
|
||||
passes += 1
|
||||
if new >= n * 0.97:
|
||||
n = new
|
||||
break
|
||||
n = new
|
||||
return n
|
||||
|
||||
|
||||
def scan_contract():
|
||||
names = {o.name for o in bpy.data.objects}
|
||||
return [c for c in CONTRACT if c in names]
|
||||
|
||||
|
||||
def process(spec):
|
||||
wipe()
|
||||
src = spec["src"] if os.path.isabs(spec["src"]) else os.path.join(ROOT, spec["src"])
|
||||
bpy.ops.import_scene.gltf(filepath=src)
|
||||
|
||||
before = scan_contract()
|
||||
all_mesh = [o for o in bpy.data.objects if o.type == 'MESH']
|
||||
tri_before = sum(tris_of(o) for o in all_mesh)
|
||||
draws_before = len(all_mesh)
|
||||
mode = spec.get("mode", "static")
|
||||
budget = spec.get("tris", 5000)
|
||||
|
||||
keep, ctrl_meshes = [], []
|
||||
if mode == "rigged":
|
||||
# Each contract node absorbs its own subtree and keeps its pivot; scenery goes to the body.
|
||||
#
|
||||
# The named node stays whatever the source made it. In this library the controls are
|
||||
# EMPTIES with the cap/stem/rib parented underneath (dj-gear/README.md: "moving the bare
|
||||
# node moves the whole fader"), so the empty IS the contract and the pivot. An earlier
|
||||
# pass promoted the biggest mesh child to carry the name and deleted the empty — which
|
||||
# silently dropped the empty's translation and flung Btn_STARTSTOP to the origin, making
|
||||
# the rigged deck 0.28 m tall against the static copy's 0.12 m. Keep the empty.
|
||||
for name in before:
|
||||
o = bpy.data.objects.get(name)
|
||||
if not o:
|
||||
continue
|
||||
subtree = [d for d in descendants(o) if d.type == 'MESH']
|
||||
if o.type == 'MESH':
|
||||
join_into(o, subtree)
|
||||
ctrl_meshes.append(o)
|
||||
else:
|
||||
if not subtree:
|
||||
continue
|
||||
host = max(subtree, key=tris_of)
|
||||
join_into(host, [s for s in subtree if s is not host])
|
||||
ctrl_meshes.append(host)
|
||||
# detach from the source hierarchy, keeping the world placement
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
o.select_set(True)
|
||||
bpy.context.view_layer.objects.active = o
|
||||
bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM')
|
||||
keep.append(o)
|
||||
|
||||
keep_names = {o.name for o in keep} | {o.name for o in ctrl_meshes}
|
||||
body_parts = [o for o in bpy.data.objects
|
||||
if o.type == 'MESH' and o.name not in keep_names]
|
||||
if not body_parts:
|
||||
raise ValueError("no body geometry left")
|
||||
body = max(body_parts, key=tris_of)
|
||||
body = join_into(body, body_parts)
|
||||
body.name = "Body"
|
||||
|
||||
# controls become children of the body so one transform moves the whole unit
|
||||
for o in keep:
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
o.select_set(True)
|
||||
body.select_set(True)
|
||||
bpy.context.view_layer.objects.active = body
|
||||
bpy.ops.object.parent_set(type='OBJECT', keep_transform=True)
|
||||
|
||||
ctrl_tris = sum(tris_of(o) for o in ctrl_meshes)
|
||||
decimate_to(body, max(600, budget - ctrl_tris))
|
||||
for o in ctrl_meshes: # keep controls readable, but bounded
|
||||
if tris_of(o) > 400:
|
||||
decimate_to(o, 400)
|
||||
|
||||
# orientation, height-normalise, base-origin — on the whole unit
|
||||
objs = [body] + keep + ctrl_meshes
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for o in objs:
|
||||
o.select_set(True)
|
||||
bpy.context.view_layer.objects.active = body
|
||||
if spec.get("yaw"):
|
||||
body.rotation_mode = 'XYZ'
|
||||
body.rotation_euler[2] += math.radians(spec["yaw"])
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
lo, hi = world_bounds(objs)
|
||||
h = spec.get("height_m", 0)
|
||||
if h and h > 0:
|
||||
s = h / max(hi.z - lo.z, 1e-6)
|
||||
body.scale *= s
|
||||
bpy.context.view_layer.update()
|
||||
lo, hi = world_bounds(objs)
|
||||
body.location -= Vector(((lo.x + hi.x) / 2, (lo.y + hi.y) / 2, lo.z))
|
||||
bpy.context.view_layer.update()
|
||||
lo, hi = world_bounds(objs)
|
||||
|
||||
for img in bpy.data.images:
|
||||
if img.size[0] > 1024 or img.size[1] > 1024:
|
||||
sc = 1024 / max(img.size)
|
||||
img.scale(max(1, int(img.size[0] * sc)), max(1, int(img.size[1] * sc)))
|
||||
|
||||
name = f"procity_{spec['category']}_{spec['id']}_01.glb"
|
||||
out = os.path.join(OUT_DIR, name)
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for o in objs:
|
||||
o.select_set(True)
|
||||
bpy.context.view_layer.objects.active = body
|
||||
bpy.ops.export_scene.gltf(filepath=out, export_format='GLB', export_yup=True,
|
||||
use_selection=True, export_apply=False,
|
||||
export_image_format='WEBP', export_image_quality=85)
|
||||
|
||||
after = scan_contract()
|
||||
return {
|
||||
"src": spec["src"], "id": spec["id"], "file": name, "mode": mode,
|
||||
"tri_before": tri_before, "tri_after": sum(tris_of(o) for o in objs if o.type == 'MESH'),
|
||||
"draws_before": draws_before,
|
||||
"draws_after": sum(1 for o in objs if o.type == 'MESH'),
|
||||
"contract_before": before, "contract_after": after,
|
||||
"contract_ok": set(before) == set(after) if mode == "rigged" else True,
|
||||
"controls": sorted(o.name for o in keep),
|
||||
"size_m": [round(hi.x - lo.x, 3), round(hi.z - lo.z, 3), round(hi.y - lo.y, 3)],
|
||||
"footprint": [max(round(hi.x - lo.x, 2), 0.01), max(round(hi.y - lo.y, 2), 0.01)],
|
||||
"height": max(round(hi.z - lo.z, 3), 0.01),
|
||||
"out": out, "thumb": f"thumbs/{name[:-4]}.png",
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
specs = json.load(open(BATCH))
|
||||
results, errors = [], []
|
||||
for spec in specs:
|
||||
try:
|
||||
r = process(spec)
|
||||
results.append(r)
|
||||
flag = "" if r["contract_ok"] else " *** CONTRACT BROKEN ***"
|
||||
print(f"OK {r['file']:44} {r['mode']:6} tris {r['tri_before']:>7}->{r['tri_after']:>5} "
|
||||
f"draws {r['draws_before']:>5}->{r['draws_after']:<3} "
|
||||
f"contract {','.join(r['contract_after']) or '-'}{flag}")
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
errors.append({"id": spec.get("id"), "src": spec.get("src"), "error": str(e)})
|
||||
print(f"ERR {spec.get('src')}: {e}")
|
||||
json.dump({"results": results, "errors": errors},
|
||||
open(os.path.join(OUT_DIR, "_rig_results.json"), "w"), indent=2)
|
||||
broken = [r for r in results if not r["contract_ok"]]
|
||||
print(f"\nRIG_DONE ok={len(results)} err={len(errors)} contract_broken={len(broken)} -> {OUT_DIR}")
|
||||
|
||||
|
||||
main()
|
||||
112
pipeline/thumbs.py
Normal file
@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render a 256px manifest thumbnail for each GLB — many per Blender session.
|
||||
|
||||
Separate from `normalize.py`'s `render_thumb` on purpose: that one is part of the normalize pass
|
||||
and shades with BLENDER_WORKBENCH `color_type='TEXTURE'`, which renders a **vertex-coloured,
|
||||
material-less** mesh flat grey. Two of R41's three libraries are exactly that (the 3dstore
|
||||
fittings after `merge_prims.py`, and the `art_incoming` props, which carry COLOR_0 and no material
|
||||
block at all), so their thumbnails would have shipped colourless. This renders in EEVEE off the
|
||||
real material graph, so what Lane C sees in the thumb is what the game draws.
|
||||
|
||||
BL=/Applications/Blender.app/Contents/MacOS/Blender
|
||||
"$BL" --background --python pipeline/thumbs.py -- OUT_DIR GLB [GLB ...]
|
||||
"""
|
||||
import bpy, sys, os, math
|
||||
from mathutils import Vector
|
||||
|
||||
ARGV = sys.argv[sys.argv.index("--") + 1:]
|
||||
OUT_DIR = ARGV[0]
|
||||
FILES = [f for f in ARGV[1:] if f.lower().endswith(".glb")]
|
||||
RES = 256
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def wipe():
|
||||
for o in list(bpy.data.objects):
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
for blk in (bpy.data.meshes, bpy.data.materials, bpy.data.images):
|
||||
for d in list(blk):
|
||||
if d.users == 0:
|
||||
blk.remove(d)
|
||||
|
||||
|
||||
def bounds(objs):
|
||||
lo = Vector((1e9,) * 3); hi = Vector((-1e9,) * 3)
|
||||
for o in objs:
|
||||
if o.type != 'MESH':
|
||||
continue
|
||||
for c in o.bound_box:
|
||||
w = o.matrix_world @ Vector(c)
|
||||
for k in range(3):
|
||||
lo[k] = min(lo[k], w[k]); hi[k] = max(hi[k], w[k])
|
||||
return lo, hi
|
||||
|
||||
|
||||
def setup_world():
|
||||
w = bpy.data.worlds.get("tw") or bpy.data.worlds.new("tw")
|
||||
bpy.context.scene.world = w
|
||||
w.use_nodes = True
|
||||
w.node_tree.nodes["Background"].inputs[0].default_value = (0.85, 0.87, 0.92, 1)
|
||||
w.node_tree.nodes["Background"].inputs[1].default_value = 1.6
|
||||
|
||||
|
||||
def main():
|
||||
scn = bpy.context.scene
|
||||
for e in ('BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'):
|
||||
try:
|
||||
scn.render.engine = e
|
||||
break
|
||||
except TypeError:
|
||||
continue
|
||||
scn.render.film_transparent = True
|
||||
scn.render.resolution_x = scn.render.resolution_y = RES
|
||||
scn.render.image_settings.file_format = 'PNG'
|
||||
scn.render.image_settings.color_mode = 'RGBA'
|
||||
scn.view_settings.view_transform = 'Standard'
|
||||
|
||||
ok = 0
|
||||
for path in FILES:
|
||||
wipe()
|
||||
setup_world()
|
||||
try:
|
||||
bpy.ops.import_scene.gltf(filepath=path)
|
||||
except Exception as ex:
|
||||
print(f"IMPORT FAIL {path}: {ex}")
|
||||
continue
|
||||
meshes = [o for o in bpy.data.objects if o.type == 'MESH']
|
||||
if not meshes:
|
||||
print(f"NO MESH {path}")
|
||||
continue
|
||||
lo, hi = bounds(meshes)
|
||||
ctr = (lo + hi) / 2
|
||||
size = max((hi - lo).x, (hi - lo).y, (hi - lo).z) or 1.0
|
||||
|
||||
bpy.ops.object.light_add(type='AREA',
|
||||
location=(ctr.x + size, ctr.y - size, hi.z + size * 1.2))
|
||||
L = bpy.context.object
|
||||
L.data.energy = 260 * size * size + 40
|
||||
L.data.size = size * 2.5
|
||||
L.rotation_euler = (math.radians(40), 0, math.radians(35))
|
||||
bpy.ops.object.light_add(type='AREA',
|
||||
location=(ctr.x - size, ctr.y - size * 1.4, ctr.z))
|
||||
L2 = bpy.context.object
|
||||
L2.data.energy = 90 * size * size + 15
|
||||
L2.data.size = size * 3
|
||||
|
||||
bpy.ops.object.camera_add()
|
||||
cam = bpy.context.object
|
||||
scn.camera = cam
|
||||
cam.data.type = 'ORTHO'
|
||||
cam.data.ortho_scale = size * 1.5
|
||||
d = Vector((0.82, -1.0, 0.62)).normalized()
|
||||
cam.location = ctr + d * (size * 4 + 2)
|
||||
cam.rotation_euler = (-d).to_track_quat('-Z', 'Y').to_euler()
|
||||
cam.data.clip_end = size * 20 + 50
|
||||
|
||||
scn.render.filepath = os.path.join(OUT_DIR, os.path.basename(path)[:-4] + ".png")
|
||||
bpy.ops.render.render(write_still=True)
|
||||
ok += 1
|
||||
print(f"THUMBS_DONE {ok}/{len(FILES)} -> {OUT_DIR}")
|
||||
|
||||
|
||||
main()
|
||||
631
web/assets/motion_manifest.json
Normal file
@ -0,0 +1,631 @@
|
||||
{
|
||||
"_schema": "PROCITY motion manifest v1 (R41 41.1). groups{<file>} -> one GLB under web/models/clips/, holding N named animations. clips{<clipId>} -> {group,category,duration,loopable,source,mixamo,pair?}. Load a group ONCE (gltf.animations[] are named by clipId); index by clips[id].group.",
|
||||
"_licence": "Mixamo (Adobe) via ultra ~/Documents/MOTIONLIB/green/mixamo_full/. GREEN: free to use inside games/projects, NO attribution required. Never redistribute as a clip pack. R41 ships this licence regime only (ruling 2).",
|
||||
"_tracks": "rotation-only",
|
||||
"_encoding": "rotation outputs are int16 NORMALIZED (glTF-legal, three.js r175 de-normalises in GLTFLoader._getArrayFromAccessor); all 65 joints animated incl. fingers",
|
||||
"_note": "Skeleton-only, 0 tris, 66 nodes (65 mixamorig joints + Armature) \u2014 the same byte-shape as web/models/peds/idle.glb. rigs.js `_canon` folds the mixamorigN: prefix, `_rotOnly` keeps quaternions and drops Hips.quaternion.",
|
||||
"fps": 30,
|
||||
"groups": {
|
||||
"idles.glb": {
|
||||
"category": "idle",
|
||||
"desc": "the clone-army killer: 10 distinct standing idles",
|
||||
"clips": [
|
||||
"idle_breathing",
|
||||
"idle_standing",
|
||||
"idle_look_around",
|
||||
"idle_weight_shift",
|
||||
"idle_cocky_lean",
|
||||
"idle_yawn",
|
||||
"idle_happy_1",
|
||||
"idle_happy_2",
|
||||
"idle_old_man",
|
||||
"idle_smoking"
|
||||
],
|
||||
"bytes": 1029496,
|
||||
"nodes": 66,
|
||||
"durationSum": 78.267
|
||||
},
|
||||
"browse.glb": {
|
||||
"category": "browse",
|
||||
"desc": "shop-floor: hold / examine / open / carry / pick up",
|
||||
"clips": [
|
||||
"browse_hold_idle",
|
||||
"browse_hold_walk",
|
||||
"browse_hold_turn_l",
|
||||
"browse_hold_turn_r",
|
||||
"browse_open_cabinet",
|
||||
"browse_close_cabinet",
|
||||
"browse_carry_box",
|
||||
"browse_pick_up"
|
||||
],
|
||||
"bytes": 476056,
|
||||
"nodes": 66,
|
||||
"durationSum": 30.833
|
||||
},
|
||||
"sitlean.glb": {
|
||||
"category": "sitlean",
|
||||
"desc": "benches and shopfronts: 4 seated + 4 leaning",
|
||||
"clips": [
|
||||
"sit_bench_swing",
|
||||
"sit_hands_thighs",
|
||||
"sit_impatient",
|
||||
"sit_fidget_feet",
|
||||
"lean_wall_postures",
|
||||
"lean_wall_leg_up",
|
||||
"lean_wall_shoulders",
|
||||
"lean_wall_walkman"
|
||||
],
|
||||
"bytes": 358112,
|
||||
"nodes": 66,
|
||||
"durationSum": 27.533
|
||||
},
|
||||
"social.glb": {
|
||||
"category": "social",
|
||||
"desc": "two-person street/shop talk; the handshake pair stays paired",
|
||||
"clips": [
|
||||
"social_conversation",
|
||||
"social_chat_water",
|
||||
"social_greeting",
|
||||
"social_shake_1",
|
||||
"social_shake_2",
|
||||
"social_nod_yes",
|
||||
"social_shake_no",
|
||||
"social_wave"
|
||||
],
|
||||
"bytes": 895648,
|
||||
"nodes": 66,
|
||||
"durationSum": 73.867
|
||||
},
|
||||
"locomotion.glb": {
|
||||
"category": "locomotion",
|
||||
"desc": "civilian walk + turn set (the named Degree_Turn set is rifle-aiming \u2014 substituted, see AUDIT)",
|
||||
"clips": [
|
||||
"walk_shopping_bag",
|
||||
"turn_left_90",
|
||||
"turn_right_90",
|
||||
"turn_walk_180",
|
||||
"turn_in_place",
|
||||
"walk_to_stand"
|
||||
],
|
||||
"bytes": 212776,
|
||||
"nodes": 66,
|
||||
"durationSum": 12.133
|
||||
},
|
||||
"venue.glb": {
|
||||
"category": "venue",
|
||||
"desc": "pub / gig / record shop",
|
||||
"clips": [
|
||||
"venue_clap_stand",
|
||||
"venue_clap_seated",
|
||||
"venue_cheer",
|
||||
"venue_northern_soul",
|
||||
"venue_bartending",
|
||||
"venue_headphones"
|
||||
],
|
||||
"bytes": 526036,
|
||||
"nodes": 66,
|
||||
"durationSum": 39.533
|
||||
}
|
||||
},
|
||||
"clips": {
|
||||
"idle_breathing": {
|
||||
"group": "idles.glb",
|
||||
"category": "idle",
|
||||
"duration": 9.9667,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Breathing_Idle__c9c6d0d5.fbx",
|
||||
"mixamo": "Breathing Idle",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"idle_standing": {
|
||||
"group": "idles.glb",
|
||||
"category": "idle",
|
||||
"duration": 6.0333,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Standing_Idle__c9c831cf.fbx",
|
||||
"mixamo": "Standing Idle",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"idle_look_around": {
|
||||
"group": "idles.glb",
|
||||
"category": "idle",
|
||||
"duration": 6.3667,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Idle_Stand_Looking_Around__c9c6cdf7.fbx",
|
||||
"mixamo": "Looking Around",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"idle_weight_shift": {
|
||||
"group": "idles.glb",
|
||||
"category": "idle",
|
||||
"duration": 9.4667,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Shifting_Weight_From_Side_To_Side__c9c908f7.fbx",
|
||||
"mixamo": "Weight Shift",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"idle_cocky_lean": {
|
||||
"group": "idles.glb",
|
||||
"category": "idle",
|
||||
"duration": 2.9333,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Cocky_Lean_Back__c9c90e3c.fbx",
|
||||
"mixamo": "Being Cocky",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"idle_yawn": {
|
||||
"group": "idles.glb",
|
||||
"category": "idle",
|
||||
"duration": 8.3667,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Big_Yawn_While_Standing__c9c98361.fbx",
|
||||
"mixamo": "Yawn",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"idle_happy_1": {
|
||||
"group": "idles.glb",
|
||||
"category": "idle",
|
||||
"duration": 2.0333,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.28,
|
||||
"source": "Happy_Idle_Variation_1__c9ccf37e.fbx",
|
||||
"mixamo": "Happy Idle",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"idle_happy_2": {
|
||||
"group": "idles.glb",
|
||||
"category": "idle",
|
||||
"duration": 2.9667,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Happy_Idle_Variation_2__c9ccf445.fbx",
|
||||
"mixamo": "Happy Idle",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"idle_old_man": {
|
||||
"group": "idles.glb",
|
||||
"category": "idle",
|
||||
"duration": 12.2,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Old_Man_Standing_Idle__c9c6c004.fbx",
|
||||
"mixamo": "Old Man Idle",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"idle_smoking": {
|
||||
"group": "idles.glb",
|
||||
"category": "idle",
|
||||
"duration": 17.9333,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.37,
|
||||
"source": "Idle_Smoking__c9c603fd.fbx",
|
||||
"mixamo": "Smoking",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"browse_hold_idle": {
|
||||
"group": "browse.glb",
|
||||
"category": "browse",
|
||||
"duration": 6.0333,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.06,
|
||||
"source": "Holding_An_Object_Idle__c9cda11a.fbx",
|
||||
"mixamo": "Holding Idle",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"browse_hold_walk": {
|
||||
"group": "browse.glb",
|
||||
"category": "browse",
|
||||
"duration": 1.4,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 6.35,
|
||||
"source": "Holding_An_Object_While_Walking__c9cda1e7.fbx",
|
||||
"mixamo": "Holding Walk",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"browse_hold_turn_l": {
|
||||
"group": "browse.glb",
|
||||
"category": "browse",
|
||||
"duration": 1.2333,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 172.54,
|
||||
"source": "Holding_An_Object_While_Turning_Left__c9cda2af.fbx",
|
||||
"mixamo": "Holding Turn Left",
|
||||
"channels": 65,
|
||||
"rotBones": 65,
|
||||
"pair": "browse_hold_turn_r"
|
||||
},
|
||||
"browse_hold_turn_r": {
|
||||
"group": "browse.glb",
|
||||
"category": "browse",
|
||||
"duration": 1.3333,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 147.31,
|
||||
"source": "Holding_An_Object_While_Turning_Right__c9cda378.fbx",
|
||||
"mixamo": "Holding Turn Right",
|
||||
"channels": 65,
|
||||
"rotBones": 65,
|
||||
"pair": "browse_hold_turn_l"
|
||||
},
|
||||
"browse_open_cabinet": {
|
||||
"group": "browse.glb",
|
||||
"category": "browse",
|
||||
"duration": 4.8333,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Opening_Cabinet_Door__c9c6d49e.fbx",
|
||||
"mixamo": "Opening",
|
||||
"channels": 65,
|
||||
"rotBones": 65,
|
||||
"pair": "browse_close_cabinet"
|
||||
},
|
||||
"browse_close_cabinet": {
|
||||
"group": "browse.glb",
|
||||
"category": "browse",
|
||||
"duration": 6.2,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Closing_Cabinet_Door__c9c6d55d.fbx",
|
||||
"mixamo": "Closing",
|
||||
"channels": 65,
|
||||
"rotBones": 65,
|
||||
"pair": "browse_open_cabinet"
|
||||
},
|
||||
"browse_carry_box": {
|
||||
"group": "browse.glb",
|
||||
"category": "browse",
|
||||
"duration": 6.3333,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.03,
|
||||
"source": "Carrying_A_Box__c9cd8d90.fbx",
|
||||
"mixamo": "Box Idle",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"browse_pick_up": {
|
||||
"group": "browse.glb",
|
||||
"category": "browse",
|
||||
"duration": 3.4667,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 92.15,
|
||||
"source": "Picking_Up_An_Object_With_One_Hand__c9c6c5eb.fbx",
|
||||
"mixamo": "Picking Up Object",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"sit_bench_swing": {
|
||||
"group": "sitlean.glb",
|
||||
"category": "sitlean",
|
||||
"duration": 2.1333,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Little_Girl_Sitting_On_A_Bench_Swinging_Her_Legs__c9c89165.fbx",
|
||||
"mixamo": "Sitting",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"sit_hands_thighs": {
|
||||
"group": "sitlean.glb",
|
||||
"category": "sitlean",
|
||||
"duration": 6.4333,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.05,
|
||||
"source": "Sitting_In_Chair_Hands_Resting_On_Thighs__c9cb9783.fbx",
|
||||
"mixamo": "Sitting Idle",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"sit_impatient": {
|
||||
"group": "sitlean.glb",
|
||||
"category": "sitlean",
|
||||
"duration": 7.0333,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Sitting_And_Impatiently_Waiting__c9ccad56.fbx",
|
||||
"mixamo": "Sitting",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"sit_fidget_feet": {
|
||||
"group": "sitlean.glb",
|
||||
"category": "sitlean",
|
||||
"duration": 1.2,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.19,
|
||||
"source": "Sit_With_Fidgeting_Feet__c9c699a5.fbx",
|
||||
"mixamo": "Sitting",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"lean_wall_postures": {
|
||||
"group": "sitlean.glb",
|
||||
"category": "sitlean",
|
||||
"duration": 2.0333,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Leaning_Against_A_Wall_With_Various_Postures__c9c695f0.fbx",
|
||||
"mixamo": "One Shoulder Lean",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"lean_wall_leg_up": {
|
||||
"group": "sitlean.glb",
|
||||
"category": "sitlean",
|
||||
"duration": 3.3667,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Leaning_Square_Against_A_Wall_With_One_Leg_Up__c9c696b0.fbx",
|
||||
"mixamo": "Leaning",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"lean_wall_shoulders": {
|
||||
"group": "sitlean.glb",
|
||||
"category": "sitlean",
|
||||
"duration": 3.3667,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Leaning_Square_With_Shoulders_Against_A_Wall__c9c69770.fbx",
|
||||
"mixamo": "Leaning",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"lean_wall_walkman": {
|
||||
"group": "sitlean.glb",
|
||||
"category": "sitlean",
|
||||
"duration": 1.9667,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Little_Girl_Leaning_On_A_Wall_Listening_To_Music__c9c8bace.fbx",
|
||||
"mixamo": "Leaning On A Wall",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"social_conversation": {
|
||||
"group": "social.glb",
|
||||
"category": "social",
|
||||
"duration": 10.3,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.29,
|
||||
"source": "General_Conversation__c9c88301.fbx",
|
||||
"mixamo": "Talking",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"social_chat_water": {
|
||||
"group": "social.glb",
|
||||
"category": "social",
|
||||
"duration": 44.6,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.78,
|
||||
"source": "Having_A_Chat_At_The_Watercooler__c9c9f1c9.fbx",
|
||||
"mixamo": "Talking At Watercooler",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"social_greeting": {
|
||||
"group": "social.glb",
|
||||
"category": "social",
|
||||
"duration": 5.1333,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Greeting_While_Standing__c9c7996a.fbx",
|
||||
"mixamo": "Standing Greeting",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"social_shake_1": {
|
||||
"group": "social.glb",
|
||||
"category": "social",
|
||||
"duration": 4.4,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 0.03,
|
||||
"source": "2_People_Shaking_Hands_Part_1_-_Female__c9c9f343.fbx",
|
||||
"mixamo": "Shaking Hands 1",
|
||||
"channels": 65,
|
||||
"rotBones": 65,
|
||||
"pair": "social_shake_2"
|
||||
},
|
||||
"social_shake_2": {
|
||||
"group": "social.glb",
|
||||
"category": "social",
|
||||
"duration": 4.4,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 0.13,
|
||||
"source": "2_People_Shaking_Hands_Part_2_-_Male__c9c9f403.fbx",
|
||||
"mixamo": "Shaking Hands 2",
|
||||
"channels": 65,
|
||||
"rotBones": 65,
|
||||
"pair": "social_shake_1"
|
||||
},
|
||||
"social_nod_yes": {
|
||||
"group": "social.glb",
|
||||
"category": "social",
|
||||
"duration": 2.6333,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Nodding_Head_Yes__c9c90d7b.fbx",
|
||||
"mixamo": "Head Nod Yes",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"social_shake_no": {
|
||||
"group": "social.glb",
|
||||
"category": "social",
|
||||
"duration": 1.8333,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Shaking_Head_No_Dismissively__c9c90b3c.fbx",
|
||||
"mixamo": "Shaking Head No",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"social_wave": {
|
||||
"group": "social.glb",
|
||||
"category": "social",
|
||||
"duration": 0.5667,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Waving__c9c5ed32.fbx",
|
||||
"mixamo": "Waving",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"walk_shopping_bag": {
|
||||
"group": "locomotion.glb",
|
||||
"category": "locomotion",
|
||||
"duration": 1.3,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.03,
|
||||
"source": "Male_Walking_With_Shopping_Bag__c9c64027.fbx",
|
||||
"mixamo": "Walking With Shopping Bag",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"turn_left_90": {
|
||||
"group": "locomotion.glb",
|
||||
"category": "locomotion",
|
||||
"duration": 1.5333,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 84.6,
|
||||
"source": "Turning_90_Degrees_Left__c9ceef5f.fbx",
|
||||
"mixamo": "Unarmed Turn Left 90",
|
||||
"channels": 65,
|
||||
"rotBones": 65,
|
||||
"pair": "turn_right_90"
|
||||
},
|
||||
"turn_right_90": {
|
||||
"group": "locomotion.glb",
|
||||
"category": "locomotion",
|
||||
"duration": 1.5333,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 83.16,
|
||||
"source": "Turning_90_Degrees_Right__c9cef01d.fbx",
|
||||
"mixamo": "Unarmed Turn Right 90",
|
||||
"channels": 65,
|
||||
"rotBones": 65,
|
||||
"pair": "turn_left_90"
|
||||
},
|
||||
"turn_walk_180": {
|
||||
"group": "locomotion.glb",
|
||||
"category": "locomotion",
|
||||
"duration": 1.0333,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 180.0,
|
||||
"source": "Turning_180_Degrees_While_Walking__c9c9c73f.fbx",
|
||||
"mixamo": "Walking Turn 180",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"turn_in_place": {
|
||||
"group": "locomotion.glb",
|
||||
"category": "locomotion",
|
||||
"duration": 3.7,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 36.68,
|
||||
"source": "Turning_In_Place__c9c70ef0.fbx",
|
||||
"mixamo": "Turning",
|
||||
"channels": 65,
|
||||
"rotBones": 65,
|
||||
"loopDemoted": "curated loopable but loop seam 36.7 deg > 25.0"
|
||||
},
|
||||
"walk_to_stand": {
|
||||
"group": "locomotion.glb",
|
||||
"category": "locomotion",
|
||||
"duration": 3.0333,
|
||||
"loopable": false,
|
||||
"loopSeamDeg": 55.33,
|
||||
"source": "Walking_To_Standing_Idle__c9c8d966.fbx",
|
||||
"mixamo": "Stop Walking",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"venue_clap_stand": {
|
||||
"group": "venue.glb",
|
||||
"category": "venue",
|
||||
"duration": 1.2,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.07,
|
||||
"source": "Clap_While_Standing__c9c60ef2.fbx",
|
||||
"mixamo": "Clapping",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"venue_clap_seated": {
|
||||
"group": "venue.glb",
|
||||
"category": "venue",
|
||||
"duration": 1.2,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.07,
|
||||
"source": "Clap_While_Seated__c9c60fce.fbx",
|
||||
"mixamo": "Clapping",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"venue_cheer": {
|
||||
"group": "venue.glb",
|
||||
"category": "venue",
|
||||
"duration": 2.9333,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.04,
|
||||
"source": "Male_Cheering_With_Two_Fists_Pump__c9c66dfd.fbx",
|
||||
"mixamo": "Cheering",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"venue_northern_soul": {
|
||||
"group": "venue.glb",
|
||||
"category": "venue",
|
||||
"duration": 16.1,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.41,
|
||||
"source": "Basic_Northern_Soul_Step__c9cbe56d.fbx",
|
||||
"mixamo": "Northern Soul Dance",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"venue_bartending": {
|
||||
"group": "venue.glb",
|
||||
"category": "venue",
|
||||
"duration": 13.6,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.03,
|
||||
"source": "Pouring_And_Serving_Drinks__c9c78799.fbx",
|
||||
"mixamo": "Bartending",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
},
|
||||
"venue_headphones": {
|
||||
"group": "venue.glb",
|
||||
"category": "venue",
|
||||
"duration": 4.5,
|
||||
"loopable": true,
|
||||
"loopSeamDeg": 0.03,
|
||||
"source": "Male_With_Headphones_Listening_To_Music__c9c63329.fbx",
|
||||
"mixamo": "Listening To Music",
|
||||
"channels": 65,
|
||||
"rotBones": 65
|
||||
}
|
||||
},
|
||||
"totalBytes": 3498124,
|
||||
"clipCount": 46,
|
||||
"groupCount": 6,
|
||||
"dedupSavedBytes": 1341724
|
||||
}
|
||||
BIN
web/assets/thumbs/procity_fit_aircon_unit_01.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
web/assets/thumbs/procity_fit_ash_urn_01.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
web/assets/thumbs/procity_fit_ashtray_post_01.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
web/assets/thumbs/procity_fit_bain_marie_01.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
web/assets/thumbs/procity_fit_banquette_01.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
web/assets/thumbs/procity_fit_beam_roof_01.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
web/assets/thumbs/procity_fit_beer_umbrella_01.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
web/assets/thumbs/procity_fit_bin_big_4x4_01.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
web/assets/thumbs/procity_fit_bin_endoffunk_01.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
web/assets/thumbs/procity_fit_bin_round_01.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
web/assets/thumbs/procity_fit_bistro_table_01.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
web/assets/thumbs/procity_fit_broken_basin_01.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
web/assets/thumbs/procity_fit_cable_snake_01.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
web/assets/thumbs/procity_fit_can_stack_01.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
web/assets/thumbs/procity_fit_cart_case_01.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
web/assets/thumbs/procity_fit_cart_concorde_01.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
web/assets/thumbs/procity_fit_cart_m447_01.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
web/assets/thumbs/procity_fit_cart_stanton_01.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
web/assets/thumbs/procity_fit_cdj_deck_01.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
web/assets/thumbs/procity_fit_champ_bucket_01.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
web/assets/thumbs/procity_fit_chiu_bin_01.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
web/assets/thumbs/procity_fit_chiu_bin_half_01.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
web/assets/thumbs/procity_fit_chiu_bin_low_01.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
web/assets/thumbs/procity_fit_chiu_rack_long_01.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
web/assets/thumbs/procity_fit_chiu_rack_short_01.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
web/assets/thumbs/procity_fit_column_square_01.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
web/assets/thumbs/procity_fit_column_wide_01.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
web/assets/thumbs/procity_fit_concrete_plinth_01.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
web/assets/thumbs/procity_fit_counter_station_01.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
web/assets/thumbs/procity_fit_counter_station_side_01.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
web/assets/thumbs/procity_fit_crate_blue_01.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
web/assets/thumbs/procity_fit_crowd_barrier_01.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
web/assets/thumbs/procity_fit_deck_1200_01.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
web/assets/thumbs/procity_fit_deck_1200_rigged_01.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
web/assets/thumbs/procity_fit_distro_box_01.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
web/assets/thumbs/procity_fit_door_shop_01.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
web/assets/thumbs/procity_fit_drum_808_01.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
web/assets/thumbs/procity_fit_drum_tr1000_01.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
web/assets/thumbs/procity_fit_fire_ext_01.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
web/assets/thumbs/procity_fit_firepit_01.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
web/assets/thumbs/procity_fit_flight_case_01.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
web/assets/thumbs/procity_fit_headphones_hd25_01.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
web/assets/thumbs/procity_fit_headshell_technics_01.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
web/assets/thumbs/procity_fit_jukebox_01.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
web/assets/thumbs/procity_fit_keg_01.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
web/assets/thumbs/procity_fit_library_trolley_01.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
web/assets/thumbs/procity_fit_lounge_chair_01.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
web/assets/thumbs/procity_fit_marble_sink_01.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
web/assets/thumbs/procity_fit_mixer_mx2200_01.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
web/assets/thumbs/procity_fit_mixer_pmc05pro_01.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
web/assets/thumbs/procity_fit_mixer_pmc05pro_rigged_01.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
web/assets/thumbs/procity_fit_mixer_pmc06pro_01.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
web/assets/thumbs/procity_fit_mixer_pmc07pro_01.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
web/assets/thumbs/procity_fit_mixer_pmc08pro_01.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
web/assets/thumbs/procity_fit_mixer_pmc10_01.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
web/assets/thumbs/procity_fit_mixer_pmc20_01.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
web/assets/thumbs/procity_fit_mixer_pmx9000_01.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
web/assets/thumbs/procity_fit_mixer_shdj1200_01.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
web/assets/thumbs/procity_fit_mixer_shdj1200_rigged_01.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
web/assets/thumbs/procity_fit_mixer_ttm54i_01.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
web/assets/thumbs/procity_fit_mixer_ttm54i_rigged_01.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
web/assets/thumbs/procity_fit_mos_bin_bottom_01.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
web/assets/thumbs/procity_fit_mos_bin_lower_mid_01.png
Normal file
|
After Width: | Height: | Size: 71 KiB |