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>
111 lines
4.8 KiB
Python
111 lines
4.8 KiB
Python
#!/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()
|