Compare commits
11 Commits
7f4300eae6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 854dbb5ce4 | |||
| 28fdf940f7 | |||
| 06d950649b | |||
| 078b0b7a07 | |||
| 1219954fdc | |||
| 82266c97bb | |||
| b4d962167f | |||
| 998e306804 | |||
| 228f26e7dc | |||
| cd2e598354 | |||
| b68fa7b920 |
@@ -0,0 +1,11 @@
|
|||||||
|
# .agents/AGENTS.md — registry + delegation
|
||||||
|
|
||||||
|
Detailed layer behind root `AGENTS.md`. This repo has no repo-specific
|
||||||
|
multi-agent registry beyond:
|
||||||
|
|
||||||
|
- **`loop-qc`** (`.claude/agents/loop-qc.md`) — sub-agent for the loop-QC pass
|
||||||
|
on converted GLBs; use it instead of hand-running `tools/loop_qc.py` when
|
||||||
|
checking a batch of clips.
|
||||||
|
|
||||||
|
For cross-repo agent roster, MCP services, and delegation rules, see the hub:
|
||||||
|
`tinqs-ltd/docs/.agents/AGENTS.md`.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# SOUL.md — animation repo identity
|
||||||
|
|
||||||
|
Inherits the shared identity, values, and confidentiality rules from the hub:
|
||||||
|
`tinqs-ltd/docs/.agents/SOUL.md`. Read that first if this is your first session
|
||||||
|
on any Tinqs repo.
|
||||||
|
|
||||||
|
## What this repo is
|
||||||
|
|
||||||
|
The animation-pipeline hub and Mac↔PC bridge for Ariki. iClone 8 + Video Mocap
|
||||||
|
run on Jeremy's Windows PC; Blender 5, the retarget tools, and `ariki-game` live
|
||||||
|
on both machines. This repo carries the skills, tools, and files-in-flight that
|
||||||
|
turn PC-side motion capture (iClone renders, video mocap) into game-ready
|
||||||
|
Quaternius-rig GLB clips consumed by `ariki-game`.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **In scope:** FBX→GLB retargeting (Blender headless), loop QC and loop-fixing,
|
||||||
|
dance/clip naming and the registry, boat/prop export, the exchange-folder
|
||||||
|
bridge protocol, and the skills that encode iClone/mocap operator knowledge.
|
||||||
|
- **Out of scope:** game-side rendering, ClipCatalog code, dance ceremony
|
||||||
|
design (owned by `ariki-game`) — this repo only produces and ships the assets.
|
||||||
|
|
||||||
|
## Confidentiality
|
||||||
|
|
||||||
|
Same as the hub: this repo is private/internal. No special classified material
|
||||||
|
lives here (Cosmos-related confidentiality does not apply to animation tooling).
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# rules/ — always-on conventions for this repo
|
||||||
|
|
||||||
|
No repo-specific rules beyond what's already always-on in root `AGENTS.md`
|
||||||
|
(git via `tinqs push`/`tinqs pull`, stage-by-explicit-path, naming law) and the
|
||||||
|
shared hub rules at `tinqs-ltd/docs/.agents/rules/`. Add a file here when a
|
||||||
|
convention needs to be enforced repo-wide and doesn't fit in `AGENTS.md`'s
|
||||||
|
thin-entry budget.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# skills/ — playbooks
|
||||||
|
|
||||||
|
This repo's operator playbooks are Claude-Code-native skills and live at
|
||||||
|
`.claude/skills/` (auto-discovered by Claude Code), not duplicated here:
|
||||||
|
|
||||||
|
| Skill | Covers |
|
||||||
|
|---|---|
|
||||||
|
| `animation` | Operator playbook — the batch workflow, naming, loop QC, gotchas |
|
||||||
|
| `animation-creation` | Authoring new clips on the shared Quaternius skeleton |
|
||||||
|
| `iclone-video-mocap` | iClone 8 + Video Mocap: filming, cleanup, FBX export |
|
||||||
|
| `pose-estimation` | Video→pose-landmark extraction (MediaPipe) |
|
||||||
|
| `retarget-animations` | Deprecated early retarget notes, kept for history |
|
||||||
|
|
||||||
|
An agent-level playbook (`loop-qc`) lives at `.claude/agents/loop-qc.md`. Follow
|
||||||
|
the per-repo convention (`.agents/skills/` as the canonical location) once this
|
||||||
|
repo's skills are ported off `.claude/`; until then this file is the pointer so
|
||||||
|
there's a single place agents look, per the SoT rule (no second source of
|
||||||
|
truth).
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Architecture — animation pipeline
|
||||||
|
|
||||||
|
Entry point to architecture law for this repo. Per-system detail lives in
|
||||||
|
`.agents/wiki/architecture/`.
|
||||||
|
|
||||||
|
## The one-sentence shape
|
||||||
|
|
||||||
|
PC-side motion capture (iClone 8 + Video Mocap) → this repo (Mac↔PC bridge,
|
||||||
|
Blender-headless retargeting, loop QC) → `ariki-game` (Quaternius-rig GLB
|
||||||
|
consumption via ClipCatalog).
|
||||||
|
|
||||||
|
## Machines and where things live
|
||||||
|
|
||||||
|
| Machine | Owns |
|
||||||
|
|---|---|
|
||||||
|
| PC (Windows) | iClone 8 + Video Mocap plugin — authors motion, exports FBX |
|
||||||
|
| Mac (and PC) | Blender 5.1.2, this repo's retarget tools, `ariki-game` checkout |
|
||||||
|
|
||||||
|
Files move machine-to-machine only through `exchange/` in this repo, synced
|
||||||
|
with `tinqs push`/`tinqs pull` (Git LFS for `.fbx/.glb/.mp4/.task` — see
|
||||||
|
`.gitattributes`). Raw `git pull` hangs on LFS in tinqs.com repos.
|
||||||
|
|
||||||
|
## Pipeline stages
|
||||||
|
|
||||||
|
1. **Capture (PC)** — iClone export, Blender preset, Range=All, Preserve Bone
|
||||||
|
Names ON → `exchange/incoming-fbx/`.
|
||||||
|
2. **Retarget (either machine)** — Blender-headless, source-specific retargeter
|
||||||
|
in `tools/`, all bake onto the shared Quaternius rig and export a
|
||||||
|
mesh-stripped GLB with one NLA track per clip:
|
||||||
|
- `cc_retarget.py` — Reallusion CC/iClone `CC_Base_*` FBX (60fps; rejects
|
||||||
|
2-frame `_TempMotion` exports as a guard against motion-less takes)
|
||||||
|
- `mixamo_retarget.py` — Mixamo FBX (prefix auto-detect)
|
||||||
|
- `kevin_retarget.py` — Kevin Iglesias Unity FBX (`B-*` rig; hardcoded
|
||||||
|
target path, non-portable)
|
||||||
|
- `mocap_retarget.py` — MediaPipe pose JSON → GLB (rotation solving, no
|
||||||
|
source armature)
|
||||||
|
→ `exchange/converted-glb/`.
|
||||||
|
3. **Loop QC** — `tools/loop_qc.py` (pre-commit gate: pose/velocity/root-drift
|
||||||
|
continuity across the seam, exit 0 = smooth). Fixers when it fails:
|
||||||
|
`tools/loop_fix.py` (de-drift/trim/seam-blend → `exchange/looped-glb/`) and
|
||||||
|
`tools/pingpong_bake.py` (palindrome bake, seam-perfect but motion reverses
|
||||||
|
at the turn → `exchange/pingpong-glb/`, clip suffix `PP`). Every adopted
|
||||||
|
dance ships as a base clip + `_pp` pair.
|
||||||
|
4. **Ship** — commit + `tinqs push` the GLBs in this repo, then copy into
|
||||||
|
`ariki-game/assets/quaternius/dancegen/` (auto-discovered by ClipCatalog as
|
||||||
|
`<pack>/<ClipName>` — also watches `kevin/`, `mixamo/`), write one dance JSON
|
||||||
|
per clip in `ariki-game/assets/dances/`, `game.sh import`, verify clip names
|
||||||
|
match GLB `animations[].name` exactly, commit in ariki-game (push only when
|
||||||
|
Jeremy asks).
|
||||||
|
5. **Post-ingest QC (game side, out of this repo)** — `ariki-game/tools/anim_qc.py`
|
||||||
|
re-checks world-space seam continuity + renders a seam-cross video for eye
|
||||||
|
review. Two QC tools by design: this repo's gate is pre-commit/local-space,
|
||||||
|
the game's is post-ingest/world-space.
|
||||||
|
|
||||||
|
## Naming law
|
||||||
|
|
||||||
|
Full detail: `.agents/wiki/dances/REGISTRY.md` (source of truth — do not
|
||||||
|
duplicate its content elsewhere). Summary: new takes get a provisional `nd_##`
|
||||||
|
id (never a content-derived name); a take is renamed to its ceremony code
|
||||||
|
(snake_case of the sim's `DanceType` enum + index) only once Jeremy assigns it
|
||||||
|
to a specific ceremony, via `tools/rename_clip.py`. Rejected/superseded takes
|
||||||
|
are archived (`archive/`), never deleted; numbers are never reused.
|
||||||
|
|
||||||
|
## Canonical target rig
|
||||||
|
|
||||||
|
All retargeters bake onto ariki-game's
|
||||||
|
`assets/quaternius/base-characters/.../Godot - UE/Superhero_Male_FullBody.gltf`.
|
||||||
|
Run from this repo, `--target` must be passed explicitly (the tool's default
|
||||||
|
path is ariki-relative and won't resolve here). The rig/pack standard itself
|
||||||
|
(65-bone Quaternius UE-named skeleton, hero body `Ariki_Female_QuatSkin.glb`) is
|
||||||
|
governed by `ariki-game/.humans/animation-bone-sources.html` and
|
||||||
|
`ariki-game/.agents/wiki/quaternius-ual-packs.md` — this repo's iClone/mocap
|
||||||
|
lane is explicitly a separate clip source (not UAL1/UAL2/Kevin/Mixamo), landing
|
||||||
|
in `dancegen/` only.
|
||||||
|
|
||||||
|
## Tool provenance / mirroring
|
||||||
|
|
||||||
|
`tools/cc_retarget.py`, `mixamo_retarget.py`, `kevin_retarget.py`,
|
||||||
|
`mocap_retarget.py` are **mirrors of `ariki-game/tools/`** — the game copies are
|
||||||
|
authoritative; re-copy from ariki-game when they change (they have silently
|
||||||
|
diverged before). `.claude/skills/` here mirror a subset of `ariki-game/.claude/skills/`
|
||||||
|
plus `~/.claude/skills/pose-estimation` — see `README.md` Provenance section.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- `.agents/wiki/architecture/` — per-system detail (currently: this file covers
|
||||||
|
the whole pipeline; split out if a subsystem grows its own doc).
|
||||||
|
- `.agents/wiki/dances/REGISTRY.md` — naming/registry source of truth.
|
||||||
|
- `.agents/wiki/iclone-bridge.md` — PC-lane routing stub.
|
||||||
|
- `.agents/wiki/devops-reports/` — point-in-time audits and convergence reports.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# architecture/ — per-system detail
|
||||||
|
|
||||||
|
This repo's pipeline is small enough that `.agents/wiki/ARCHITECTURE.md` covers
|
||||||
|
it end to end today; nothing has grown large enough yet to need its own
|
||||||
|
per-system doc here. Split a topic out into this folder (and link it from
|
||||||
|
`ARCHITECTURE.md`) when it does — likely candidates as the repo grows:
|
||||||
|
|
||||||
|
- Retarget-tool internals (per-source rig mapping, bone-name conventions).
|
||||||
|
- Loop QC/fix algorithms (pose-gap, velocity-gap, root-drift math).
|
||||||
|
- The exchange-folder state machine (`exchange/GUIDE.md` currently owns this —
|
||||||
|
promote here if it needs architecture-level treatment beyond an operator
|
||||||
|
guide).
|
||||||
@@ -39,7 +39,7 @@ renamed once Jeremy says which ceremony it corresponds to. Rename with:
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `water_dance_01` | 1 | awaiting take | — |
|
| `water_dance_01` | 1 | awaiting take | — |
|
||||||
| `fire_dance_01` | 1 | awaiting take | — (the old "firedance1" was mislabeled dance1 choreography) |
|
| `fire_dance_01` | 1 | awaiting take | — (the old "firedance1" was mislabeled dance1 choreography) |
|
||||||
| `tide_dance_01` | 1 | awaiting take | — |
|
| `tide_dance_01` | 1 | ⚠ **no enum entry** | — (no `TideDance` in the sim `DanceType` enum as of 2026-07-23 — confirm with Ozan whether the slot is real before filming a take) |
|
||||||
| `fertility_dance_01` | 1 | **adopted** 2026-07-16 | aloha1 (hula) — shipped with `_pp` loop |
|
| `fertility_dance_01` | 1 | **adopted** 2026-07-16 | aloha1 (hula) — shipped with `_pp` loop |
|
||||||
| `food_dance_01` | 1 | awaiting take | — |
|
| `food_dance_01` | 1 | awaiting take | — |
|
||||||
| `taming_dance_01` | 1 | **adopted** 2026-07-16 | dance1 — shipped with `_pp` loop |
|
| `taming_dance_01` | 1 | **adopted** 2026-07-16 | dance1 — shipped with `_pp` loop |
|
||||||
@@ -49,7 +49,7 @@ renamed once Jeremy says which ceremony it corresponds to. Rename with:
|
|||||||
| `colony_shield_dance_01` | 2 | awaiting take | — |
|
| `colony_shield_dance_01` | 2 | awaiting take | — |
|
||||||
| `age_progression_dance_01` | 3 | awaiting take | — (gates Age advancement) |
|
| `age_progression_dance_01` | 3 | awaiting take | — (gates Age advancement) |
|
||||||
| `star_watching_dance_01` | 3 | awaiting take | — |
|
| `star_watching_dance_01` | 3 | awaiting take | — |
|
||||||
| `rain_dance_01` | tech | awaiting take | — (tech-tree "Rain Maker"; not in DanceType enum yet) |
|
| `rain_dance_01` | 1 | awaiting take | — (tech-tree "Rain Maker"; `RainDance` **is** in the DanceType enum, Age 1 — earlier "not in enum yet" note was wrong, corrected 2026-07-23) |
|
||||||
|
|
||||||
## Register
|
## Register
|
||||||
|
|
||||||
@@ -82,3 +82,14 @@ Cyclic clips (`*_loop`, `*_idle`) must pass `loop_qc.py` before shipping; one-sh
|
|||||||
| `boat_row_loop` | `BoatRowLoop` | awaiting take | helm, stern-oar sweep ±18° |
|
| `boat_row_loop` | `BoatRowLoop` | awaiting take | helm, stern-oar sweep ±18° |
|
||||||
| `boat_fish_loop` | `BoatFishLoop` | awaiting take | Seat_Fisher; coexists with kevin Fishing01_Loop |
|
| `boat_fish_loop` | `BoatFishLoop` | awaiting take | Seat_Fisher; coexists with kevin Fishing01_Loop |
|
||||||
| `boat_sail_idle` | `BoatSailIdle` | awaiting take | sway/line-hauling loop |
|
| `boat_sail_idle` | `BoatSailIdle` | awaiting take | sway/line-hauling loop |
|
||||||
|
| *(unregistered)* | `CanoeDismount`, `CanoeSitIdle` | **live in-game** since 2026-07-21 | shipped as `ariki-game/assets/quaternius/dancegen/canoedismount1.glb` (content-guessed name, predates this table) — register under a `boat_*` code at next adoption pass |
|
||||||
|
| *(unregistered)* | `run_to_dive` | **live in-game** | Mixamo one-off at `ariki-game/assets/quaternius/mixamo/run_to_dive.glb` — outside this pipeline, listed for completeness |
|
||||||
|
|
||||||
|
## QC tool split (audit 2026-07-23)
|
||||||
|
|
||||||
|
Two QC tools exist and gate different stages — both stay:
|
||||||
|
- **`tools/loop_qc.py` (this repo)** — the **pre-commit loop gate**: local-rotation pose gap,
|
||||||
|
velocity gap, root drift; exit 0/1. Every `_pp` (and `*_loop`/`*_idle` boat clip) must pass
|
||||||
|
it before adoption — this is the "must pass loop_qc" rule above.
|
||||||
|
- **`ariki-game/tools/anim_qc.py`** — **post-ingest QC** on the game side: world-space seam
|
||||||
|
position/velocity, mid-clip stutter scan, seam-cross video render for eye review.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# DevOps Report — Animation Standard Convergence (2026-07-23 → 24)
|
||||||
|
|
||||||
|
**Author:** Claude (Fable 5), Jeremy's session · **Repos:** `animation` + `ariki-game`
|
||||||
|
**Trigger:** Ozan landed the animation standardization contract on ariki-game main
|
||||||
|
(2026-07-23: `5409a82e0`, `3d7f3ef94`); Jeremy requested a full conformance assessment
|
||||||
|
of our lane (dance pipeline, this bridge repo, test beds), then execution of the fixes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What the standard says (one paragraph)
|
||||||
|
|
||||||
|
One runtime skeleton — the Quaternius 65-bone UE-named rig. Hero body =
|
||||||
|
`Ariki_Female_QuatSkin.glb` (Mixamo weights on Quaternius rest orientations; the earlier
|
||||||
|
MixamoSkin+msrig pairing is superseded). Clip policy: UAL1 Pro = locomotion, UAL2 Source =
|
||||||
|
work (under `ariki-game/assets/quaternius/anim/`), Kevin packs gaps-only, Mixamo one-offs,
|
||||||
|
and **our iClone/video-mocap lane is explicitly separate**: `cc_retarget.py` →
|
||||||
|
`dancegen/` → dance JSON. Canonical docs: `ariki-game/.humans/animation-bone-sources.html`,
|
||||||
|
wiki `player-quaternius-animator.md` + `quaternius-ual-packs.md`, skill
|
||||||
|
`aaa-player-animation`.
|
||||||
|
|
||||||
|
## 2. The assessment (how it ran)
|
||||||
|
|
||||||
|
- 52 checkable rules extracted from the 7 standard docs.
|
||||||
|
- 10 audit roles, model-routed: GLM-5.2 ×2 (inline-input tasks), DeepSeek v4-pro ×5
|
||||||
|
(code/doc/bed sweeps; one run died and was completed by the orchestrator), Fable ×3
|
||||||
|
(wiring, dance system, synthesis). Every CRITICAL/HIGH re-verified mechanically.
|
||||||
|
- Bone-census probe over 21 GLBs (this repo's `exchange/{converted,pingpong}-glb/` +
|
||||||
|
ariki `dancegen/` + the three body GLBs).
|
||||||
|
- Full report: `ariki-game/.agents/audits/animation-standard-assessment-2026-07-23.md`;
|
||||||
|
meeting page: `ariki-game/.humans/animation-standard-assessment.html`.
|
||||||
|
|
||||||
|
**Result:** the pipeline core was already conformant — 100% of runtime clip GLBs on the
|
||||||
|
65-bone contract, 4/4 `_pp` loop pairs, all dance JSONs clean post-rename, 16/17 test
|
||||||
|
beds compliant. The defects clustered in the superseded MixamoSkin/msrig mechanism and
|
||||||
|
stale doc/tool mirrors.
|
||||||
|
|
||||||
|
## 3. What we shipped
|
||||||
|
|
||||||
|
### This repo (`b4d9621`, pushed 2026-07-23)
|
||||||
|
| Change | Why |
|
||||||
|
|---|---|
|
||||||
|
| `tools/cc_retarget.py` + `mixamo_retarget.py` re-synced from ariki-game | They were stale forks (2026-07-15) silently missing `--despike`, `--fingers`, `--map ual` — PC batches would have lost QC |
|
||||||
|
| Tool **mirroring rule** added to `exchange/GUIDE.md` | Game copies are authoritative; never edit mirrors here |
|
||||||
|
| `tools/kevin_retarget.py` path fix (mirrored) | Was hardcoded to Ozan's home dir |
|
||||||
|
| `exchange/INBOX/lena/Ariki_Female_QuatSkin.glb` added (LFS) + README rewrite | The drop shipped only the superseded MixamoSkin body; **import the QuatSkin one on the PC** |
|
||||||
|
| `docs/dances/REGISTRY.md` corrections | `tide_dance_01` flagged (NO `TideDance` in the sim enum); rain note fixed (`RainDance` IS in the enum); boat table now records live `canoedismount1`/`run_to_dive`; QC split documented (loop_qc = pre-commit gate here, anim_qc = post-ingest game-side) |
|
||||||
|
| `docs/iclone-bridge.md` created | GUIDE referenced it; it never existed |
|
||||||
|
|
||||||
|
Earlier same day (`998e306`): the original Lena INBOX drop (GLB + rigged/unrigged T-pose
|
||||||
|
FBX + toned textures).
|
||||||
|
|
||||||
|
### ariki-game (`b70d5a11c` + `934b6d4dc`, pushed)
|
||||||
|
- **Dance bed `LenaBodyPath` → QuatSkin** (the bed had gone Lena-only on the superseded
|
||||||
|
MixamoSkin body whose msrig clip-reroute is dead — basename rot after the Pro rename).
|
||||||
|
Build green.
|
||||||
|
- Doc convergence: clip-inventory wiki corrected (23→30 wired UAL2; shield/sword set was
|
||||||
|
mislabeled "unused"), UALPACKS "Standard removed" claim scoped, `animation-pipelines.md`
|
||||||
|
coverage extended (+`anim-combined/ msrig/ derived-bodies/`), `pose-estimation` skill
|
||||||
|
reference re-pointed to THIS repo.
|
||||||
|
- Bug filed for Ozan: `.agents/bugs/2026-07-23-ninjajump-idle-missing-clip.md` —
|
||||||
|
code requests `NinjaJump_Idle`, pack ships `NinjaJump_Idle_Loop`, resolver can't bridge.
|
||||||
|
|
||||||
|
## 4. Live verification (2026-07-24)
|
||||||
|
|
||||||
|
Screenshot-verified on a running dance bed (post-fix build): QuatSkin Lena dancers
|
||||||
|
upright, phase-locked, `war_challenge` playing on the stock clip libraries. Engine log
|
||||||
|
carries **zero** `MISSING clip` warnings → the NinjaJump bug is **latent** (current
|
||||||
|
Godot import config strips `_Loop`), not active; hardening still recommended. Bug file
|
||||||
|
updated with this evidence (`934b6d4dc`).
|
||||||
|
|
||||||
|
## 5. Open items (owners)
|
||||||
|
|
||||||
|
| Item | Owner |
|
||||||
|
|---|---|
|
||||||
|
| Import `INBOX/lena/Ariki_Female_QuatSkin.glb` on the PC (not the MixamoSkin one) | Jeremy |
|
||||||
|
| Retire `msrig/` + MixamoSkin GLB outright (recommended) or re-retarget under new basenames | Ozan (negotiate) |
|
||||||
|
| Amend the Kevin-swim keep-set rule to match the restored swim override in code | Ozan (negotiate) |
|
||||||
|
| Add derived/import packs (Run_Punch, mixamo one-offs, canoe clips) to the BONES tables | Ozan (negotiate) |
|
||||||
|
| NinjaJump 1-token fix + resolver `+_Loop` arm | Ozan (bug filed) |
|
||||||
|
| Register `canoedismount1` under a `boat_*` code at next adoption pass | Jeremy (this repo) |
|
||||||
|
|
||||||
|
## 6. Ops notes
|
||||||
|
|
||||||
|
- All syncs via `tinqs pull`/`tinqs push` (LFS-safe); QuatSkin GLB verified as an LFS pointer.
|
||||||
|
- Executor telemetry: GLM headless = 1 clean / 1 silent-empty; DeepSeek = 4 clean / 1 SIGTERM;
|
||||||
|
Fable tracks found both deep bugs (dead msrig reroute, NinjaJump). Full disclosure table
|
||||||
|
in the audit report.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# iClone bridge — PC-lane overview
|
||||||
|
|
||||||
|
(Stub created 2026-07-23 — this path was referenced by `exchange/GUIDE.md` but never
|
||||||
|
written. The full operational detail lives elsewhere; this page just routes you.)
|
||||||
|
|
||||||
|
The Mac↔PC animation bridge: iClone 8 + Video Mocap on Jeremy's Windows machine authors
|
||||||
|
motion; this repo carries files both directions; ariki-game consumes 65-bone Quaternius
|
||||||
|
GLBs.
|
||||||
|
|
||||||
|
| Need | Go to |
|
||||||
|
|---|---|
|
||||||
|
| Which exchange/ folder to use, when files move/archive | `exchange/GUIDE.md` |
|
||||||
|
| Naming (nd_## → ceremony codes), loop-pair rule, registry | `.agents/wiki/dances/REGISTRY.md` |
|
||||||
|
| Filming rules, iClone/AccuFACE settings, mocap cleanup, FBX export | `iclone-video-mocap` skill (`.claude/skills/` here and in ariki-game) |
|
||||||
|
| CC FBX → Quaternius GLB conversion | `tools/cc_retarget.py` (mirror of ariki-game — game copy is authoritative) |
|
||||||
|
| Looping (`_pp`) + QC gates | `tools/pingpong_bake.py` + `tools/loop_qc.py`; game-side `anim_qc.py` (split documented in REGISTRY.md) |
|
||||||
|
| Game-side landing zone | `ariki-game/assets/quaternius/dancegen/` + `assets/dances/*.json` |
|
||||||
|
| The skeleton/pack standard itself | `ariki-game/.humans/animation-bone-sources.html` + `.agents/wiki/quaternius-ual-packs.md` |
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Master plan — animation repo
|
||||||
|
|
||||||
|
What's in flight and recently shipped. See `.agents/plans/` for the closed-out
|
||||||
|
plan/results docs behind these items; see `.agents/wiki/dances/REGISTRY.md` for
|
||||||
|
the live per-clip status table (source of truth, not duplicated here).
|
||||||
|
|
||||||
|
## Recently shipped (closed plans, folded into wiki)
|
||||||
|
|
||||||
|
- **Animation-gen pipeline** (2026-07-13) — the FBX→Blender-retarget→GLB→dance-JSON
|
||||||
|
bridge itself; core of `.agents/wiki/ARCHITECTURE.md` today.
|
||||||
|
- **Animation skills adoption** (2026-07-13) — `.claude/skills/animation*`,
|
||||||
|
`iclone-video-mocap`, `pose-estimation` brought in as repo skills.
|
||||||
|
- **Loop fix** (2026-07-16, plan + results) — `tools/loop_fix.py` (de-drift/trim/
|
||||||
|
seam-blend) and `tools/pingpong_bake.py` shipped; every adopted dance now ships
|
||||||
|
as a base clip + `_pp` pair, gated by `tools/loop_qc.py`.
|
||||||
|
- **Boat prop export** (2026-07-17, plan + fixes + results) — `tools/export_boat_prop.py`
|
||||||
|
staging pipeline for the boat action-clip set (see "Boat actions" below).
|
||||||
|
- **Animation-standard convergence** (2026-07-23/24, `.agents/wiki/devops-reports/`) —
|
||||||
|
full audit against ariki-game's animation-standardization contract. Result:
|
||||||
|
pipeline core already conformant (100% of runtime clip GLBs on the 65-bone
|
||||||
|
Quaternius rig, all `_pp` pairs present, dance JSONs clean). Fixes landed:
|
||||||
|
registry corrections (`tide_dance_01` has no sim enum entry — flag before
|
||||||
|
filming; `rain_dance_01` confirmed in-enum), `iclone-bridge.md` written
|
||||||
|
(previously a dead reference), QC-tool split documented (`loop_qc.py` here =
|
||||||
|
pre-commit gate, `ariki-game/tools/anim_qc.py` = post-ingest gate).
|
||||||
|
- **agents.md migration** (2026-07-24, this change) — repo restructured onto the
|
||||||
|
open agent protocol: root `AGENTS.md` (thin) + `.agents/` (SOUL, wiki, plans,
|
||||||
|
skills-pointer). `docs/` and root `plans/`/`devops-reports/` folded into
|
||||||
|
`.agents/wiki/`.
|
||||||
|
|
||||||
|
## In flight — ceremony dance slots awaiting a take
|
||||||
|
|
||||||
|
Per `.agents/wiki/dances/REGISTRY.md`, adopted so far: `fertility_dance_01`
|
||||||
|
(hula), `taming_dance_01`, `war_dance_01` (haka) — each shipped with its `_pp`
|
||||||
|
loop. One provisional take (`nd_01`, baraka1) is filmed and QC-passed but not
|
||||||
|
yet assigned to a ceremony. Still **awaiting a take**: `water_dance_01`,
|
||||||
|
`fire_dance_01`, `food_dance_01`, `sailing_dance_01`, `building_dance_01`,
|
||||||
|
`colony_shield_dance_01`, `age_progression_dance_01`, `star_watching_dance_01`,
|
||||||
|
`rain_dance_01`. `tide_dance_01` has no confirmed sim `DanceType` entry — resolve
|
||||||
|
with Ozan before filming.
|
||||||
|
|
||||||
|
## In flight — boat action clips
|
||||||
|
|
||||||
|
Staging tooling (`tools/export_boat_prop.py`) is built and the boat prop rig
|
||||||
|
verified; the action-clip set itself is **awaiting takes**: `boat_dive`,
|
||||||
|
`boat_climb`, `boat_collect`, `boat_row_loop`, `boat_fish_loop`,
|
||||||
|
`boat_sail_idle`. Two boat-adjacent clips are already live in-game
|
||||||
|
(`CanoeDismount`/`CanoeSitIdle`, `run_to_dive`) but predate this registry and
|
||||||
|
need a `boat_*` code assigned at the next adoption pass.
|
||||||
|
|
||||||
|
## Open threads
|
||||||
|
|
||||||
|
- `tools/kevin_retarget.py` has a hardcoded, non-portable target path — fine for
|
||||||
|
now, flagged in `AGENTS.md` gotchas.
|
||||||
|
- `.claude/skills/retarget-animations/` is kept only for history (deprecated,
|
||||||
|
superseded by the per-source retargeters in `tools/`).
|
||||||
@@ -33,7 +33,7 @@ This skill is the accumulated **do-it-this-way** knowledge from real runs.
|
|||||||
```
|
```
|
||||||
Blender here is **5.1.2**. When running from this repo the `--target` is **required** — the tool's default target path is ariki-relative and won't resolve.
|
Blender here is **5.1.2**. When running from this repo the `--target` is **required** — the tool's default target path is ariki-relative and won't resolve.
|
||||||
|
|
||||||
## Naming system (registry: `docs/dances/REGISTRY.md` — read it, it is the source of truth)
|
## Naming system (registry: `.agents/wiki/dances/REGISTRY.md` — read it, it is the source of truth)
|
||||||
Two-stage names, following the **game's item-id convention (lowercase snake_case)**.
|
Two-stage names, following the **game's item-id convention (lowercase snake_case)**.
|
||||||
**New dances get a provisional id, never a content name**: at conversion, assign the
|
**New dances get a provisional id, never a content name**: at conversion, assign the
|
||||||
next free `nd_##` from the registry — pack `nd_08`, clip `ND08`, JSON `nd_08.json`,
|
next free `nd_##` from the registry — pack `nd_08`, clip `ND08`, JSON `nd_08.json`,
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# AGENTS.md — animation repo
|
||||||
|
|
||||||
|
Entry point for AI agents working in `local.repo/animation` (all tools: pi,
|
||||||
|
Claude Code, Cursor). This repo is Jeremy's animation-pipeline hub **and** the
|
||||||
|
Mac↔PC bridge that carries iClone/mocap exports into `ariki-game`. iClone 8 +
|
||||||
|
Video Mocap live on the PC; Blender 5, the retarget tools, and the game repo
|
||||||
|
live on both machines.
|
||||||
|
|
||||||
|
Read `.agents/SOUL.md` at session start, then use the knowledge map below —
|
||||||
|
this file stays thin; the operational detail lives in `.claude/skills/` and
|
||||||
|
`.agents/`.
|
||||||
|
|
||||||
|
## Knowledge map
|
||||||
|
|
||||||
|
| Category | Path | When |
|
||||||
|
|---|---|---|
|
||||||
|
| Identity | `.agents/SOUL.md` | always-on |
|
||||||
|
| Batch workflow, canonical retarget command, naming, loop QC, gotchas, repo etiquette | `.claude/skills/animation/SKILL.md` | always-on — read before any batch or clip work |
|
||||||
|
| Architecture (pipeline stages, canonical rig, tool provenance) | `.agents/wiki/ARCHITECTURE.md` | consult when touching the pipeline |
|
||||||
|
| Per-system architecture detail | `.agents/wiki/architecture/` | consult-when |
|
||||||
|
| In-flight work + recently shipped | `.agents/wiki/master-plan.md` | check before starting new work |
|
||||||
|
| Dance/clip naming registry (source of truth for names) | `.agents/wiki/dances/REGISTRY.md` | always-on when naming or shipping a clip |
|
||||||
|
| PC-lane routing stub | `.agents/wiki/iclone-bridge.md` | consult-when |
|
||||||
|
| Point-in-time audits / convergence reports | `.agents/wiki/devops-reports/` | consult-when |
|
||||||
|
| Open/executing plans | `.agents/plans/` | consult-when |
|
||||||
|
| Other operator playbooks (iClone/mocap, clip authoring, pose estimation) | `.claude/skills/` (index: `.agents/skills/README.md`) | consult-when |
|
||||||
|
| Loop-QC sub-agent | `.claude/agents/loop-qc.md` | consult-when |
|
||||||
|
| Repo-wide conventions | `.agents/rules/` | always-on |
|
||||||
|
| Exchange-folder operator guide (which folder, when files move/archive) | `exchange/GUIDE.md` | consult-when doing a batch |
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
AGENTS.md ← you are here
|
||||||
|
.agents/ ← agent context: SOUL, wiki (architecture/master-plan/registry/
|
||||||
|
devops-reports), plans, rules, skills-pointer
|
||||||
|
.claude/ ← Claude-Code-native skills + agents (auto-discovered)
|
||||||
|
tools/ ← Blender-headless retargeters, loop tools, prop export
|
||||||
|
exchange/ ← files in flight between Mac and PC (see exchange/GUIDE.md)
|
||||||
|
archive/ ← retired takes — never deleted, never renumbered
|
||||||
|
```
|
||||||
|
|
||||||
|
## Git
|
||||||
|
|
||||||
|
- Branch is **`main`**. Sync via **`tinqs push`/`tinqs pull`** (not raw git —
|
||||||
|
this repo uses Git LFS for `.fbx/.glb/.mp4/.task`; a plain `git pull` hangs).
|
||||||
|
Report the commit hash after pushing.
|
||||||
|
- **Stage explicit paths, never `git add -A`** — ariki-game and this repo often
|
||||||
|
carry unrelated in-flight files.
|
||||||
|
- Never commit secrets.
|
||||||
|
|
||||||
|
## The rule that bites most often
|
||||||
|
|
||||||
|
**Green verify ≠ correct.** An agent-authored gate once passed an upside-down
|
||||||
|
boat export. Always render and eyeball a new visual asset before shipping —
|
||||||
|
full gotcha list in `.claude/skills/animation/SKILL.md`.
|
||||||
@@ -41,7 +41,7 @@ into the game repo.
|
|||||||
- `kevin_retarget.py` — Kevin Iglesias Unity FBX (`B-*` rig; hardcoded target path — non-portable)
|
- `kevin_retarget.py` — Kevin Iglesias Unity FBX (`B-*` rig; hardcoded target path — non-portable)
|
||||||
- `mocap_retarget.py` — MediaPipe pose JSON → GLB (rotation solving, no source armature)
|
- `mocap_retarget.py` — MediaPipe pose JSON → GLB (rotation solving, no source armature)
|
||||||
- `bake_run_punch.mjs` — composite upper/lower clip baking reference
|
- `bake_run_punch.mjs` — composite upper/lower clip baking reference
|
||||||
- `plans/` — pipeline design docs (animation generation pipeline, skills adoption)
|
- `AGENTS.md` / `.agents/` — agent context (identity, wiki, plans, rules); see `AGENTS.md` for the knowledge map
|
||||||
|
|
||||||
## Canonical target rig
|
## Canonical target rig
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# exchange/ — Folder guide for agents
|
||||||
|
|
||||||
|
This folder is the Mac↔PC bridge for the animation pipeline (iClone on the PC ↔
|
||||||
|
ariki-game on the Mac). Every file in here is **in transit** on one of the tracks
|
||||||
|
below. If you are an agent dropping files into this repo, or processing a batch,
|
||||||
|
this file tells you which folder to use, when a file moves to the next stage, and
|
||||||
|
when the previous version gets archived.
|
||||||
|
|
||||||
|
Related docs: `.agents/wiki/dances/REGISTRY.md` (naming — the source of truth),
|
||||||
|
`.agents/wiki/iclone-bridge.md` (PC-lane overview), and the `animation` skill (batch workflow details).
|
||||||
|
|
||||||
|
**Tool mirroring rule (audit 2026-07-23):** the retargeters in `tools/`
|
||||||
|
(`cc_retarget.py`, `mixamo_retarget.py`, `kevin_retarget.py`, `mocap_retarget.py`) are
|
||||||
|
**mirrors of `ariki-game/tools/`** — the game copies are authoritative. Never edit them
|
||||||
|
here; re-copy from ariki-game when they change (they silently diverged once already:
|
||||||
|
2026-07-15 → 2026-07-23, losing `--despike`/`--fingers`/`--map ual`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Folder map — EDIT PATHS HERE ONLY
|
||||||
|
|
||||||
|
Folder locations may change. Every rule in this guide refers to folders by the
|
||||||
|
**KEY** below, never by a hardcoded path. If a folder moves, update this table
|
||||||
|
(and nothing else in this file).
|
||||||
|
|
||||||
|
| KEY | Current path | One-liner |
|
||||||
|
|---|---|---|
|
||||||
|
| `INBOX` | `exchange/INBOX/` | Untriaged drop zone for anything sent to Jeremy |
|
||||||
|
| `INCOMING_FBX` | `exchange/incoming-fbx/` | Accepted FBX takes queued for retargeting |
|
||||||
|
| `CONVERTED_GLB` | `exchange/converted-glb/` | Retargeted game-ready GLBs (base clips) |
|
||||||
|
| `LOOPED_GLB` | `exchange/looped-glb/` | Seam-fixed loop variants from `tools/loop_fix.py` (created on demand) |
|
||||||
|
| `PINGPONG_GLB` | `exchange/pingpong-glb/` | Palindrome `_pp` loop variants from `tools/pingpong_bake.py` |
|
||||||
|
| `OUTGOING_PROPS` | `exchange/outgoing-props/` | Game → iClone direction: staging props + source motions for iClone editing |
|
||||||
|
| `REFERENCE_VIDEO` | `exchange/reference-video/` | Choreography/mocap reference footage |
|
||||||
|
| `ICLONE_PROJECTS` | `exchange/workingfiles-iclone/` | iClone `.iProject` working-file backups |
|
||||||
|
| `ARCHIVE` | `archive/` (repo root) | Rejected/superseded files — mirrors the exchange subfolder names |
|
||||||
|
| `GAME_DANCEGEN` | `<ariki-game>/assets/quaternius/dancegen/` | Final GLB destination in the game repo |
|
||||||
|
| `GAME_DANCES` | `<ariki-game>/assets/dances/` | Dance JSON destination in the game repo |
|
||||||
|
|
||||||
|
Notes that apply everywhere:
|
||||||
|
- Binary formats (`.fbx`, `.glb`, `.mp4`, `.task`) go through **Git LFS**; push with `tinqs push`.
|
||||||
|
- **Move, never delete.** Anything obsolete goes to `ARCHIVE` (see archiving rules at the bottom).
|
||||||
|
- Stage files **by explicit path** — never `git add -A` (this repo often has unrelated in-flight work).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Track 1 — INBOX triage (anyone → Jeremy)
|
||||||
|
|
||||||
|
**Folder:** `INBOX`
|
||||||
|
|
||||||
|
The only folder outside agents should ever write to. Drop whatever you're sending
|
||||||
|
(FBX, textures, JSON, video, etc.), commit, push. Do **not** place files directly
|
||||||
|
into the pipeline folders and do not reorganize the rest of `exchange/`.
|
||||||
|
|
||||||
|
| Event | Action |
|
||||||
|
|---|---|
|
||||||
|
| File arrives | Sits in `INBOX` untouched until Jeremy (or a session he directs) triages it |
|
||||||
|
| Triaged as an animation take | Move to `INCOMING_FBX` → enters Track 2 |
|
||||||
|
| Triaged as reference footage | Move to `REFERENCE_VIDEO` → Track 4 |
|
||||||
|
| Triaged as anything else | Jeremy decides; ask, don't guess |
|
||||||
|
| Junk / duplicate | Move to the matching `ARCHIVE` subfolder |
|
||||||
|
|
||||||
|
`INBOX` should trend toward **empty** — a file living there long-term means triage
|
||||||
|
was missed.
|
||||||
|
|
||||||
|
## Track 2 — Animation intake (iClone/PC → game) — the main track
|
||||||
|
|
||||||
|
**Stages:** `INBOX` → `INCOMING_FBX` → `CONVERTED_GLB` (+ loop variant in
|
||||||
|
`PINGPONG_GLB` or `LOOPED_GLB`) → copied to `GAME_DANCEGEN` + JSON in `GAME_DANCES`.
|
||||||
|
|
||||||
|
A "take" is a **file set that always travels together**: `<take>.fbx` +
|
||||||
|
`<take>.json` + `<take>.fbm/` texture folder (when present). Move all three,
|
||||||
|
always.
|
||||||
|
|
||||||
|
| Stage | What lives here | Moves forward when |
|
||||||
|
|---|---|---|
|
||||||
|
| `INCOMING_FBX` | Raw iClone FBX exports (1800f @ 60fps typical) awaiting retarget | Retargeted via `tools/cc_retarget.py` → GLB written to `CONVERTED_GLB` |
|
||||||
|
| `CONVERTED_GLB` | Game-ready GLB, named per the registry (`nd_##` provisional, ceremony code once adopted) | Copied (not moved) into `GAME_DANCEGEN` with a dance JSON |
|
||||||
|
| `PINGPONG_GLB` / `LOOPED_GLB` | The loop variant (`<pack>_pp` / seam-fixed). **Every shipped dance is a pair: base + loop.** Must pass `tools/loop_qc.py` (exit 0) | Copied into `GAME_DANCEGEN` alongside the base clip |
|
||||||
|
|
||||||
|
Rules on this track:
|
||||||
|
- **Naming:** new takes get the next free `nd_##` from `.agents/wiki/dances/REGISTRY.md`
|
||||||
|
at conversion — never a content name. Update the registry row at every
|
||||||
|
conversion/adoption/archive.
|
||||||
|
- **FBX stays in `INCOMING_FBX` after conversion** (it's the re-export source)
|
||||||
|
until its take is adopted or rejected.
|
||||||
|
- The GLB is **copied** to the game repo, not moved — `CONVERTED_GLB` keeps the
|
||||||
|
canonical copy.
|
||||||
|
|
||||||
|
**Archive triggers (move old version → matching `ARCHIVE` subfolder):**
|
||||||
|
|
||||||
|
| Trigger | What to archive |
|
||||||
|
|---|---|
|
||||||
|
| A **re-export of the same take** arrives (same choreography, new FBX) | The old FBX set from `INCOMING_FBX` and its old GLBs from `CONVERTED_GLB`/`PINGPONG_GLB`/`LOOPED_GLB` |
|
||||||
|
| Take is **rejected** | Its entire file set across all stage folders (and `archive/dances/` on the game side) |
|
||||||
|
| Take is **adopted** (renamed `nd_##` → ceremony code via `tools/rename_clip.py`) | The old `nd_##`-named files, once the renamed set is verified in-game |
|
||||||
|
|
||||||
|
Never archive mid-batch — wait until Jeremy confirms the batch looks right in-game.
|
||||||
|
|
||||||
|
## Track 3 — Outgoing to iClone (game/Mac → PC)
|
||||||
|
|
||||||
|
**Folder:** `OUTGOING_PROPS`
|
||||||
|
|
||||||
|
The reverse direction: anything exported from the game side for use inside
|
||||||
|
iClone. Two kinds live here today:
|
||||||
|
- **Staging props** (e.g. `boat/` — built by `tools/export_boat_prop.py`, with
|
||||||
|
renders for eyeball QA). Each prop gets its own subfolder with a README.
|
||||||
|
- **Source motion FBX** (Mixamo/Kevin-Iglesias clips like `Swimming.fbx`) sent
|
||||||
|
over as raw material for iClone editing. Note: some earlier source-motion drops
|
||||||
|
(the `HumanM@Mining*` set) landed in `INCOMING_FBX`; going forward,
|
||||||
|
game→PC source motions belong here.
|
||||||
|
|
||||||
|
| Event | Action |
|
||||||
|
|---|---|
|
||||||
|
| New version of a prop | Overwrite in place (git history is the archive for props) — but **render and eyeball before shipping**; a green verify gate has passed a visually-wrong prop before |
|
||||||
|
| PC has consumed the files and the resulting take shipped | Leave in place until Jeremy says to clean up |
|
||||||
|
|
||||||
|
## Track 4 — Reference footage
|
||||||
|
|
||||||
|
**Folder:** `REFERENCE_VIDEO`
|
||||||
|
|
||||||
|
Choreography/mocap reference videos (inputs for iClone Video Mocap and for
|
||||||
|
`pose-estimation` scoring). These are **inputs, not pipeline items** — they don't
|
||||||
|
advance through stages.
|
||||||
|
|
||||||
|
| Event | Action |
|
||||||
|
|---|---|
|
||||||
|
| New reference arrives (usually via `INBOX`) | Move here |
|
||||||
|
| The dance it references is adopted and shipped | Video becomes archivable — ask Jeremy before moving (these are large LFS objects; he may want them kept for re-scoring) |
|
||||||
|
|
||||||
|
## Track 5 — iClone working files
|
||||||
|
|
||||||
|
**Folder:** `ICLONE_PROJECTS`
|
||||||
|
|
||||||
|
`.iProject` backups from the PC so iClone sessions can be resumed/recovered.
|
||||||
|
These are big (100 MB+) LFS blobs.
|
||||||
|
|
||||||
|
| Event | Action |
|
||||||
|
|---|---|
|
||||||
|
| Newer save of the same project | Overwrite in place — git history keeps priors |
|
||||||
|
| Project's takes all adopted or rejected | Archivable on Jeremy's call only |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Archiving rules (all tracks)
|
||||||
|
|
||||||
|
1. `ARCHIVE` **mirrors the exchange subfolder names** — a file from
|
||||||
|
`INCOMING_FBX` goes to `ARCHIVE/incoming-fbx/`, a GLB from `CONVERTED_GLB` to
|
||||||
|
`ARCHIVE/converted-glb/`, etc. If a mirror subfolder doesn't exist yet, create it.
|
||||||
|
2. **`git mv`, never delete.** Archived files keep their names; nothing in
|
||||||
|
`ARCHIVE` is ever renamed or reused.
|
||||||
|
3. **Registry numbers are never reused.** An archived `nd_03` retires the number
|
||||||
|
forever.
|
||||||
|
4. Every archive action gets a **registry update** (`.agents/wiki/dances/REGISTRY.md`)
|
||||||
|
when it involves a tracked dance.
|
||||||
|
5. Game-side counterparts (GLBs/JSON already copied into ariki-game) archive to
|
||||||
|
`<ariki-game>/archive/dances/` (which is `.gdignore`d), in the same commit
|
||||||
|
cycle.
|
||||||
|
|
||||||
|
## Quick decision table
|
||||||
|
|
||||||
|
| You are… | You should… |
|
||||||
|
|---|---|
|
||||||
|
| An outside agent sending files | Drop in `INBOX`, commit, push. Touch nothing else. |
|
||||||
|
| Processing a new batch | Follow Track 2 top to bottom; read the `animation` skill first |
|
||||||
|
| Delivering game assets to the PC/iClone | Put them in `OUTGOING_PROPS` (subfolder per prop) |
|
||||||
|
| Holding a newer version of something already here | Same-name working files: overwrite. Pipeline takes: move the old set to `ARCHIVE` first (Track 2 triggers) |
|
||||||
|
| Unsure which track applies | Leave it in `INBOX` and say so — wrong-folder moves are worse than no move |
|
||||||
@@ -9,3 +9,5 @@ folders (`incoming-fbx/`, etc.) from there.
|
|||||||
Notes:
|
Notes:
|
||||||
- Binary formats (`.fbx`, `.glb`, `.mp4`, `.task`) go through Git LFS automatically.
|
- Binary formats (`.fbx`, `.glb`, `.mp4`, `.task`) go through Git LFS automatically.
|
||||||
- Don't reorganize other folders in `exchange/` — just drop your files here.
|
- Don't reorganize other folders in `exchange/` — just drop your files here.
|
||||||
|
- Full folder guide (what each `exchange/` folder is for, and how files move
|
||||||
|
between them): see [`../GUIDE.md`](../GUIDE.md).
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 9.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.3 MiB |
@@ -0,0 +1,13 @@
|
|||||||
|
# Lena character (from ariki-game)
|
||||||
|
|
||||||
|
The new Lena character, exported from `ariki-game/assets/quaternius/derived-bodies/`
|
||||||
|
(Ozan's standard-rig work, 2026-07-21 → standardized 2026-07-23).
|
||||||
|
|
||||||
|
| File | What it is |
|
||||||
|
|---|---|
|
||||||
|
| `Ariki_Female_QuatSkin.glb` | **⭐ THE canonical game body (standard 2026-07-23)** — Mixamo weights on Quaternius rest orientations, exact 65-bone UE-named contract, plays the stock clip libraries natively. Import THIS one. |
|
||||||
|
| `Ariki_Female_MixamoSkin.glb` | **SUPERSEDED** by QuatSkin (kept for reference) — Mixamo-convention rig (no `root` bone, Mixamo leaf names); needed the now-stale msrig clip reroute. |
|
||||||
|
| `lena_tpose_legs5_mixamo_rigged.fbx` | Mixamo-rigged T-pose FBX (legs5 variant) — the rigged source; best starting point for importing into iClone/CC or Mixamo on the PC. |
|
||||||
|
| `lena_tpose_for_mixamo.fbx` | Unrigged T-pose mesh, the file that was uploaded to Mixamo's auto-rigger. |
|
||||||
|
| `Lena_Body_Toned.png` / `Lena_Head_Toned.png` | Toned base-color textures (FBX files don't embed them). |
|
||||||
|
| `Lena_Head_Detail.png` | High-res head detail texture. |
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 9.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.3 MiB |
Binary file not shown.
+118
-1
@@ -12,6 +12,20 @@
|
|||||||
# Usage:
|
# Usage:
|
||||||
# blender --background --python tools/cc_retarget.py -- \
|
# blender --background --python tools/cc_retarget.py -- \
|
||||||
# --src "<dir-or-fbx>[,<more>…]" --out <out.glb> [--name <ClipName>] [--target <gltf>]
|
# --src "<dir-or-fbx>[,<more>…]" --out <out.glb> [--name <ClipName>] [--target <gltf>]
|
||||||
|
# [--fingers track|static] static = ignore captured finger articulation; fingers
|
||||||
|
# hold the target rig's rest pose (open hand) and the
|
||||||
|
# hand moves as one unit. Use for noisy AI-mocap fingers.
|
||||||
|
# [--despike] heal short AI-mocap glitches: frames whose world-space
|
||||||
|
# acceleration spikes far above the clip's median get
|
||||||
|
# re-interpolated from the good frames around them.
|
||||||
|
# Only short windows are touched, so intentional sharp
|
||||||
|
# moves survive. Extra important for ping-pong clips —
|
||||||
|
# a glitch plays twice (forward + reversed).
|
||||||
|
#
|
||||||
|
# Looping note: seamless looping is owned by the ping-pong bake (animation repo
|
||||||
|
# pingpong_bake.py → <pack>_pp), which loops and returns to start by construction.
|
||||||
|
# The former --inplace / --loop-blend passes were removed 2026-07-17; resurrect from
|
||||||
|
# git history (commit 535719601) if a forward-loop clip ever needs them again.
|
||||||
|
|
||||||
import bpy, sys, os, re
|
import bpy, sys, os, re
|
||||||
from mathutils import Matrix, Quaternion, Vector
|
from mathutils import Matrix, Quaternion, Vector
|
||||||
@@ -190,12 +204,113 @@ def retarget_one(src_arm, tgt_arm, action, name, scn, unit):
|
|||||||
return new_act
|
return new_act
|
||||||
|
|
||||||
|
|
||||||
|
DESPIKE_FACTOR = 200.0 # world accel must exceed FACTOR x the clip's median accel
|
||||||
|
DESPIKE_MAX_WIN = 12 # frames; longer regions are real motion, leave them alone
|
||||||
|
DESPIKE_PAD = 3
|
||||||
|
|
||||||
|
|
||||||
|
def _hermite_patch(kps, a, b):
|
||||||
|
"""Replace kps[a+1..b-1] with a cubic that matches value AND slope at both edges."""
|
||||||
|
v = [k.co.y for k in kps]
|
||||||
|
n = b - a
|
||||||
|
m0 = (v[a] - v[a - 1]) * n
|
||||||
|
m1 = (v[b + 1] - v[b]) * n
|
||||||
|
for i in range(a + 1, b):
|
||||||
|
t = (i - a) / n
|
||||||
|
h00 = 2 * t ** 3 - 3 * t ** 2 + 1
|
||||||
|
h10 = t ** 3 - 2 * t ** 2 + t
|
||||||
|
h01 = -2 * t ** 3 + 3 * t ** 2
|
||||||
|
h11 = t ** 3 - t ** 2
|
||||||
|
kps[i].co.y = h00 * v[a] + h10 * m0 + h01 * v[b] + h11 * m1
|
||||||
|
|
||||||
|
|
||||||
|
def despike(act, tgt_arm, scn):
|
||||||
|
"""Heal short AI-mocap glitches. Detection runs in WORLD space (same metric as
|
||||||
|
tools/anim_qc.py): a bone whose world acceleration towers over the clip's median
|
||||||
|
for a few frames is a tracking error, not choreography. The flagged bone's local
|
||||||
|
curves — and its parent's, where the glitch usually originates — are re-interpolated
|
||||||
|
across the window with slope-matched cubics."""
|
||||||
|
bones = sorted(set(BONE_MAP.values()))
|
||||||
|
prev = tgt_arm.animation_data.action
|
||||||
|
tgt_arm.animation_data.action = act
|
||||||
|
if getattr(act, "slots", None) and tgt_arm.animation_data.action_slot is None:
|
||||||
|
tgt_arm.animation_data.action_slot = act.slots[0]
|
||||||
|
f0, f1 = (int(x) for x in act.frame_range)
|
||||||
|
|
||||||
|
pos = {n: [] for n in bones}
|
||||||
|
for f in range(f0, f1 + 1):
|
||||||
|
scn.frame_set(f)
|
||||||
|
dg = bpy.context.evaluated_depsgraph_get()
|
||||||
|
te = tgt_arm.evaluated_get(dg)
|
||||||
|
for n in bones:
|
||||||
|
pos[n].append((tgt_arm.matrix_world @ te.pose.bones[n].matrix).translation.copy())
|
||||||
|
tgt_arm.animation_data.action = prev
|
||||||
|
|
||||||
|
acc = {n: [(p[i + 1] - 2 * p[i] + p[i - 1]).length for i in range(1, len(p) - 1)]
|
||||||
|
for n, p in pos.items()}
|
||||||
|
flat = sorted(x for arr in acc.values() for x in arr)
|
||||||
|
thr = DESPIKE_FACTOR * (flat[len(flat) // 2] or 1e-9)
|
||||||
|
|
||||||
|
by_bone = {}
|
||||||
|
for fc in action_fcurves(act):
|
||||||
|
if fc.data_path.startswith('pose.bones["'):
|
||||||
|
by_bone.setdefault(fc.data_path.split('"')[1], []).append(fc)
|
||||||
|
|
||||||
|
repaired = 0
|
||||||
|
for n, arr in acc.items():
|
||||||
|
bad = [i + 1 for i, a in enumerate(arr) if a > thr] # -> pos/key index
|
||||||
|
if not bad:
|
||||||
|
continue
|
||||||
|
spans, s, e = [], bad[0], bad[0]
|
||||||
|
for i in bad[1:]:
|
||||||
|
if i - e <= DESPIKE_PAD * 2:
|
||||||
|
e = i
|
||||||
|
else:
|
||||||
|
spans.append((s, e))
|
||||||
|
s = e = i
|
||||||
|
spans.append((s, e))
|
||||||
|
parent = tgt_arm.data.bones[n].parent
|
||||||
|
chain = [n] + ([parent.name] if parent else [])
|
||||||
|
for s, e in spans:
|
||||||
|
a = max(1, s - DESPIKE_PAD)
|
||||||
|
b = min(len(pos[n]) - 2, e + DESPIKE_PAD)
|
||||||
|
if b - a > DESPIKE_MAX_WIN or b - a < 2:
|
||||||
|
continue
|
||||||
|
for bone in chain:
|
||||||
|
for fc in by_bone.get(bone, []):
|
||||||
|
kps = sorted(fc.keyframe_points, key=lambda k: k.co.x)
|
||||||
|
_hermite_patch(kps, a, b)
|
||||||
|
fc.update()
|
||||||
|
repaired += 1
|
||||||
|
print(f"[cc] --despike: {n} @ frame {f0 + s}"
|
||||||
|
f" ({(s) / 60:.2f}s) window {b - a}f via {'+'.join(chain)}")
|
||||||
|
print(f"[cc] --despike: repaired {repaired} glitch window(s), threshold {thr:.4f}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||||
args = dict(zip(argv[::2], argv[1::2]))
|
args, flags, i = {}, set(), 0
|
||||||
|
while i < len(argv):
|
||||||
|
if argv[i] == "--despike":
|
||||||
|
flags.add("despike")
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
args[argv[i]] = argv[i + 1]
|
||||||
|
i += 2
|
||||||
srcs, out = args["--src"].split(","), args["--out"]
|
srcs, out = args["--src"].split(","), args["--out"]
|
||||||
target = args.get("--target", DEFAULT_TARGET_GLTF)
|
target = args.get("--target", DEFAULT_TARGET_GLTF)
|
||||||
forced_name = args.get("--name")
|
forced_name = args.get("--name")
|
||||||
|
fingers = args.get("--fingers", "track")
|
||||||
|
if fingers not in ("track", "static"):
|
||||||
|
raise SystemExit(f"--fingers must be 'track' or 'static', got '{fingers}'")
|
||||||
|
if fingers == "static":
|
||||||
|
# Unmapped bones ride their parent at rest offset, so dropping the finger
|
||||||
|
# entries makes each hand move as one rigid unit in the rig's rest hand pose.
|
||||||
|
dropped = [k for k, v in BONE_MAP.items()
|
||||||
|
if re.match(r"(thumb|index|middle|ring|pinky)_", v)]
|
||||||
|
for k in dropped:
|
||||||
|
del BONE_MAP[k]
|
||||||
|
print(f"[cc] --fingers static: ignoring {len(dropped)} finger bone mappings")
|
||||||
|
|
||||||
fbx_files = []
|
fbx_files = []
|
||||||
for s in srcs:
|
for s in srcs:
|
||||||
@@ -241,6 +356,8 @@ def main():
|
|||||||
print(f"[cc] SKIP (no armature take): {f}")
|
print(f"[cc] SKIP (no armature take): {f}")
|
||||||
else:
|
else:
|
||||||
baked = retarget_one(src_arm, tgt_arm, act, name, scn, unit)
|
baked = retarget_one(src_arm, tgt_arm, act, name, scn, unit)
|
||||||
|
if "despike" in flags:
|
||||||
|
despike(baked, tgt_arm, scn)
|
||||||
done.append(baked)
|
done.append(baked)
|
||||||
print(f"[cc] {i + 1}/{len(fbx_files)} {name} "
|
print(f"[cc] {i + 1}/{len(fbx_files)} {name} "
|
||||||
f"({int(baked.frame_range[1])}f @60, from take '{act.name}')")
|
f"({int(baked.frame_range[1])}f @60, from take '{act.name}')")
|
||||||
|
|||||||
@@ -23,7 +23,8 @@
|
|||||||
import bpy, sys, os, re
|
import bpy, sys, os, re
|
||||||
from mathutils import Matrix, Quaternion, Vector
|
from mathutils import Matrix, Quaternion, Vector
|
||||||
|
|
||||||
TARGET_GLTF = "/Users/behcetozanbozkurt/Documents/tinqs-ltd/ariki-game/assets/quaternius/base-characters/Universal Base Characters[Standard]/Base Characters/Godot - UE/Superhero_Male_FullBody.gltf"
|
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
TARGET_GLTF = os.path.join(REPO_ROOT, "assets/quaternius/base-characters/Universal Base Characters[Standard]/Base Characters/Godot - UE/Superhero_Male_FullBody.gltf")
|
||||||
|
|
||||||
# Kevin "B-*" → Quaternius UE names. Fingers map 1:1 (both rigs have 3-segment fingers).
|
# Kevin "B-*" → Quaternius UE names. Fingers map 1:1 (both rigs have 3-segment fingers).
|
||||||
BONE_MAP = {
|
BONE_MAP = {
|
||||||
|
|||||||
@@ -57,6 +57,22 @@ def detect_prefix(arm):
|
|||||||
raise RuntimeError("no *Hips bone found — not a Mixamo rig?")
|
raise RuntimeError("no *Hips bone found — not a Mixamo rig?")
|
||||||
|
|
||||||
|
|
||||||
|
# ── UAL/Quaternius-source mode (2026-07-21): retarget stock-skeleton clips (UAL,
|
||||||
|
# Kevin) onto a DIFFERENT-convention skeleton (e.g. the renamed Mixamo rig — its
|
||||||
|
# bone local frames differ, so raw clip playback tips her face-down). Identity
|
||||||
|
# bone map (names already match), source frame bones are the Quaternius names.
|
||||||
|
QUAT_BONES = ["pelvis", "spine_01", "spine_02", "spine_03", "neck_01", "Head"]
|
||||||
|
for _T in ("l", "r"):
|
||||||
|
QUAT_BONES += [f"clavicle_{_T}", f"upperarm_{_T}", f"lowerarm_{_T}", f"hand_{_T}",
|
||||||
|
f"thigh_{_T}", f"calf_{_T}", f"foot_{_T}", f"ball_{_T}"]
|
||||||
|
for _i in (1, 2, 3):
|
||||||
|
QUAT_BONES += [f"index_0{_i}_{_T}", f"middle_0{_i}_{_T}", f"ring_0{_i}_{_T}",
|
||||||
|
f"pinky_0{_i}_{_T}", f"thumb_0{_i}_{_T}"]
|
||||||
|
UAL_MAP = {n: n for n in QUAT_BONES}
|
||||||
|
# Per-mode source-frame bones (see retarget_one): mixamo | ual
|
||||||
|
SRC_FRAME_BONES = ("Hips", "Head", "LeftArm")
|
||||||
|
|
||||||
|
|
||||||
def apply_object_transform(obj):
|
def apply_object_transform(obj):
|
||||||
"""Bake importer object-level rotation/scale into the armature data (meters, Z-up)."""
|
"""Bake importer object-level rotation/scale into the armature data (meters, Z-up)."""
|
||||||
bpy.ops.object.select_all(action="DESELECT")
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
@@ -109,11 +125,12 @@ def retarget_one(src_arm, tgt_arm, action, name, scn, prefix, unit):
|
|||||||
tgt_rest_q = {n: m.to_quaternion().normalized() for n, m in tgt_rest.items()}
|
tgt_rest_q = {n: m.to_quaternion().normalized() for n, m in tgt_rest.items()}
|
||||||
|
|
||||||
C = (char_frame(tgt_rest, "pelvis", "Head", "upperarm_l")
|
C = (char_frame(tgt_rest, "pelvis", "Head", "upperarm_l")
|
||||||
@ char_frame(src_rest, prefix + "Hips", prefix + "Head", prefix + "LeftArm").inverted()).normalized()
|
@ char_frame(src_rest, prefix + SRC_FRAME_BONES[0], prefix + SRC_FRAME_BONES[1],
|
||||||
|
prefix + SRC_FRAME_BONES[2]).inverted()).normalized()
|
||||||
Cinv = C.inverted()
|
Cinv = C.inverted()
|
||||||
|
|
||||||
# Uniform size ratio from hips→head world distance (unit/scale agnostic).
|
# Uniform size ratio from hips→head world distance (unit/scale agnostic).
|
||||||
h_src = (src_rest[prefix + "Head"].translation - src_rest[prefix + "Hips"].translation).length
|
h_src = (src_rest[prefix + SRC_FRAME_BONES[1]].translation - src_rest[prefix + SRC_FRAME_BONES[0]].translation).length
|
||||||
h_tgt = (tgt_rest["Head"].translation - tgt_rest["pelvis"].translation).length
|
h_tgt = (tgt_rest["Head"].translation - tgt_rest["pelvis"].translation).length
|
||||||
tscale = h_tgt / h_src if h_src > 1e-5 else 1.0
|
tscale = h_tgt / h_src if h_src > 1e-5 else 1.0
|
||||||
|
|
||||||
@@ -127,7 +144,7 @@ def retarget_one(src_arm, tgt_arm, action, name, scn, prefix, unit):
|
|||||||
for pb in tgt_arm.pose.bones:
|
for pb in tgt_arm.pose.bones:
|
||||||
pb.rotation_mode = "QUATERNION"
|
pb.rotation_mode = "QUATERNION"
|
||||||
|
|
||||||
src_hips_rest_t = src_rest[prefix + "Hips"].translation
|
src_hips_rest_t = src_rest[prefix + SRC_FRAME_BONES[0]].translation
|
||||||
|
|
||||||
for f in range(f0, f1 + 1):
|
for f in range(f0, f1 + 1):
|
||||||
scn.frame_set(f)
|
scn.frame_set(f)
|
||||||
@@ -181,18 +198,23 @@ def retarget_one(src_arm, tgt_arm, action, name, scn, prefix, unit):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
global BONE_MAP, SRC_FRAME_BONES
|
||||||
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||||
args = dict(zip(argv[::2], argv[1::2]))
|
args = dict(zip(argv[::2], argv[1::2]))
|
||||||
srcs, out = args["--src"].split(","), args["--out"]
|
srcs, out = args["--src"].split(","), args["--out"]
|
||||||
limit = int(args.get("--limit", 0))
|
limit = int(args.get("--limit", 0))
|
||||||
target = args.get("--target", DEFAULT_TARGET_GLTF)
|
target = args.get("--target", DEFAULT_TARGET_GLTF)
|
||||||
|
clips_filter = [c.strip().lower() for c in args.get("--clips", "").split(",") if c.strip()]
|
||||||
|
if args.get("--map") == "ual":
|
||||||
|
BONE_MAP = UAL_MAP # identity: source bones already quat-named
|
||||||
|
SRC_FRAME_BONES = ("pelvis", "Head", "upperarm_l")
|
||||||
|
|
||||||
fbx_files = []
|
fbx_files = []
|
||||||
for s in srcs:
|
for s in srcs:
|
||||||
s = s.strip()
|
s = s.strip()
|
||||||
if os.path.isdir(s):
|
if os.path.isdir(s):
|
||||||
for root, _, files in os.walk(s):
|
for root, _, files in os.walk(s):
|
||||||
fbx_files += [os.path.join(root, x) for x in sorted(files) if x.lower().endswith(".fbx")]
|
fbx_files += [os.path.join(root, x) for x in sorted(files) if x.lower().endswith((".fbx", ".glb", ".gltf"))]
|
||||||
else:
|
else:
|
||||||
fbx_files.append(s)
|
fbx_files.append(s)
|
||||||
if limit:
|
if limit:
|
||||||
@@ -218,22 +240,31 @@ def main():
|
|||||||
name = clip_name(f)
|
name = clip_name(f)
|
||||||
pre = set(bpy.data.objects)
|
pre = set(bpy.data.objects)
|
||||||
pre_actions = set(bpy.data.actions)
|
pre_actions = set(bpy.data.actions)
|
||||||
bpy.ops.import_scene.fbx(filepath=f)
|
if f.lower().endswith((".glb", ".gltf")):
|
||||||
|
bpy.ops.import_scene.gltf(filepath=f)
|
||||||
|
else:
|
||||||
|
bpy.ops.import_scene.fbx(filepath=f)
|
||||||
new_objs = [o for o in bpy.data.objects if o not in pre]
|
new_objs = [o for o in bpy.data.objects if o not in pre]
|
||||||
src_arm = next((o for o in new_objs if o.type == "ARMATURE"), None)
|
src_arm = next((o for o in new_objs if o.type == "ARMATURE"), None)
|
||||||
new_acts = [a for a in bpy.data.actions if a not in pre_actions]
|
new_acts = [a for a in bpy.data.actions if a not in pre_actions]
|
||||||
|
if clips_filter:
|
||||||
|
new_acts = [a for a in new_acts if any(c in a.name.lower() for c in clips_filter)]
|
||||||
if src_arm is None or not new_acts:
|
if src_arm is None or not new_acts:
|
||||||
print(f"[mixamo] SKIP (no armature/action): {f}")
|
print(f"[mixamo] SKIP (no armature/action): {f}")
|
||||||
else:
|
else:
|
||||||
bpy.context.view_layer.update()
|
bpy.context.view_layer.update()
|
||||||
prefix = detect_prefix(src_arm)
|
prefix = "" if args.get("--map") == "ual" else detect_prefix(src_arm)
|
||||||
# Importer unit scale (cm→m 0.01) — capture BEFORE transform_apply wipes it;
|
# Importer unit scale (cm→m 0.01) — capture BEFORE transform_apply wipes it;
|
||||||
# the hips translation math needs it (fcurves stay in source units).
|
# the hips translation math needs it (fcurves stay in source units).
|
||||||
unit = src_arm.scale.x
|
unit = src_arm.scale.x
|
||||||
apply_object_transform(src_arm)
|
apply_object_transform(src_arm)
|
||||||
act = retarget_one(src_arm, tgt_arm, new_acts[0], name, scn, prefix, unit)
|
for act_src in new_acts:
|
||||||
done.append(act)
|
out_name = act_src.name if args.get("--map") == "ual" else name
|
||||||
print(f"[mixamo] {i + 1}/{len(fbx_files)} {name} ({int(act.frame_range[1])}f, prefix='{prefix}')")
|
if args.get("--map") == "ual":
|
||||||
|
act_src.name += "__src" # free the clean name — Blender dedupes the output to .001 otherwise
|
||||||
|
act = retarget_one(src_arm, tgt_arm, act_src, out_name, scn, prefix, unit)
|
||||||
|
done.append(act)
|
||||||
|
print(f"[mixamo] {i + 1}/{len(fbx_files)} {out_name} ({int(act.frame_range[1])}f, prefix='{prefix}')")
|
||||||
for o in new_objs:
|
for o in new_objs:
|
||||||
bpy.data.objects.remove(o, do_unlink=True)
|
bpy.data.objects.remove(o, do_unlink=True)
|
||||||
for a in new_acts:
|
for a in new_acts:
|
||||||
|
|||||||
Reference in New Issue
Block a user