Lane E round 4 (E2): fix record_crate + ship counter-with-till, both live on depot

- record_crate: R3 normalize left a sourceless texture (packed AO/roughness map
  didn't survive WebP export; metallicRoughness+occlusion pointed at it) → three.js
  'undefined uri' crash. New pipeline/fix_glb_textures.py strips material slots that
  reference a sourceless texture. Verified loads in vendored GLTFLoader; re-published.
- counter_till (new fitting): ~2m timber counter + period till, flux_local→trellis_mac
  on-device, 1.61x0.64m / 1.15m tall, 32k tris. Verified loads; published. Lane C maps
  kind counter -> counter_till (balcao 4m 'counter' kept for long-bar use).
- publish.py: --only <name> filter + passwordless tailnet detection (GOD3_DEPOT override).
- manifest rebuilt (15 fittings, alias counter->counter_till); validate --depot 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-14 19:07:15 +10:00
parent aad9baa584
commit dafb079ff4
12 changed files with 216 additions and 10 deletions

View File

@ -4,6 +4,33 @@
Reviewed against `docs/LANES/LANE_E_ASSETS.md` + `docs/LANES/ROUND3_INSTRUCTIONS.md`. Nothing in
other lanes' files was touched.*
## Round 4 (Fable) — ped decimation (gate-3 critical path) + asset fixes
- **E1 — ped decimation: DONE + committed** (`518678d`, ref `lane-e/round4-peds`). The 19 ped rigs
in `web/models/peds/` were **924k tris total** (31k56k each) — the gate-3 blocker. Decimated to
**≤3k each** (15642805; fleet **924k→46.5k**, ~2.45k avg) via `gltf-transform weld → simplify
--ratio 0.05 --error 0.12` (meshoptimizer — works on these manifold skinned meshes, unlike R3
TRELLIS shell-soup). **Skinning + skeleton fully preserved** (65 joints intact, `walk`/`idle`
untouched); contact sheet `docs/shots/laneE/peds_decimated.png` clean at street distance. Originals
gitignored in `pipeline/.peds_orig/`; per-ped in `pipeline/_peds_decim.json`. → **Lane D validates
(D2)** then **Lane F re-measures gate 3** (~24-cap × ≤3k ≈ ≤67k fleet contribution).
- **E2a — `record_crate` FIXED, verified + published.** Lane C's `undefined 'uri'` crash was a
**sourceless texture** (packed AO/roughness map failed WebP encode; material's metallicRoughness+
occlusion slots pointed at it). New tool `pipeline/fix_glb_textures.py` strips those slots (three.js
loads textures lazily → no crash). **Verified it loads in the real vendored `GLTFLoader`** (meshes=1,
tris=5000). **Re-published to the depot** (the fixed version is live, tex refs all-sourced).
- **E2b — counter-with-till (~2 m): LIVE.** New `counter_till` fitting
(`procity_fit_counter_till_01.glb`, **1.61 × 0.64 m, 1.15 m tall**, 32k tris) — timber counter +
cabinet doors + period till. Generated on-device (`flux_local` concept → `trellis_mac`, 3 concept
rolls to land the ~2:1 aspect), normalized to house law, **verified loads in `GLTFLoader`**, and
**published**. Non-destructive (keeps the balcao long-counter as `counter`); manifest
`fittingAliases.counter``counter_till`; Lane C re-maps kind `counter``counter_till` in `glb.js`.
- **Publish: DONE** (John approved). Both re-exports live; **manifest `validate --depot` GREEN
(0 err, 23 GLBs live)**. `publish.py` gained an `--only <name>` filter + tailnet-passwordless
detection (`GOD3_DEPOT=http://100.94.195.115:8788`, decision #4).
- **Not doing** (per Fable decisions): fal.ai spend (glass-case/bus-shelter parked); hero-prop
bake-to-lowpoly.
## Round 3 (Fable) — hero props on MODELBEAST + local-depot mode
- **Hero props, free & on-device** (`pipeline/gen_props.py`): `flux_local` concept image → `trellis_mac`

View File

@ -0,0 +1,49 @@
# LANE E — cross-lane notes (round 4)
## → Lane D (task D2): decimated peds ready to validate
The 19 ped rigs in `web/models/peds/` are decimated and staged (committed `518678d`, ref
`lane-e/round4-peds`). Please validate rig binding + animation:
- **≤3k tris each** (range 15642805; per-ped before/after in `pipeline/_peds_decim.json`).
Fleet total **924k → 46.5k tris** (48.6k → 2.45k avg). At the 24-rig near cap ≈ **≤67k**.
- **Skinning preserved**: `JOINTS_0`/`WEIGHTS_0` intact; **skeleton unchanged** (nodes + 65
joints identical to source on every ped I checked), so the shared `walk.glb`/`idle.glb`
clips retarget unchanged. **`walk.glb`/`idle.glb` were NOT touched.**
- Method: `gltf-transform weld``simplify --ratio 0.05 --error 0.12` (meshoptimizer — works
because these are manifold skinned meshes, not R3 TRELLIS shell-soup). Contact sheet:
`docs/shots/laneE/peds_decimated.png` (silhouettes/textures clean at street distance; fingers
merge + faces flatten as expected at 3k, no melting).
- Originals backed up (gitignored) at `pipeline/.peds_orig/` if you need a before/after diff.
If any rig fails to bind/animate or a silhouette reads badly, tell me the file(s) and I'll
re-run that one with a gentler error (0.08 keeps ~50% more tris but still <5k). Then F re-measures
gate 3 after your sign-off.
## → Lane C: E2 asset re-exports
1. **`procity_fit_record_crate_01.glb` — FIXED.** Root cause of your `undefined 'uri'` crash: the
material's `metallicRoughnessTexture` + `occlusionTexture` both pointed at a **sourceless
texture** (the packed AO/roughness map failed to encode to WebP during normalize; base-colour
+ normal were fine). Fix: `pipeline/fix_glb_textures.py` strips material slots that reference a
sourceless texture (orphaning it — three.js loads textures lazily, so no crash). **Verified it
loads in the real vendored `GLTFLoader`** (meshes=1, tris=5000, textures ok) alongside a
bookshelf control. Re-published to the depot — re-map `record_crate`/`recordBin` in `glb.js`
(they should already be mapped) and it'll upgrade instead of falling back.
2. **counter-with-till — LIVE.** New fitting `counter_till` (`procity_fit_counter_till_01.glb`):
**1.61 × 0.64 m footprint, 1.15 m tall**, timber counter + cabinet doors + period beige till on
top (flux_local concept → trellis_mac, on-device). Fixes the R3 4 m till-less counter that
squashed. **Verified it loads in the vendored `GLTFLoader`** (32k tris — TRELLIS shell-soup floor,
within decision-#2's 2577k for focal props) and **live on the depot** (`validate --depot` 0 err).
**In `glb.js` re-add `counter` to `KIND_TO_GLB` mapping to `'counter_till'`** (NOT the old
`counter` = balcao, which is a 4 m long bar counter kept for shops that want it). The manifest
`fittingAliases.counter``counter_till` too. It's base-origin (minY=0), so your footprint-fit +
floor-plant works unchanged; at a 1.42.0 m counter slot it scales to a believable ~1.01.4 m
tall counter.
## → Fable: status
- **E1 (gate-3 critical path): DONE.** Peds ≤3k, committed + pinned. Unblocks D2 → F3.
- **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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

View File

@ -179,3 +179,18 @@ intact, skeleton unchanged (nodes + 65 joints identical to source), so the share
(fine for street distance). Originals backed up (gitignored) at `pipeline/.peds_orig/`; per-ped
before/after in `pipeline/_peds_decim.json`. Lane D validates rig binding + anims (task D2); Lane F
re-measures gate 3 (~24-rig near cap × ≤3k ≈ ≤67k fleet contribution).
## Round 4 — E2 asset re-exports (published)
- **`procity_fit_record_crate_01.glb` re-export (fixed).** The R3 normalize left a **sourceless
texture** (packed AO/roughness map didn't survive the WebP export; the material's
`metallicRoughnessTexture` + `occlusionTexture` pointed at it), which crashed three.js GLTFLoader
with `undefined 'uri'`. `pipeline/fix_glb_textures.py` strips material slots that reference a
sourceless texture (base-colour + normal kept; the orphan is never loaded). Verified it loads in
the vendored `GLTFLoader` (5000 tris, textures ok); re-published to the depot.
- **`procity_fit_counter_till_01.glb` (new).** ~2 m timber shop counter with a period beige till —
`flux_local` concept (3 rolls for the ~2:1 aspect) → `trellis_mac` mesh → `normalize.py`. 1.61 ×
0.64 m, 1.15 m tall, 32k tris (TRELLIS shell-soup floor, within decision-#2's 2577k for focal
props). **Provenance: 100% synthetic/on-device** (FLUX.2-klein-4B + TRELLIS.2 on the M3 Ultra), no
third-party source — cleanest licence zone. Replaces the R3 4 m till-less counter for the interior
counter slot (Lane C maps `counter``counter_till`); the balcao 4 m `counter` stays for long-bar use.

View File

@ -84,6 +84,23 @@
1.91,
0.93
]
},
{
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/_m_counter3/trellis2_counter_till_v3.glb",
"file": "procity_fit_counter_till_01.glb",
"out": "/tmp/counter_norm/procity_fit_counter_till_01.glb",
"thumb": "web/assets/thumbs/procity_fit_counter_till_01.png",
"tri_before": 195285,
"tri_after": 32234,
"size_m": [
1.609,
1.15,
0.642
],
"footprint": [
1.61,
0.64
]
}
],
"errors": []

