Files
animation/plans/fbx-pipeline-plan-2026-07-21.md
T
jeremy 3ba86b2ea8 feat: clothing lane, character sources, and DCC bridges
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>
2026-08-06 15:55:43 -07:00

18 KiB
Raw Blame History

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 in tools/ 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.py applies the standard export block (§4.4) and lands the FBX + metadata sidecar in exchange/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 parsing docs/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/.glb already 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-game Superhero_Male_FullBody.gltf).
  • verify_target.py compares 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) ~515k tokens per clip; ~2040k per small batch convenience wrapper that runs loop_qc.py and interprets numbers
Claude running a full convert batch interactively session model ~50150k 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.51d
P5 Docs + pilot pipeline/README.md; skills point at it; pilot batch: 4× HumanM@Mining*.fbx (bstyle) P2P4 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)

  1. 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.
  2. All rig families in §4.2 convert; §9 matrix green (mixamo allowed "unverified").
  3. Unknown-rig and no-motion FBX fail fast with actionable messages.
  4. export_take.py (or its documented fallback) yields an intake-valid, T-pose iClone FBX with settings-recording sidecar.
  5. Legacy tools deleted (P6) after two clean production batches.
  6. Skills/docs reference only pipeline/ paths; pipeline/README.md is the runbook.

12. Open questions (tracked, non-blocking)

  • Q1 RLPy FBX-export settings coverage (resolves in P3).
  • Q2 Should loop_qc.py gain 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.glb orphan in dancegen — cleanup during next ariki-game touch.