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:
parent
2b13dd4ad7
commit
b322c998db
@ -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).
|
||||
|
||||
BIN
docs/shots/laneE/sit_clip.png
Normal file
BIN
docs/shots/laneE/sit_clip.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
@ -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
134
pipeline/fbx_to_clip.py
Normal 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
BIN
web/models/peds/sit.glb
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user