Files
animation/plans/fbx-pipeline-plan-2026-07-21.md
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

305 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```json
{
"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.