Bulk import of the working lanes that were living untracked on the PC. Content: - characters/ Lena/male body lanes, bakes, texture work, run logs - clothing/ garment pipeline, configs, gates, contract docs - garments/ MD-authored garment sources (.zprj/.zpac) - UAL-Lib/ Universal Animation Library 2 source (.blend/.fbx/.glb) - tools/ blender_bridge, iclone_bridge, md_bridge, tailor, glm_agent - docs/, plans/, dev/, .agents/plans/ Repo hygiene: - .gitattributes: LFS now covers .blend, .zprj, .zpac, .obj, .npy and the Reallusion .iAvatar/.ccAvatar/.ccRestore containers. Without this the ~3.8 GB in this commit would land as raw blobs. .png/.jpg are left out on purpose — ~250 are already tracked raw and converting them would rewrite every one without shrinking history. - .gitignore: exclude /accurig/ (~1 GB AccuRig program files, redistributable from Reallusion, nothing authored here) and /dev/null/ (git-lfs hook copies dropped by a `>/dev/null` redirect on Windows). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
18 KiB
FBX → Quaternius Pipeline — Formal Plan & Specification
Doc id: fbx-pipeline-plan-2026-07-21 · Status: APPROVED direction, pre-implementation
Owner: Jeremy · Author: Claude (5-agent research audit, 2026-07-21)
Supersedes: ad-hoc tools/* pipeline docs scattered across skills/READMEs.
1. Purpose & scope
After the FBX-vs-GLB study (2026-07-21) we adopted Recommendation 1: the game keeps GLB clips on the shared Quaternius skeleton; the conversion pipeline gets consolidated.
In scope
- S1. Relocate the FBX→Quaternius pipeline into a single folder
pipeline/at the animation-repo root, runnable end-to-end on this Windows machine and on the Mac. - S2. Accept any registered FBX source ("regular fbx"), not only iClone exports.
- S3. House the iClone integration in the pipeline, driven by an export script over the TinqsBridge API (localhost 18800).
- S4. Define and enforce a standardized bone/rig contract for all incoming FBX.
- S5. A reviewed, regression-gated migration (baseline before/after parity).
Out of scope (non-goals)
- N1. No change to ariki-game runtime code, ClipCatalog, dance JSON, or shipped GLBs.
- N2. No skeleton change (CC-skeleton option was evaluated and rejected).
- N3. No FBX-in-Godot ingestion (Option 2 spike may be scheduled separately).
- N4. Video-mocap tools (
mocap_retarget.py) and the reverse prop bridge (export_boat_prop.py) stay intools/unchanged.
Asset flow decision: all assets continue to run through exchange/ (§6). The
pipeline reads and writes only exchange/ stage folders; no new asset roots.
2. Definitions
| Term | Meaning |
|---|---|
| Canonical skeleton | The game's Quaternius 65-bone UE4-named rig (§4.1). Sole retarget target. |
| Rig family | A named source-skeleton convention with a registered bone map (§4.2). |
| Take | One FBX file containing one baked animation clip + skinned mesh. |
| Pack / Clip | GLB basename / animation name baked into the GLB; game ref is pack/Clip. |
| Intake contract | The validity rules every FBX must satisfy before retargeting (§4.3). |
| Stage folder | A subfolder of exchange/ representing one pipeline stage (§6). |
| Bridge | TinqsBridge iClone plugin + iclone_bridge.py client (verified 2026-07-21). |
3. Requirements
Functional
- FR-1 One command converts a take: intake validation → retarget → ping-pong bake → loop QC (both variants) → per-take report. Partial failures stop at the failing gate.
- FR-2 Rig family is auto-detected; a take on an unregistered rig is rejected with the complete list of unmapped bones and a nonzero exit code.
- FR-3 Adding a rig family requires only a new bone-map file + registry entry (no retargeter code change).
- FR-4 iClone export is scriptable:
export_take.pyapplies the standard export block (§4.4) and lands the FBX + metadata sidecar inexchange/incoming-fbx/. - FR-5 All existing outputs remain reproducible: for each rig family, the unified retargeter's output matches the legacy tool's output within tolerance (§9).
- FR-6 The pipeline runs identically from Windows and macOS; Blender executable is
resolved via
$BLENDER→ per-OS default paths, never hardcoded in commands. - FR-7 The driver surfaces the next free
nd_##id by parsingdocs/dances/REGISTRY.md, and refuses output names that collide with registry rows.
Non-functional
- NFR-1 Fail loud: every gate exits nonzero on failure; no silent partial output. A failed stage must not leave a half-written GLB in a stage folder (write to temp, rename on success).
- NFR-2 Provenance: every produced GLB gets a sibling JSON recording source file hash, rig family, tool versions (pipeline git rev, Blender version), settings, and QC verdicts.
- NFR-3 Determinism: same input + same pipeline rev ⇒ byte-stable animation curves (float tolerance per §9).
- NFR-4 Compatibility window: legacy tools remain runnable (moved to
tools/legacy/) until the Phase-2 regression matrix passes; deleted in a separate, single-purpose commit. - NFR-5 3.8 constraint: any code executed inside iClone stays Python 3.8.8 compatible. Pipeline-side code targets the repo's standard Python 3.
- NFR-6 LFS hygiene: no new large-binary patterns;
.fbx/.glbalready ride LFS.
4. Standards (the contract)
4.1 Canonical skeleton
Quaternius 65-bone rig, UE4-style names: root, pelvis, spine_01..03, neck_01, Head
(capital H); per side clavicle_, upperarm_, lowerarm_, hand_, thigh_, calf_, foot_, ball_, ball_leaf_; fingers thumb_01..03, index/middle/ring/pinky_01, *_04_leaf.
- Pinned snapshot committed at
pipeline/retarget/rigs/target/quaternius_ue.gltf(mesh-stripped copy of ariki-gameSuperhero_Male_FullBody.gltf). verify_target.pycompares pinned rig vs the local ariki-game checkout (bone names + rest transforms, tolerance 1e-5) and is run automatically by the driver; drift ⇒ hard fail with instructions to re-pin deliberately.
4.2 Rig-family registry — pipeline/retarget/rigs/*.json
One JSON per family:
{
"family": "cc_base",
"version": 1,
"probe_bones": ["CC_Base_Hip", "CC_Base_L_Upperarm"],
"char_frame": ["CC_Base_Hip", "CC_Base_Head", "CC_Base_L_Upperarm"],
"bone_map": { "CC_Base_Hip": "pelvis", "...": "..." },
"notes": "iClone 8 / CC4 exports; twist bones intentionally unmapped (ride parent)"
}
Registered at launch:
| Family | Probe | Source | Seed map |
|---|---|---|---|
cc_base |
CC_Base_Hip + CC_Base_L_Upperarm |
iClone/CC exports (primary) | cc_retarget.py BONE_MAP |
bstyle |
B-hips + B-upperArm.L |
Kevin Iglesias packs (HumanM@*, BasicMotions@*) |
kevin_retarget.py BONE_MAP |
mixamo |
mixamorig prefix (strip-variants handled) |
Mixamo | mixamo_retarget.py BONE_MAP |
quaternius_ue |
pelvis + spine_01 |
Quaternius vendor FBX (UAL) | identity pass-through |
Detection order: most-specific probe first; ambiguous or no match ⇒ reject (FR-2).
Bone-map edits bump version; provenance sidecars record it (NFR-2).
4.3 Intake contract (validated by validate_fbx.py, pre-Blender)
| # | Rule | Check | On violation |
|---|---|---|---|
| IC-1 | Registered rig family | probe-bone scan of FBX node names | REJECT, list unmapped bones |
| IC-2 | Baked motion present, >5 frames | anim-stack scan (TempMotion guard retained) |
REJECT: "exported WITHOUT motion" |
| IC-3 | T-pose bind preferred | cc_base: sidecar says t_pose: true, else WARN "A-pose — arm QA required" |
WARN (blocks nothing; flags QA) |
| IC-4 | In-place motion | not statically checkable; enforced downstream by loop-QC root-drift metric | n/a (QC gate) |
| IC-5 | One take per file | anim-stack count == 1 | REJECT |
| IC-6 | Mesh + skin weights present | at least one skinned mesh node | REJECT (bind pose needs weighted mesh) |
| IC-7 | Textures absent or ≤512px | embedded-texture scan | WARN (size hygiene) |
| IC-8 | Metadata sidecar present for iClone takes | <take>.json next to FBX |
WARN |
| IC-9 | Output name valid + free | registry parse (FR-7) | REJECT on collision |
4.4 iClone standard export block (enforced by export_take.py)
Target Tool Preset Blender · Export Range All · FPS 60 ·
Preserve Bone Names (CC Base) ON · Use T-Pose As Bind Pose ON ·
Reset Bone Scale ON · Embed Textures OFF/≤512 ·
precondition: Animation > Flatten All Motion with Constraint.
The script writes the settings actually used into the metadata sidecar (IC-8), so QA
can distinguish "contract says" from "export did".
5. Target layout
pipeline/
README.md # operator doc; contract §4 + runbook. Source of truth.
convert.py # FR-1 driver
validate_fbx.py # IC-1..IC-9 gate (no Blender; raw FBX scan; seconds)
blender_env.py # FR-6 Blender resolver ($BLENDER → per-OS defaults)
retarget/
retarget.py # unified retargeter (replaces cc/kevin/mixamo_retarget.py)
rigs/ # §4.2 registry
cc_base.json bstyle.json mixamo.json quaternius_ue.json
target/quaternius_ue.gltf target/verify_target.py
qc/loop_qc.py # moved unchanged (then FBX-input tolerant later if needed)
fix/loop_fix.py # moved
fix/pingpong_bake.py # moved
post/rename_clip.py # moved
post/dance_profile.py # moved
iclone/
export_take.py # FR-4 (bridge client side)
bridge/ # moved from tools/iclone_bridge/ (plugin src + installer)
tools/
legacy/ # NFR-4: cc_retarget.py, kevin_retarget.py,
# mixamo_retarget.py until Phase-2 matrix passes
export_boat_prop.py mocap_retarget.py bake_run_punch.mjs # out of scope (N4)
6. Exchange data-flow contract (assets run through exchange/ — decided)
exchange/
incoming-fbx/ STAGE 0 raw takes + .fbm textures + <take>.json sidecars
converted-glb/ STAGE 1 retargeted GLB (+ provenance .json) [convert.py]
pingpong-glb/ STAGE 2 <pack>_pp.glb loop variants (+ provenance)[convert.py]
looped-glb/ STAGE 2b optional loop_fix outputs (recreated on demand)
reports/ STAGE 3 per-take markdown QC report [convert.py] ← NEW
reference-video/ QA reference footage (unchanged)
workingfiles-iclone/ iClone .iProject staging (unchanged)
outgoing-props/ reverse direction, game→iClone (unchanged, out of scope)
Rules:
- E-1 Stages only move forward; a stage folder never contains hand-edited files.
- E-2 Ship step remains manual+reviewed: copy STAGE 1/2 GLBs into ariki-game
assets/quaternius/dancegen/with dance JSON, per existing skill workflow. - E-3 Cleanup only after Jeremy confirms in-game (existing etiquette, unchanged).
- E-4 Everything in
exchange/is LFS-tracked already; sidecars/reports are plain text.
7. Component contracts
| Component | CLI (canonical) | Exit codes |
|---|---|---|
validate_fbx.py |
python pipeline/validate_fbx.py <fbx> [--name <Clip>] |
0 pass / 1 reject / 2 warn-only / 3 error |
retarget.py |
<blender> --background --python pipeline/retarget/retarget.py -- --src <fbx…> --out <glb> --name <Clip> [--family <f>] [--target <gltf>] |
0 ok / 1 reject (unmapped/no-motion) / 2 error |
loop_qc.py |
unchanged: -- --src <glb> |
0 SMOOTH / 1 NOT SMOOTH / 2 error (unchanged) |
pingpong_bake.py, loop_fix.py, rename_clip.py |
unchanged flags, new paths | unchanged |
convert.py |
python pipeline/convert.py --src exchange/incoming-fbx/<take>.fbx --id nd_02 |
0 all gates green / 1 QC not smooth / 2 rejected at intake / 3 error |
export_take.py |
python pipeline/iclone/export_take.py --take <name> [--project <.iProject>] |
0 exported+valid / 1 bridge down / 2 export invalid vs §4.3 |
Driver behavior (FR-1): temp-write + atomic rename (NFR-1); provenance sidecars
(NFR-2); report to exchange/reports/<id>-<date>.md containing IC results, QC metrics
for base and _pp, worst-offending bones, and the ship checklist.
7.1 QC architecture (the QC element)
QC is layered, script-based, and mandatory — convert.py will not emit a shippable
GLB past a red gate:
| Gate | Tool | Runs | Catches |
|---|---|---|---|
| QC-0 Intake | validate_fbx.py (IC-1..9) |
before Blender, seconds | wrong rig, missing motion, A-pose flag, name collisions |
| QC-1 Target integrity | verify_target.py |
every convert.py run |
pinned skeleton drifting from the game's rig |
| QC-2 Loop seam | qc/loop_qc.py on STAGE 1 GLB and the _pp variant |
every take | pose gap / velocity gap / root drift at the loop seam, worst-offending bones |
| QC-3 Report + human eyeball | exchange/reports/<id>.md + test-bed viewing |
every take | everything numbers can't catch (arm orientation, style, reversal reading as moonwalk) |
| QC-R Regression | §9 matrix | on retargeter/bone-map changes | math/output drift vs known-good clips |
QC-3 stays human by policy: the boat-prop lesson (all asserts green, boat upside down) is why no automated verdict ships a clip without an eyeball in the test bed.
7.2 Model & token usage
The pipeline itself calls no AI models and consumes zero tokens. Every component in §7 is deterministic Python/Blender (required by NFR-3 — determinism and model calls are mutually exclusive here). There are no LLM API calls, no cloud services; the only network-adjacent piece is the TinqsBridge localhost socket to iClone.
Tokens are spent only when a Claude agent operates the pipeline, and only in the session doing the operating:
| Consumer | Model | Typical cost | Notes |
|---|---|---|---|
Scripts (convert.py, retarget, QC, bakes, export_take.py) |
none | 0 tokens | pure Python/Blender |
loop-qc agent (.claude/agents/loop-qc.md) |
inherits the session's model (no override pinned) | ~5–15k tokens per clip; ~20–40k per small batch | convenience wrapper that runs loop_qc.py and interprets numbers |
| Claude running a full convert batch interactively | session model | ~50–150k tokens per batch today | convert.py + reports are designed to shrink this: one command + one readable report instead of many tool round-trips |
Cost-control decisions baked into this plan: QC verdicts live in exchange/reports/
markdown so future sessions read a summary instead of re-running tools; validate_fbx.py
fails bad takes in seconds before any expensive step; and the loop-qc agent SHOULD be
pinned to a small model (model: haiku) in its frontmatter during P1 — it only runs a
script and relays numbers, which doesn't need a frontier model.
8. Phases, gates, deliverables
| Phase | Work | Entry gate | Exit gate (must pass) | Est |
|---|---|---|---|---|
| P0 Baseline review | Run CURRENT tools on Windows (explicit Blender path) on hakadance1.fbx; curve-diff vs shipped war_dance_01.glb; write plans/fbx-pipeline-baseline-2026-07-21.md incl. per-tool audit table |
plan approved | Windows/Mac parity verdict documented; blockers listed | 0.5d |
| P1 Move & wire | git mv per §5; blender_env.py; fix kevin hardcoded path; sweep docs/skills (grep checklist: tools/, /Applications/Blender) |
P0 pass | P0 run repeats green from new paths; grep sweep clean | 0.5d |
| P2 Unified retargeter | retarget.py + rigs registry; port maps from legacy tools |
P1 done | Regression matrix §9 green; legacy tools moved to tools/legacy/ |
1d |
| P3 iClone export script | Verify RLPy FBX-export API coverage of §4.4 FIRST; implement export_take.py; else checklist-fallback mode; T-pose QA on first export |
P2 done; bridge pings | One take exported from iClone passes validate_fbx.py with t_pose: true; arm QA eyeballed in test bed |
1d |
| P4 Driver + gates | convert.py, validate_fbx.py, reports, registry-id logic (FR-7) |
P2 done (parallel w/ P3) | convert.py on a cc_base take: green end-to-end, report emitted |
0.5–1d |
| P5 Docs + pilot | pipeline/README.md; skills point at it; pilot batch: 4× HumanM@Mining*.fbx (bstyle) |
P2–P4 done | Pilot converted + QC'd + staged; registry updated | 0.5d |
| P6 Retire legacy | Delete tools/legacy/ in a single-purpose commit |
2 real batches through new pipeline w/o legacy fallback | commit merged | 0.1d |
Total ≈ 4 days. P3 and P4 can run in parallel.
9. Regression & test matrix (P2 exit gate)
| Case | Family | Legacy tool | Comparison |
|---|---|---|---|
aloha1.fbx |
cc_base | cc_retarget.py |
per-bone quaternion curves, max abs delta ≤ 1e-4; pelvis translation ≤ 1e-4 m; frame count equal |
HumanM@Mining01 - Loop Ground.fbx |
bstyle | kevin_retarget.py (target path fixed) |
same |
UAL1_Standard.fbx (one clip) |
quaternius_ue | n/a (new capability) | output plays on pinned rig; bone set ⊆ canonical 65 |
| mixamo | mixamo | mixamo_retarget.py |
no source FBX on disk — gate on any newly downloaded Mixamo take; if none by P2, mark family "ported, unverified" in README |
| Negative: unknown rig | — | — | reject w/ unmapped-bone list (FR-2) |
| Negative: 2-frame TempMotion | cc_base | — | IC-2 reject message |
verify_target.py |
— | — | pinned rig == ariki-game rig |
Float-tolerance rationale: Blender 5.1 (Win) vs 5.1.2 (Mac) may differ in low-order bits; 1e-4 is far below visible thresholds but catches real math changes.
10. Risk register
| # | Risk | L×I | Mitigation / trigger |
|---|---|---|---|
| R1 | RLPy export API lacks §4.4 settings | M×M | P3 verifies API surface before coding; fallback = scene-prep + human-clicked checklist; contract still enforced by validate_fbx.py |
| R2 | T-pose-bind switch shifts cc_base output vs shipped clips | M×M | Deliberate: new takes only; shipped GLBs untouched; first T-pose take gets arm QA (P3 exit gate) |
| R3 | Blender Win/Mac version drift | L×M | P0 curve-diff gate; pin "Blender 5.1.x" in README; upgrade both sides together |
| R4 | Pinned target rig drifts from game skeleton | L×H | verify_target.py auto-run by driver (hard fail) |
| R5 | Doc/skill path rot after move | H×L | P1 grep checklist is a named exit gate |
| R6 | Unified retargeter regression on edge takes | M×H | §9 matrix + NFR-4 legacy window + P6 requires 2 clean real batches |
| R7 | Registry parse (FR-7) breaks on table format change | M×L | Parser tolerant to column additions; on parse failure driver degrades to WARN, never blocks |
| R8 | Mixamo family unverifiable at P2 | M×L | Explicitly labeled "unverified" until a take arrives; not a launch blocker |
11. Acceptance criteria (project-level)
python pipeline/convert.py --src exchange/incoming-fbx/<take>.fbx --id nd_##on this Windows machine produces STAGE 1+2 GLBs, provenance sidecars, and a QC report — zero Mac involvement, zero manual Blender paths.- All rig families in §4.2 convert; §9 matrix green (mixamo allowed "unverified").
- Unknown-rig and no-motion FBX fail fast with actionable messages.
export_take.py(or its documented fallback) yields an intake-valid, T-pose iClone FBX with settings-recording sidecar.- Legacy tools deleted (P6) after two clean production batches.
- Skills/docs reference only
pipeline/paths;pipeline/README.mdis the runbook.
12. Open questions (tracked, non-blocking)
- Q1 RLPy FBX-export settings coverage (resolves in P3).
- Q2 Should
loop_qc.pygain FBX input for pre-retarget QC? (defer; not needed for launch) - Q3 Option-2 spike (Godot import-time retarget of FBX) — separate plan if/when wanted.
- Q4
hakadance1_static.glborphan in dancegen — cleanup during next ariki-game touch.