View File

@ -5,6 +5,7 @@
"procity_fit_clothes_rack_01.glb",
"procity_fit_coffee_table_01.glb",
"procity_fit_counter_01.glb",
"procity_fit_counter_till_01.glb",
"procity_fit_cube_shelf_wide_01.glb",
"procity_fit_drinks_fridge_01.glb",
"procity_fit_listening_booth_01.glb",
@ -19,6 +20,5 @@
"procity_street_longbench_01.glb",
"procity_street_novelty_record_01.glb",
"procity_street_park_bench_01.glb",
"procity_street_streetlight_01.glb",
"vintage-cash-register.glb"
"procity_street_streetlight_01.glb"
]

View File

@ -0,0 +1,3 @@
[
{"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/_m_counter3/trellis2_counter_till_v3.glb", "id": "counter_till", "category": "fit", "height_m": 1.15, "tris": 8000}
]

View File

@ -168,7 +168,8 @@ def build():
# stay primitives — see MESHGOD_BATCH.md). Lets a consumer that iterates registry SHOP_TYPES.fittings
# resolve to a GLB. Lane C's glb.js already maps its own kinds straight to the physical ids below.
fitting_aliases = {
"record_bins": "record_crate", "crates": "box_crate", "counter": "counter",
# registry 'counter' → the ~2m counter-with-till (counter_till), NOT the 4m balcao 'counter'
"record_bins": "record_crate", "crates": "box_crate", "counter": "counter_till",
"clothes_racks": "clothes_rack", "bricabrac_shelving": "wire_shelf", "book_wall": "bookshelf",
"cube_shelves": "cube_shelf_wide", "display_tables": "coffee_table",
"bookshelves": "bookshelf", "vhs_shelving": "wire_shelf", "trestle_tables": "work_table",

View File

@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Repair a GLB whose material references a texture with no image source.
Blender's WebP glTF export occasionally emits a texture slot whose image failed to encode
(no `source`, no `EXT_texture_webp.source`) e.g. a packed metallic-roughness/AO map. three.js
GLTFLoader then throws `Cannot read properties of undefined (reading 'uri')` when a material points
at it. This strips every material texture-slot that points at a sourceless texture (base-colour /
normal are kept; the orphaned texture is never loaded because GLTFLoader loads textures lazily),
then repacks the GLB (BIN chunk untouched).
python3 fix_glb_textures.py in.glb out.glb
"""
import json, struct, sys
SLOTS = ("baseColorTexture", "metallicRoughnessTexture", "normalTexture",
"occlusionTexture", "emissiveTexture")
def read_glb(path):
d = open(path, "rb").read()
off, gltf, chunks = 12, None, []
while off < len(d):
clen, ctype = struct.unpack("<II", d[off:off + 8])
body = d[off + 8: off + 8 + clen]
if ctype == 0x4E4F534A:
gltf = json.loads(body.decode("utf-8"))
chunks.append((ctype, body))
off += 8 + clen
return gltf, chunks
def has_source(tex):
if "source" in tex:
return True
ext = tex.get("extensions", {})
return any("source" in v for v in ext.values() if isinstance(v, dict))
def repair(gltf):
textures = gltf.get("textures", [])
bad = {i for i, t in enumerate(textures) if not has_source(t)}
if not bad:
return 0
stripped = 0
for m in gltf.get("materials", []):
pbr = m.get("pbrMetallicRoughness", {})
for container in (m, pbr):
for slot in list(container.keys()):
if slot in SLOTS and isinstance(container[slot], dict) \
and container[slot].get("index") in bad:
del container[slot]
stripped += 1
return stripped
def write_glb(gltf, chunks, out):
new_json = json.dumps(gltf, separators=(",", ":")).encode("utf-8")
new_json += b" " * ((4 - len(new_json) % 4) % 4) # pad to 4 bytes
body = struct.pack("<II", len(new_json), 0x4E4F534A) + new_json
for ctype, data in chunks:
if ctype == 0x4E4F534A:
continue
pad = b"\x00" * ((4 - len(data) % 4) % 4)
body += struct.pack("<II", len(data) + len(pad), ctype) + data + pad
header = struct.pack("<III", 0x46546C67, 2, 12 + len(body))
open(out, "wb").write(header + body)
def main():
inp, out = sys.argv[1], sys.argv[2]
gltf, chunks = read_glb(inp)
n = repair(gltf)
write_glb(gltf, chunks, out)
print(f"stripped {n} material slot(s) pointing at sourceless textures → {out}")
if __name__ == "__main__":
main()

View File

@ -60,13 +60,20 @@ def post(path, body, ctype):
def main():
glbs = sorted(g for g in glob.glob(NORM + "/*.glb") if os.path.basename(g) not in SKIP)
only = sys.argv[sys.argv.index("--only") + 1] if "--only" in sys.argv else ""
# 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 not glbs:
print(f"no GLBs in {NORM} — run normalize.py first"); sys.exit(1)
print(f"depot: {DEPOT} assets: {len(glbs)} {'(DRY RUN)' if DRY else ''}")
if not DRY and not PW:
print("ERROR: GOD3_PW not set in environment. Ask John for the depot password, then:\n"
" export GOD3_PW=... && python3 pipeline/publish.py")
print(f"no GLBs in {NORM} matching {only!r} — run normalize.py first"); sys.exit(1)
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:
print("ERROR: GOD3_PW not set and no tailnet GOD3_DEPOT. Either:\n"
" export GOD3_DEPOT=http://100.94.195.115:8788 # passwordless from an allowlisted box\n"
" or export GOD3_PW=... # public depot (ask John)")
sys.exit(2)
published = []
for g in glbs:

View File

@ -684,6 +684,15 @@
"height": 0.5,
"thumb": "thumbs/procity_fit_milkshake_mixer_01.png"
},
"counter_till": {
"file": "procity_fit_counter_till_01.glb",
"footprint": [
1.61,
0.64
],
"height": 1.15,
"thumb": "thumbs/procity_fit_counter_till_01.png"
},
"cash_register": {
"file": "vintage-cash-register.glb",
"footprint": [
@ -770,7 +779,7 @@
"fittingAliases": {
"record_bins": "record_crate",
"crates": "box_crate",
"counter": "counter",
"counter": "counter_till",
"clothes_racks": "clothes_rack",
"bricabrac_shelving": "wire_shelf",
"book_wall": "bookshelf",

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB