Lane E R16 (v3.1): the drummer's sit clip — sit.glb produced + published

Ledger #2 asset half (E produces, D wires). Fetched Adobe Mixamo Sitting_Idle.fbx (533KB) by scp
from the tailnet source box, converted on-device via Blender (new pipeline/fbx_to_clip.py: import ->
skinned GLB export -> pure-python mesh-strip) to web/models/peds/sit.glb (109KB, mesh-free,
structurally identical to walk/idle: 66 nodes, 65 mixamorig bones, dangling skin, 195 channels).
Verified: three.js loads + 195/195 tracks bind to peds + 0 NaN; Blender confirms seated (hips
0.955->0.299m, stature 1.276->0.758m) + eyes-on bone-dot render (docs/shots/laneE/sit_clip.png).
Published to depot via tailnet under John's R13 auth (_published.json 38->39; benign non-procity_
drift note). Provenance: Adobe Mixamo royalty-free, same zone as the shipped walk/idle; LANE_E_NOTES
v3.1 amendment marker.

-> Lane D: sit.glb ready at web/models/peds/sit.glb (+ on depot); canon binds 195/195; seated confirmed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-16 12:37:27 +10:00
parent 2b13dd4ad7
commit b322c998db
5 changed files with 174 additions and 0 deletions

View File

@ -1,5 +1,43 @@
# LANE E — cross-lane notes
## Round 16 — v3.1 THE FLIP: the drummer's sit clip (E asset half) ⟨v3.1 amendment⟩
> Ledger #2 asset half — the drummer finally sits. **E produces the clip; D wires it** (`_rotWithHips`
> + `spawnRig` foot re-plant + F's seated-stature gate). **`sit.glb` delivered, verified, committed +
> on depot.** → **Lane D: it's ready** (`web/models/peds/sit.glb`, canon binds 195/195, seated confirmed).
**Asset:** `web/models/peds/sit.glb` — a mesh-free animation clip, **structurally identical to the
shipped `walk.glb`/`idle.glb`**: 66 nodes (65 `mixamorig` bones + root), 0 meshes, 1 dangling skin
(inverseBindMatrices + 65 joints, referenced by 0 nodes — the vestigial artifact Blender glTF I/O
leaves, which makes glTF tools read it as an armature), 195 channels (65 bones × TRS). 109 KB, no Draco.
**Source + provenance:** Adobe **Mixamo `Sitting_Idle.fbx`** (533 KB) — fetched by `scp` from the
tailnet source box `johnking@100.91.239.7:~/Documents/mixamo-fetch/out/` per Lane D's recipe (the
local `~/Documents/mixamo-fetch` path is stale/empty). Converted on-device via Blender headless
(`pipeline/fbx_to_clip.py`, **new tool**): import FBX → export skinned GLB → pure-python mesh-strip
that keeps the skeleton + dangling skin + animation and re-indexes/compacts. Raw Mixamo `mixamorig:`
naming is already canonical (`rigs.js._canon` normalizes `mixamorig<N>``mixamorig`, so no bone
rename). **Licence zone: Adobe Mixamo royalty-free animation license — the SAME zone as the
already-shipped `walk`/`idle` clips** (the whole ped fleet is Mixamo-rigged). Mesh stripped, so only
the skeleton *motion* ships (retargeted onto the game's own peds), never Mixamo character geometry.
**Verified (E's half — D verifies the wired in-scene pose):**
- **three.js** (vendored GLTFLoader): loads, dur 4.33 s, 195 tracks, 65 bones, **195/195 bind** to a
ped skeleton after `_canon`, **0 NaN** track values (mesh-strip re-index is clean).
- **Blender** (the tool that measures `walk` at 1.276 m standing): the clip poses **seated** — Hips
descend 0.955 m (standing) → **0.299 m**, stature 1.276 → 0.758 m. Eyes-on bone-dot render confirms
the folded seated silhouette vs walk's upright one: `docs/shots/laneE/sit_clip.png` (left walk, right sit).
**Depot:** published to `digalot.fyi/3god` via the passwordless tailnet ingress under **John's R13
authorization** (`_published.json` 38 → 39; confirmed 200 on the public depot). **NOTE (benign):**
`sit.glb` — like `vintage-cash-register.glb` — is non-`procity_`-prefixed, so `publish.py --verify`'s
`procity_`-only drift filter lists it as "not live" (a **false drift** — it IS live). Cosmetic only;
`validate_manifest` gates are unaffected (clips are not manifest GLBs).
**"Manifest":** ped clips load by **convention** (`rigs.js` `base='models/peds/'`), NOT via
`web/assets/manifest.json` — so there is no asset-manifest entry, same as `walk`/`idle`. D owns the
loader wiring (`spawnRig`/fleet). Bench-sit loiter (Fable's stretch goal) reuses this same clip.
## Round 14 — v3.0 PROVENANCE FREEZE ✅ (release round)
> **FROZEN for the v3.0 tag** (2026-07-16). Lane E R14 is a provenance close-out — **no new synthesis**
@ -30,6 +68,7 @@
| Skins (2D) | 96 | 90sDJsim ports (7 facade + 2 ground) + thriftgod R1 store-art + `flux_local` gap-fill fronts/posters | `gen_skins.py --local` (on-device) | 🟢 web-ok |
| Audio pack | 28 | 100% procedural numpy synthesis (`gen_audio.py`) | oscillator/noise render → ffmpeg OGG+M4A | 🟢 CC0-equiv, parody-safe |
| Stock packs | 3 (350/311/273) | GODVERSE `dealgod` Postgres — **real** cover art | `build_stock_pack.py`: parody-transform metadata + atlas | 🟢 real-art / parody-metadata |
| Ped clip **⟨v3.1⟩** | `sit.glb` (E R16; + pre-existing `walk`/`idle` from D/90sDJsim) | Adobe **Mixamo** `Sitting_Idle.fbx` | `fbx_to_clip.py` (Blender, mesh-strip) | 🟢 Mixamo royalty-free (motion only, mesh stripped) |
**GLB fleet = 33 manifest (23 fit + 10 furniture) + 5 `_hi` = 38 published.** Only external spend ever:
~$0.64 (fal-Hunyuan, R5). Everything else is $0 / on-device (electricity only).

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -36,5 +36,6 @@
"procity_street_novelty_record_01.glb",
"procity_street_park_bench_01.glb",
"procity_street_streetlight_01.glb",
"sit.glb",
"vintage-cash-register.glb"
]

134
pipeline/fbx_to_clip.py Normal file
View File

@ -0,0 +1,134 @@
"""PROCITY mixamo-clip converter — a Mixamo `.fbx` animation → a mesh-free house clip GLB, the exact
shape of the shipped `web/models/peds/walk.glb` / `idle.glb`: armature + one animation, NO mesh, but
WITH the (now dangling) skin that Blender's glTF I/O leaves behind — 65 `mixamorig` joints +
inverseBindMatrices, referenced by zero nodes. That vestigial skin is what makes glTF tooling (and
Blender re-import) reconstruct the file as an armature; three.js binds the clip by track name via
Lane D's `rigs.js` `_canon` regardless, so no bone rename is needed.
Runs headless on this box's Blender:
BL=/Applications/Blender.app/Contents/MacOS/Blender
"$BL" --background --python fbx_to_clip.py -- IN.fbx OUT.glb [ClipName]
Two stages, one command: (1) Blender imports the FBX (mesh+armature+anim) and exports a SKINNED GLB
(the skin needs the mesh to be emitted); (2) a pure-python glTF strip removes the mesh + its geometry
accessors, re-indexing/compacting so only the skeleton + dangling skin + animation remain (100 KB).
"""
import bpy, sys, os, json, struct, tempfile
ARGV = sys.argv[sys.argv.index("--") + 1:]
FBX, OUT = ARGV[0], ARGV[1]
CLIP = ARGV[2] if len(ARGV) > 2 else "Clip"
os.makedirs(os.path.dirname(os.path.abspath(OUT)), exist_ok=True)
def wipe():
for o in list(bpy.data.objects):
bpy.data.objects.remove(o, do_unlink=True)
for coll in (bpy.data.meshes, bpy.data.materials, bpy.data.images, bpy.data.armatures, bpy.data.actions):
for d in list(coll):
if d.users == 0:
coll.remove(d)
# ── stage 2: pure-python glTF mesh-strip (keep skeleton + dangling skin + animation) ──────────────
def read_glb(p):
d = open(p, "rb").read()
off = 12; js = None; binblob = b""
while off < len(d):
clen, ctype = struct.unpack("<II", d[off:off + 8]); off += 8
chunk = d[off:off + clen]; off += clen
if ctype == 0x4E4F534A: js = json.loads(chunk)
elif ctype == 0x004E4942: binblob = chunk
return js, binblob
def write_glb(js, binblob, out):
jb = json.dumps(js, separators=(",", ":")).encode()
jb += b" " * ((4 - len(jb) % 4) % 4)
bb = binblob + b"\x00" * ((4 - len(binblob) % 4) % 4)
total = 12 + 8 + len(jb) + 8 + len(bb)
with open(out, "wb") as f:
f.write(b"glTF"); f.write(struct.pack("<II", 2, total))
f.write(struct.pack("<II", len(jb), 0x4E4F534A)); f.write(jb)
f.write(struct.pack("<II", len(bb), 0x004E4942)); f.write(bb)
def strip_mesh(inpath, outpath):
js, binblob = read_glb(inpath)
keep = set() # accessors to retain: skin IBMs + all anim I/O
for sk in js.get("skins", []):
if "inverseBindMatrices" in sk:
keep.add(sk["inverseBindMatrices"])
for an in js.get("animations", []):
for s in an["samplers"]:
keep.add(s["input"]); keep.add(s["output"])
mesh_nodes = {i for i, n in enumerate(js["nodes"]) if "mesh" in n}
js.pop("meshes", None) # drop mesh defs; skins[] stay (become dangling)
for n in js["nodes"]:
n.pop("mesh", None); n.pop("skin", None)
# orphan the former-mesh nodes from the scene graph (leave them in the array so node indices —
# skin.joints, animation channel targets — stay valid; three.js ignores unreachable nodes)
for sc in js.get("scenes", []):
if "nodes" in sc:
sc["nodes"] = [x for x in sc["nodes"] if x not in mesh_nodes]
for n in js["nodes"]:
if "children" in n:
n["children"] = [x for x in n["children"] if x not in mesh_nodes]
if not n["children"]:
n.pop("children")
# re-index accessors → keep only the retained set
old_acc = js["accessors"]
keep_sorted = sorted(keep)
accmap = {old: i for i, old in enumerate(keep_sorted)}
js["accessors"] = [old_acc[i] for i in keep_sorted]
# keep bufferViews referenced by retained accessors; compact the binary
bv_keep = sorted({old_acc[i]["bufferView"] for i in keep_sorted if "bufferView" in old_acc[i]})
bvmap = {old: i for i, old in enumerate(bv_keep)}
old_bv = js["bufferViews"]
newbin = bytearray(); newbvs = []
for old in bv_keep:
bv = dict(old_bv[old]); start = bv.get("byteOffset", 0); length = bv["byteLength"]
pad = (4 - len(newbin) % 4) % 4; newbin += b"\x00" * pad
bv["byteOffset"] = len(newbin); newbin += binblob[start:start + length]
newbvs.append(bv)
js["bufferViews"] = newbvs
for a in js["accessors"]:
if "bufferView" in a:
a["bufferView"] = bvmap[a["bufferView"]]
for sk in js.get("skins", []):
if "inverseBindMatrices" in sk:
sk["inverseBindMatrices"] = accmap[sk["inverseBindMatrices"]]
for an in js.get("animations", []):
for s in an["samplers"]:
s["input"] = accmap[s["input"]]; s["output"] = accmap[s["output"]]
js["buffers"] = [{"byteLength": len(newbin)}]
write_glb(js, bytes(newbin), outpath)
return js
def main():
wipe()
bpy.ops.import_scene.fbx(filepath=FBX)
arms = [o for o in bpy.data.objects if o.type == "ARMATURE"]
if not arms:
raise SystemExit("no armature in FBX")
arm = arms[0]
n_mesh = len([o for o in bpy.data.objects if o.type == "MESH"])
if arm.animation_data and arm.animation_data.action:
arm.animation_data.action.name = CLIP
bones = [b.name for b in arm.data.bones]
mixamo = [b for b in bones if b.startswith("mixamorig")]
bpy.ops.object.select_all(action="SELECT") # armature + mesh → Blender emits a skin
bpy.context.view_layer.objects.active = arm
tmp = tempfile.NamedTemporaryFile(suffix=".glb", delete=False).name
bpy.ops.export_scene.gltf(filepath=tmp, export_format="GLB", use_selection=True,
export_animations=True, export_yup=True, export_skins=True)
js = strip_mesh(tmp, OUT)
os.unlink(tmp)
sz = os.path.getsize(OUT)
print(f"CLIP_DONE {os.path.basename(OUT)} bones={len(bones)} (mixamorig {len(mixamo)}) "
f"mesh_stripped={n_mesh} meshes_out={len(js.get('meshes', []))} skins_out={len(js.get('skins', []))} "
f"nodes={len(js['nodes'])} anims={len(js.get('animations', []))} {sz // 1024}KB")
main()

BIN
web/models/peds/sit.glb Normal file

Binary file not shown.