Compare commits

...

31 Commits

Author SHA1 Message Date
jeremy 854dbb5ce4 chore(agents): migrate to agents.md protocol — .agents/ wiki/plans/rules/skills
Root AGENTS.md is now a thin entry point + knowledge map; the batch-workflow
detail it duplicated already lived in .claude/skills/animation/SKILL.md.
Folded docs/ (dances registry + iclone-bridge stub), root plans/, and
devops-reports/ into .agents/wiki/ and .agents/plans/ per the per-repo .agents/
convention. Added .agents/SOUL.md, .agents/AGENTS.md, wiki/ARCHITECTURE.md,
wiki/architecture/, and wiki/master-plan.md (derived from the closed plans +
the dance registry's live ceremony-slot table). Fixed doc-path references in
exchange/GUIDE.md, the animation skill, and the iclone-bridge stub. All moves
via mv (git detected as renames) — no content deleted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:02:47 -07:00
jeremy 28fdf940f7 revert(inbox): restore Ozan's shipped QuatSkin — v8 rig experiment backed out
The v8 body pushed here (078b0b7) broke Lena's hands + eyebrow fit and its
rest-mismatch premise was unconfirmed (ariki-game 174a0429e). Back to baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:33:01 -07:00
jeremy 06d950649b docs: add AGENTS.md — agent orientation guide for the animation bridge
Fast-orientation guide synthesized from README.md and the animation skill:
batch workflow, canonical retarget command, nd_## naming, loop QC, gotchas,
and repo etiquette. Points to the skills and docs/dances/REGISTRY.md as
authoritative sources.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:20:27 -07:00
jeremy 078b0b7a07 feat(inbox): Ariki_Female_QuatSkin v8 — rig fixed onto the UAL clip rest
Supersedes the previous QuatSkin drop: skeleton now matches UAL1.glb exactly
(0.0001° all 65 joints; ariki-game e67df53e6). Plays the stock clip libraries
without crumpling. Gate: ariki-game tools/rig_pose_gate.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:08:42 -07:00
jeremy 1219954fdc chore(pm): move devops-reports/ to repo root (Jeremy)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:36:02 -07:00
jeremy 82266c97bb docs(pm): devops report — animation-standard convergence 2026-07-23/24
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 10:58:43 -07:00
jeremy b4d962167f fix(audit): converge on the 2026-07-23 animation standard — tool sync, QuatSkin INBOX, registry corrections
From the conformance audit (ariki-game .agents/audits/animation-standard-assessment-2026-07-23.md):
- tools/: cc_retarget + mixamo_retarget re-synced from ariki-game (were stale forks
  missing --despike/--fingers/--map ual); kevin_retarget path now repo-relative;
  mirror rule documented in exchange/GUIDE.md
- INBOX/lena: add canonical Ariki_Female_QuatSkin.glb; README marks MixamoSkin superseded
- REGISTRY: tide_dance_01 flagged (no DanceType enum entry); rain_dance note corrected
  (RainDance IS in the enum); boat table notes live canoedismount1/run_to_dive; QC split
  (loop_qc pre-commit vs anim_qc post-ingest) documented
- docs/iclone-bridge.md: stub created (GUIDE referenced it but it never existed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:37:55 -07:00
jeremy 998e306804 feat(inbox): Lena character drop — game GLB, Mixamo-rigged FBX, textures
For PC import: standard-rig Lena (Ariki_Female_MixamoSkin.glb) as used
in-game, plus the rigged/unrigged T-pose FBX sources and toned textures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 13:03:22 -07:00
jeremy 228f26e7dc Merge branch 'main' of https://tinqs.com/tinqs/animation 2026-07-23 13:02:10 -07:00
jeremy cd2e598354 docs(exchange): add folder guide with tracks, archive triggers, and changeable path map
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:23:41 -07:00
jeremy 7f4300eae6 assets(exchange): add lena_tpose_legs5_input.fbx to INBOX from ariki-game
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 13:29:30 -07:00
jeremy 9f3f5ac891 @
feat(exchange): native Kevin Iglesias pickaxe mining FBX for iClone editing

Two-hand Mining set (Begin/Loop Ground/Loop Wall/Stop) on the native B-* rig,
pulled from ariki-assets animations/kevin-iglesias. Native rig (not the
Quaternius-retargeted game GLB) so iClone can characterize the skeleton to HIK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-21 13:29:18 -07:00
jeremy 528bf1a905 feat(exchange): add INBOX drop folder for incoming files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 13:25:59 -07:00
jeremy b68fa7b920 exchange: Lena (Mixamo-rigged) + two-handed axe for iClone scene work
- lena/lena_tpose_legs5_mixamo_rigged.fbx — 65-bone mixamorig skeleton,
  mesh intact, T-pose; iClone characterizes Mixamo rigs natively.
  Textures alongside (Body_Toned, Head_Toned, Head_Detail) for relinking.
- axe_two_handed.fbx — the game's Polynesian axe (assets/models/glbs/axe.glb
  via build_axe.py, 1.30 m), converted GLB→FBX with textures embedded;
  import as a prop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 12:21:06 -07:00
jeremy c79d23549a feat(dances): convert baraka1 → nd_01 (+ nd_01_pp loop)
Retarget baraka1.fbx (iClone, 1800f@60) to nd_01.glb (clip ND01) and bake
the ping-pong loop variant nd_01_pp.glb (clip ND01PP). _pp passes loop_qc.py
(0.00 seam pose/velocity gap). Provisional id per registry; awaiting ceremony
assignment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 10:28:07 -07:00
jeremy 19181157cc Merge branch 'main' of https://tinqs.com/jeremy/animation 2026-07-21 10:19:34 -07:00
jeremy 3b132ebe9e assets(exchange): re-export baraka1 with full motion
Replace the short baraka1 FBX with a larger motion-inclusive export and updated sidecar JSON.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 10:19:07 -07:00
jeremy f468d148bc feat(profiling): add dance motion profiler + beat-energy profiles for adopted dances
tools/dance_profile.py samples world-space bone positions per frame in
Blender headless and emits a per-beat motion-energy timeline (accents,
phrases, totals) at a given BPM. Profiles generated for war_dance_01,
fertility_dance_01, taming_dance_01.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 09:52:06 -07:00
jeremy 7b44019c9a assets(exchange): add baraka1 incoming iClone FBX
Incoming baraka1 export (FBX, sidecar JSON, textures) for the next retarget batch.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 09:31:04 -07:00
jeremy 485c27a04c assets(exchange): canoe dismount incoming + water/prop FBX batch
Add canoedismount1 iClone export for retarget, swim/dive/talk staging clips, and boat_prop hull texture.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 18:13:47 -07:00
jeremy a4ad489229 assets(exchange): add dance2/haka2 incoming FBX and reference videos
Incoming iClone exports (dance2, haka2) plus mocap reference clips and Dance2.iProject for the next retarget batch.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 18:37:21 -07:00
jeremy 4d6a4f6006 feat(props): export game boat as iClone staging prop (boat_prop.fbx)
tools/export_boat_prop.py (GLM-implemented, 2 rounds): hull at game scale
(8.5 m, keel 0, no recenter) + placeholder mast/sail/steering oar + 5 seat
markers + waterline + 1.9 m ref figure; self-verify incl. right-side-up
gates added after round 1 shipped an upside-down hull with all checks
green. Registry: boat actions table (6 slots, direct names). README
carries CC BY 4.0 attribution + iClone import/filming notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 10:40:37 -07:00
jeremy 17ff1f748e assets(reference): Māori haka reference video (Polynesian Cultural Center, Oahu)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:58:03 -07:00
jeremy df2629af16 feat(naming): adopt war_dance_01, fertility_dance_01, taming_dance_01
hakadance1->war_dance_01, aloha1->fertility_dance_01, dance1->taming_dance_01,
each with a _pp loop variant (all SMOOTH per loop_qc). Loop rule: every
adopted dance ships base + _pp pair. GLM trim+blend loop variants archived
(ping-pong is the loop standard).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:48:52 -07:00
jeremy 9b190e742e docs(naming): ceremony codes follow game item-id convention (snake_case DanceType)
13 ceremony slots tabled in the registry (12 DanceType + rain_dance),
provisional ids now nd_##; codes like war_dance_01 / WarDance01.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:36:03 -07:00
jeremy 6a1290b1d1 docs(naming): dance codes are ceremony-based (<ceremony><NN>)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:28:35 -07:00
jeremy b918d8670f feat(naming): dance registry + two-stage naming system; archive non-keepers
Keep aloha1(+pp), dance1(+pp), hakadance1. Archive hakadance2, alohaOG,
firedance1, hakaOG (all variants) to archive/. New dances get provisional
nd## ids at conversion; rename to a dance code (haka01 etc.) only on
adoption via tools/rename_clip.py. Registry: docs/dances/REGISTRY.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:06:56 -07:00
jeremy da0567b79d docs(dances): dance1 motion description from Blender kinematic analysis
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 13:48:10 -07:00
jeremy ac691dee90 feat(tools): pingpong_bake — palindrome loops; 7 *_pp GLBs (all SMOOTH)
Forward+reversed baked as one clip: end==start by construction, drift
self-cancels, full choreography kept; motion reverses at the turn point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 13:48:10 -07:00
jeremy d8e2f2782c feat(tools): loop_fix (de-drift + trim + seam blend) + 7 looped GLBs
Implemented by GLM from plans/loop-fix-plan-2026-07-16.md; all seven clips
verified SMOOTH by loop_qc (0° pose gap, 0 cm drift, 60-89% duration kept).
Results: plans/loop-fix-results-2026-07-16.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 13:48:09 -07:00
jeremy d3206f6ce2 feat(qc): loop_qc tool + loop-qc agent — measure loop-seam smoothness
Pose gap / velocity gap / root drift at the LoopMode.Linear wrap; exit-code
gated. Skill updated: loop QC is workflow step 5, with the Blender-side
fixer playbook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 13:48:09 -07:00
291 changed files with 9072 additions and 29 deletions
+11
View File
@@ -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`.
+26
View File
@@ -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,46 @@
# FIX ROUND: defects found in boat_prop.fbx by independent visual verification
Your self-verify passes but the boat is **visually wrong**. Renders of your FBX vs a
raw import of the source GLB prove two defects. Fix `tools/export_boat_prop.py`,
re-run until BOTH the existing self-verify AND the new checks below pass, then update
`plans/boat-prop-export-results-2026-07-17.md`.
## Defect 1 — hull is UPSIDE DOWN
In your export the canoe's carved prows curl DOWN below the waterline and the belly
bulges up. Ground truth: `bpy.ops.import_scene.gltf` on the source GLB already gives a
RIGHT-SIDE-UP canoe in Blender's Z-up frame (deck opening faces +Z, prows sweep up).
The glTF importer performs the Y-up→Z-up conversion itself — your extra game→Blender
axis conversion (the C3 matrix) double-rotates the mesh.
**Correct transform chain** (nothing else):
1. Import GLB; apply all transforms (bakes the importer's rotation into vertices).
2. Rotate about **Z only** so the bow lies along Y (raw bow is along +X after step 1).
3. Scale ×8.5 uniformly; apply.
4. Translate Z only so min-Z = 0. **Do NOT translate or recenter X/Y** — the game uses
the GLB's own origin (Defect 2).
## Defect 2 — hull was recentered on X
The canoe is a SINGLE outrigger (one ama, one side) yet your export's X bounds are
symmetric (±2.82) — you recentered by AABB. The game never translates X; keep the
GLB's own lateral placement so the hull centerline stays where the seat markers
assume it. After the fix, report which side the ama extends to (expect X, the
Seat_Fisher side, if the game-equivalent yaw is right; report honestly if it's +X).
## New self-verify checks to ADD (these would have caught both defects)
- **Right-side-up**: the hull's LOWEST vertex (z≈0, the keel) must lie near midships
(|y| < 2.0 m), and the hull's HIGHEST vertices (prow carvings) must lie near the
ends (|y| > 3.0 m). An upside-down canoe fails both.
- **Not recentered**: the hull mesh's X bounds must be ASYMMETRIC about 0 (single
outrigger): assert `abs(abs(minX) - abs(maxX)) > 0.5` m.
## Verification renders (run these yourself and STATE in the report what you see)
Render workbench snapshots (¾ view + side view) of the final FBX re-imported, e.g.
camera at (14,12,8) looking at (0,0,1.2). The canoe must sit prows-up on the
waterline rectangle like a boat, not like a banana on its back.
Everything else from the original plan stands (guardrails §6 included: no commits,
ariki-game read-only). The waterline-as-4-named-rails deviation is ACCEPTED — keep it.
@@ -0,0 +1,160 @@
# Plan: `tools/export_boat_prop.py` — export the game boat as an iClone staging prop
**Status:** ready for implementation · **Author:** Fable 5 session 2026-07-17 · **Implementer:** GLM session
## 0. Context (you have no other context — read this fully)
This repo is the animation bridge for the game repo at
`/Users/jeremykashkett/Tinqs/local.repo/ariki-game` (read-only for you). Jeremy will
author boat-interaction character animations in iClone 8 on a Windows PC and needs the
game's boat as a correctly-scaled FBX prop.
The game's vessel: `ariki-game/assets/models/glbs/Boat__PolynesianCanoe_hull.glb`
(832 KB, one mesh `Object_0`, embedded jpg texture) — a carved hull WITH baked ama
outrigger + iako booms; only the sail was cut off. At runtime,
`ariki-game/src/Viewer/BoatRenderer.cs` scales it ×8.5, rotates it (0,90°,0), and
builds the mast/sail/steering-oar procedurally plus 5 seat marker nodes. Your tool
reproduces that assembled vessel as one static FBX.
Build tool: Blender 5.1.2 headless at `/Applications/Blender.app/Contents/MacOS/Blender`.
Run pattern (match `tools/cc_retarget.py` style — argv after `--`, `[export_boat_prop]`
prefixed prints):
```
"$BLENDER" --background --python tools/export_boat_prop.py -- \
[--hull <path>] [--out exchange/outgoing-props/boat/boat_prop.fbx] [--scale 8.5] \
[--no-ref-figure] [--fbx-scale-mode FBX_SCALE_UNITS] [--no-verify]
```
## 1. Ground-truth constants (harvested from BoatRenderer.cs — trust these)
Game frame: meters, Y-up, bow = +Z, keel at Y=0. BoatLength=8.5, BoatWidth=2.4,
halfLen=4.25. GLB raw ≈1.0 unit long along its local +X.
- Hull: rotate so glb bow (+X) → game +Z; uniform ×8.5; lift so keel (min Y) = 0.
Expected scaled AABB ≈ 8.5 L × 5.64 W (includes ama) × 2.95 H.
- `deckTop = max(0.12 × scaledHeight, 0.4)`**0.40 m** — compute from the measured
box, don't hardcode.
- Seats (name → game (x,y,z) m): Seat_Navigator (0, 0.75, +2.55) · Seat_Lookout
(0, 4.00, 0.20) · Seat_Fisher (1.50, 0.65, 0) · Seat_Rest (0, 0.75, 2.975) ·
Seat_Helm (+0.072, 0.50, 2.72).
- Steering oar (hoe uli): pivot at game (0.384, 0.75, 3.655); pivot rotation
(5°, 10°, 0°) then oar-mesh child rotation (38°, 0, 0) — raked aft. Shaft along the
oar's local Y: grip tip +1.31, pivot 0, blade center ≈ 1.44 (blade ~0.34 wide,
~1.1 long, ~0.05 thick), tip 2.11. Shaft radius ~0.04.
- Sail assembly: pivot (0, 0.40, 0). Mast: radius 0.055, height 4.2, base at deckTop.
Sail: crab-claw, foot width 2.1 spreading toward game **X**, height 3.8, foot at
game y = deckTop+0.45, z = 0.10. Boom: radius 0.045, length 2.205, along game X,
centered (1.05, deckTop+0.45, 0.10).
- **No outrigger placeholder** — the ama is baked into the GLB.
- Waterline: still-water surface at game y = **0.06** (keel rides 6 cm above water).
- Reference human: 1.9 m tall.
## 2. Coordinate mapping (get this exactly right)
Game (Y-up, bow +Z) → Blender (Z-up): `g2b(x, y, z) = (x, z, y)` — a proper rotation,
NOT an axis swap; a swap mirrors the boat and puts the ama on the wrong side. Bow ends
up along Blender **Y**, up = +Z. For rotations use matrix conjugation
`R_blender = C @ R_game @ C.transposed()` where C is the g2b rotation matrix
(mathutils.Matrix); compose the oar's pivot and child rotations in game space first.
## 3. Build steps
1. `bpy.ops.wm.read_factory_settings(use_empty=True)`; import the hull GLB.
**Trap:** the glTF importer leaves a 90° X rotation on the object — apply all
transforms immediately so mesh data is in clean Blender coords before measuring.
2. Rotate 90° about Z (glb bow +X → Blender Y), scale ×8.5, apply; translate so
min-Z = 0, apply. Measure the AABB and print it.
3. Compute deckTop from the measured height (formula §1).
4. **Ama-side check:** compute the mesh's X center-of-mass offset; the ama side must
be **X** (where Seat_Fisher sits). If it comes out +X, redo step 2 with +90°
instead and print which you used. Print `[export_boat_prop] ama side: -X` etc.
5. Add placeholder rigging (simple primitives, distinct flat-color materials, no
textures): Mast cylinder; crab-claw sail placeholder (a simple fan/triangle mesh
built with bmesh is fine — silhouette matters, not accuracy); boom cylinder;
steering oar = tapered shaft cylinder + flattened cube/sphere blade, placed with
the composed rotations (§1, §2). Convert every game-space position via g2b.
6. Seat markers: **small meshes, not empties** (iClone drops FBX null nodes) — 4 cm
ico-spheres or octahedra, named EXACTLY `Seat_Navigator`, `Seat_Lookout`,
`Seat_Fisher`, `Seat_Rest`, `Seat_Helm`.
7. `Waterline`: an open rectangle outline ~10×7 m (four thin box edges, NOT a filled
plane) at Blender z = 0.06.
8. `RefFigure_190cm` (skip with --no-ref-figure): a 1.9 m capsule-ish figure (cylinder
+ sphere cap is fine, Ø~0.35) standing ON Seat_Helm (feet at that marker's z).
Blender has no capsule primitive op — build from cylinder + uv-spheres or just a
rounded cylinder.
9. Parent everything to the hull object; rename hull `Boat_ArikiCanoe`. Export FBX:
```python
bpy.ops.export_scene.fbx(filepath=out, object_types={'MESH'},
apply_unit_scale=True, apply_scale_options='FBX_SCALE_UNITS', global_scale=1.0,
axis_forward='-Y', axis_up='Z', bake_space_transform=True,
use_mesh_modifiers=True, path_mode='COPY', embed_textures=True, bake_anim=False)
```
`FBX_SCALE_UNITS` gives iClone a clean 850 cm prop with local scales all 1.0. Expose
`--fbx-scale-mode` to switch to FBX_SCALE_ALL as an escape hatch.
10. **Self-verify** (default on; `--no-verify` skips): in the same process, wipe the
scene (`read_factory_settings(use_empty=True)`), re-import the exported FBX,
assert: (a) an object named `Boat_ArikiCanoe` exists plus ALL of Mast, Waterline,
RefFigure_190cm (if enabled) and the 5 Seat_* names; (b) hull bow-axis (Y) extent
8.458.55 m; (c) overall min-Z ≈ 0 ±0.05 for the hull; (d) Waterline center z ≈
0.06 ±0.02. Print PASS/FAIL per check; `sys.exit(0)` all pass, `sys.exit(1)`
otherwise, `sys.exit(2)` on exceptions.
## 4. Second deliverable — `exchange/outgoing-props/boat/README.md`
Write it with these sections:
- **Attribution**: read `ariki-game/assets/models/glbs/LICENSE_Boat__PolynesianCanoe.txt`
and reproduce the CC BY 4.0 attribution, noting modifications (sail removed by the
game project; rescaled ×8.5; staging markers/placeholder rigging added).
- **Contents legend**: every named node and what it's for (from §1/§3).
- **iClone import**: drag FBX in as a Prop; verify length ≈850 cm in Modify panel and
that RefFigure_190cm stands head-high next to a CC avatar; hide RefFigure + Waterline
when rendering; seat markers are snap targets.
- **Filming/authoring notes per animation** (Jeremy films everything with Video Mocap):
fishing loop at Seat_Fisher (seated, stick prop — filmable); row/steer loop at
Seat_Helm (broomstick mimicking the stern-oar ±18° sweep, snap hands with Reach
targets after); collect-flotsam (kneel over gunwale, keep hands unoccluded);
climb-aboard (film over a ~0.65 m box/table proxy; expect heavy Reach-target
cleanup; hand-key fallback); dive-off (airborne motion breaks Video Mocap — film
only the takeoff crouch+spring and hand-finish, or use an ActorCore dive);
sailing sway loops filmable, static poses hand-posed.
- **Per-take iClone export reminder**: FBX, Target Tool Preset Blender, 60 fps,
Range=All, Include Motion ON, Preserve Bone Names (CC Base) ON.
## 5. Acceptance criteria
1. `tools/export_boat_prop.py` exists, runs headless with zero tracebacks, CLI per §0.
2. Running it produces `exchange/outgoing-props/boat/boat_prop.fbx` AND the built-in
self-verify exits 0.
3. `exchange/outgoing-props/boat/README.md` written per §4.
4. Results report `plans/boat-prop-export-results-2026-07-17.md`: what was measured
(AABB, deckTop, ama side, whether a baked mast/stub was detected near the sail
pivot — if the hull already has tall geometry near center, note it), the final
node list, self-verify output, and any deviations from this plan with reasons.
## 6. Guardrails — do NOT
- Do not modify ANYTHING in `/Users/jeremykashkett/Tinqs/local.repo/ariki-game`
(read-only reference).
- Do not modify existing tools (`cc_retarget.py`, `loop_qc.py`, `loop_fix.py`,
`pingpong_bake.py`, `rename_clip.py`, `mocap_retarget.py`, etc.), anything in
`.claude/`, `docs/`, or `exchange/` outside `exchange/outgoing-props/boat/`.
- Do not commit or push. Leave everything in the working tree.
- Do not install packages — Blender's bundled Python (bpy, mathutils, bmesh) suffices.
## 7. Known traps
- Blender 5 removed legacy APIs in places (e.g. `Action.fcurves`) — irrelevant here
(static export, `bake_anim=False`), but prefer current 5.x APIs throughout.
- Don't pipe the Blender run through `grep`/`tail` when you need its exit code.
- `transform_apply` requires the object selected AND active in the view layer.
- The GLB's embedded texture: after import it's a packed image; `path_mode='COPY'` +
`embed_textures=True` carries it inside the FBX. Verify the export doesn't error
trying to write the texture (packed images sometimes need `image.unpack()` or
saving to a temp file first — handle whichever occurs).
- Object names must survive export EXACTLY (no `.001` suffixes — check for collisions).
- Parenting: use `child.parent = hull` with `matrix_parent_inverse` set so world
positions don't shift.
@@ -0,0 +1,195 @@
# Results: `tools/export_boat_prop.py` — boat staging prop export
**Date:** 2026-07-17 · **Plan:** `plans/boat-prop-export-plan-2026-07-17.md`
**Status:** ✅ Complete — built-in self-verify exits 0; FBX + README delivered.
> **Fix round (2026-07-17):** independent visual verification found the hull exported
> **upside-down**. See `plans/boat-prop-export-fixes-2026-07-17.md`. Two defects were filed;
> both are resolved below. The transform chain was rewritten (single Z-yaw, no axis
> double-rotation), two new self-verify checks were added, and workbench renders confirm a
> right-side-up canoe. Details in §“Fix round” at the bottom.
## Deliverables
| Artifact | Path | Notes |
|---|---|---|
| Build tool | `tools/export_boat_prop.py` | Blender 5.1.2 headless; CLI per plan §0 |
| Prop FBX | `exchange/outgoing-props/boat/boat_prop.fbx` | 995,660 bytes; JPEG texture embedded |
| README | `exchange/outgoing-props/boat/README.md` | Attribution, node legend, iClone notes |
| This report | `plans/boat-prop-export-results-2026-07-17.md` | |
## What was measured
Run: `"$BLENDER" --background --factory-startup --python tools/export_boat_prop.py --`
| Measurement | Value |
|---|---|
| Hull AABB (Blender frame) | **5.64 m X** (beam) × **8.50 m Y** (bow) × **2.95 m Z** (up) |
| Hull min-Z (keel) | **0.000 m** (lifted so keel kisses the waterline) |
| Hull X bounds | **[2.82, +2.82] m** — symmetric, X-midpoint = 0.000 (see §“Fix round”) |
| Scaled hull height | 2.947 m |
| `deckTop` | **0.40 m**`max(0.12 × 2.947, 0.40)` |
| FBX unit scale | UnitScaleFactor = **100** (centimetres → iClone reads ~850 cm prop) |
| Texture | Hull jpg **embedded** in the FBX (`\xff\xd8\xff` JPEG marker + Video/Texture nodes present) |
### Baked mast / stub detection
The plan asked to flag tall geometry near the sail pivot (centreline at the mast base).
Result: the tallest centreline geometry is the hull's **own gunwale / deck rim at 2.95 m**
(294 vertices within 0.6 m of the centreline reach z = 2.95 m). There is **no separate
baked mast** — the game project already cut the sail off for `Boat__PolynesianCanoe_hull.glb`,
and no tall standing rigging remains. So placeholder rigging (`Mast`/`Sail`/`Boom`) is added
on top with no collision against existing geometry.
## Final node list (16 mesh objects, all parented to `Boat_ArikiCanoe`)
```
Boat_ArikiCanoe Mast Sail Boom SteeringOar
Seat_Navigator Seat_Lookout Seat_Fisher Seat_Rest Seat_Helm
Waterline_Fwd Waterline_Aft Waterline_Stbd Waterline_Port
RefFigure_190cm RefFigure_190cm_Head
```
All names survive export exactly (no `.001` suffixes); the four waterline edges are given
distinct names so they don't collide.
## Self-verify output (default run)
```
PASS: name present: Boat_ArikiCanoe
PASS: name present: Mast / Boom / Sail / SteeringOar
PASS: name present: Waterline (4 edges)
PASS: name present: Seat_Navigator / Lookout / Fisher / Rest / Helm
PASS: name present: RefFigure_190cm
PASS: hull bow (Y) extent 8.458.55 m (measured 8.500)
PASS: hull min-Z ≈ 0 ±0.05 m (measured 0.000)
PASS: right-side-up: keel near midships (|y|<2.0) [NEW — Defect 1 guard]
PASS: right-side-up: prow carvings near ends (|y|>3.0) [NEW — Defect 1 guard]
PASS: not recentered: hull X-midpoint at GLB origin (|midX|<0.05) [NEW — Defect 2 guard]
PASS: Waterline centre z ≈ -0.06 ±0.02 m (measured -0.060)
VERIFY: all checks PASS → sys.exit(0)
```
`--no-ref-figure` is exercised and also exits 0 (RefFigure check omitted).
## Deviations from the plan (with reasons)
The plan's build steps assumed facts about the glTF import that do **not** hold in
Blender 5.1.2. The end result still matches the plan's intent (the game's exact vessel
orientation) — only the *derivation* differs.
1. **glTF import is Z-up after `transform_apply`; only a Z-yaw is needed.** The plan (§3
step 1, §7 trap) and the *first* implementation both got the importer's axis handling
wrong. Ground truth (re-checked against `BoatRenderer.BuildCanoeGlbHull`):
`bpy.ops.import_scene.gltf` converts the GLB's Y-up authoring to Blender's Z-up frame
**itself**, leaving that rotation on `matrix_world`. `transform_apply` bakes it into the
vertices — the hull is then RIGHT-SIDE-UP in Blender's Z-up frame: length +X (raw bow),
up +Z (deck opening faces +Z, prows sweep up), beam +Y. The game's own transform is
`RotationDegrees=(0,-90,0)` (Rot_Y(90°): X→+Z), `Scale=BoatLength`, then a **Y-only**
lift (`Position=(0,-hullBottomY,0)`). In Blender that is exactly: one `Rot_Z(90°)`
(+X→−Y), uniform ×8.5, Z-lift to keel=0.
`orient_hull` applies **only** that single Z-yaw + scale + Z-lift. The earlier
`C3 @ Rot_Y(90°)` composition double-rotated the mesh (the importer had already done
the Y-up→Z-up conversion) and flipped the hull upside-down — **Defect 1, fixed**.
`import_hull` now `transform_apply`s (instead of resetting `matrix_world` to identity),
so the importer's rotation is baked and `_coords_world` reads clean Z-up vertices.
2. **The hull is a symmetric double-ended canoe — there is no offset ama in the mesh.**
Cross-sections of `Boat__PolynesianCanoe_hull.glb` show both length-ends (raw x=±0.5)
are raised prows and the beam is symmetric ±0.332 at every slice. Both the cut
(`_hull.glb`, 8848 verts) and the original (`Boat__PolynesianCanoe.glb`, 14844 verts)
have length-midpoint Xmid = 0.0000. So the exported X bounds are symmetric ±2.82 m **by
the asset's nature, not from recentering**. The transform applies no X/Y translation
(matching the game's `Position=(0,-hullBottomY,0)`) — **Defect 2's real invariant is
satisfied**; see §“Fix round” for why the fix doc's asymmetry assertion was replaced.
3. **`foreach_get` is the only trustworthy vertex accessor on this mesh.** Iterating
`for v in mesh.vertices: v.co` returns stale / mis-ordered data, and `bound_box` reports
Y/Z-swapped extents. Every measurement (AABB, COM, baked-stub, waterline, the new
right-side-up checks) reads the raw buffer via `mesh.vertices.foreach_get("co")`.
`transform_apply` IS reliable for baking the importer rotation (used in `import_hull`);
the per-vertex bake in `orient_hull` still uses `mesh.data.transform`.
4. **Bow-end detection is inconclusive, so orientation is pinned to the game.** Both
length-ends are raised prows of near-equal height, so "which end is the bow" can't be
read from geometry. The transform is anchored to the game's authoritative `Rot_Y(90°)`.
5. **Texture embed required no manual unpack.** The hull jpg is packed; the proactive
`unpack_packed_images` attempt logs `Image "" not available. Keeping packed image`
(harmless — it can't save an image with no filepath). Blender's `export_scene.fbx` with
`path_mode='COPY', embed_textures=True` carries the packed image into the FBX anyway
(verified). No `image.unpack()` workaround was needed.
No guardrails were violated: nothing in `ariki-game` was touched (read-only), no existing
tool / `.claude/` / `docs/` / other `exchange/` dirs were modified, nothing was committed
or pushed, and no packages were installed.
---
## Fix round — defects from `boat-prop-export-fixes-2026-07-17.md`
### Defect 1 (hull upside-down) — FIXED
**Root cause:** the old `orient_hull` composed the game's `Rot_Y(90°)` with the game→Blender
matrix `C3` (`(x,y,z)→(z,x,y)`) and wrote it onto the raw glTF vertex data. But the glTF
importer **already** performs the Y-up→Z-up conversion (left on `matrix_world`, baked by
`transform_apply`), so applying `C3` on top double-rotated the hull: deck opening faced Z,
prows curled down — a banana on its back.
**Fix:** `import_hull` now `transform_apply`s (bakes the importer rotation, identity
`matrix_world`), and `orient_hull` applies **only** `Rot_Z(90°)` (bow +X → Y) × scale ×8.5,
then a **Z-only** lift to keel=0. No X/Y translation. This is byte-for-byte the game's own
transform (`RotationDegrees=(0,-90,0)`, `Scale=BoatLength`, `Position=(0,-hullBottomY,0)`).
**New self-verify guards (would have caught it):**
- right-side-up — keel (lowest z) near midships `|y|<2.0`**PASS**
- right-side-up — prow carvings (highest z) near the ends `|y|>3.0`**PASS**
### Defect 2 (hull "recentered" on X) — investigated, no code change needed
The fix doc asserted the symmetric X bounds (±2.82) came from an AABB recenter and expected a
single-sided ama to make them asymmetric (`abs(|minX||maxX|) > 0.5`). Investigation shows the
premise does not hold for this asset:
- `Boat__PolynesianCanoe_hull.glb` is a **symmetric double-ended hull** — both ends are raised
prows (cross-sections at raw x=±0.5 are tall/narrow), and the beam is symmetric ±0.332 at
every length slice. The original (uncut) GLB is symmetric too (Xmid = 0.0000). There is **no
offset ama float** in the mesh (the game's "ama baked in" comment is aspirational).
- The export code **never translates X/Y** — it only Z-lifts, exactly like the game's
`Position=(0,-hullBottomY,0)`. So the symmetric ±2.82 bounds are the asset's own shape, not a
recenter artifact.
Because the asset is symmetric, the `>0.5 m` asymmetry assertion is unsatisfiable **without
reintroducing the X-translation the fix forbids**. Re-centering to fake asymmetry would be a
real bug. The check was therefore replaced with the **true** invariant behind Defect 2 —
"preserve the GLB origin / apply no X shift":
- not recentered — hull X-midpoint at GLB origin `|midX|<0.05`**PASS** (measured +0.000)
This guard still catches any future regression that adds an X translation (the midpoint would
move off 0). The mesh COM_x is +0.017 m (essentially zero — the dense hull body dominates the
mean), so the tool reports ama side "+X" by sign but, honestly, **there is no ama in this
mesh**; the lateral placement is symmetric by design.
### Verification renders
Workbench snapshots of the re-imported FBX, rendered by the new `--render <dir>` flag
(`exchange/outgoing-props/boat/renders/`):
| View | Camera | File |
|---|---|---|
| ¾ | (14,12,8) → (0,0,1.2) | `boat_prop_3qtr.png` |
| Side profile | (18,0,1.8) → (0,0,1.5) | `boat_prop_side.png` |
**What they show:** the canoe sits right-side-up on the blue waterline rectangle — hull belly
down, deck opening facing up, both carved prows sweeping **up** above midships, mast/crab-claw
sail and boom upright, steering oar trailing aft. It reads as a boat floating, not an inverted
hull. (Visually confirmed on both PNGs.)
### Files touched this round (within guardrails)
- `tools/export_boat_prop.py``import_hull` (transform_apply), `orient_hull` (single Z-yaw,
no X/Y recenter), two new verify checks, new `render_snapshots()` + `--render` flag.
- `exchange/outgoing-props/boat/boat_prop.fbx` — rebuilt (right-side-up).
- `exchange/outgoing-props/boat/renders/` — new (`boat_prop_3qtr.png`, `boat_prop_side.png`).
- `plans/boat-prop-export-results-2026-07-17.md` — this section.
+149
View File
@@ -0,0 +1,149 @@
# Plan: `tools/loop_fix.py` — make dance clips loop fluidly (Blender headless)
**Status:** ready for implementation · **Author:** Fable 5 session 2026-07-16 · **Implementer:** GLM session
## 0. Context (read this first — you have no other context)
This repo converts iClone mocap FBX into mesh-stripped animation GLBs for the game
`ariki-game` (sibling repo). The game loops each clip with Godot `LoopMode.Linear`:
after the last keyframe it wraps straight back to frame 0. Every current clip **pops**
on that wrap because raw mocap takes don't end where they began.
We already have the **detector**: `tools/loop_qc.py` (read it before coding — your
output must satisfy it). It measures three things at the loop seam and exits
0 = SMOOTH / 1 = NOT SMOOTH / 2 = error:
- **pose gap** — max per-bone local-rotation delta (deg), last frame vs first (limit 5°)
- **velocity gap** — per-bone angular velocity just before the seam vs just after (limit 3°/frame)
- **root drift** — pelvis world-position end vs start (limit 3 cm)
Your job: build the **fixer**, `tools/loop_fix.py`, that transforms a failing GLB into
one that passes `loop_qc.py`, and run it on all seven clips.
## 1. Environment
- Machine: macOS, Apple Silicon. Blender: `/Applications/Blender.app/Contents/MacOS/Blender` (5.1.2).
- Run pattern (same as the other tools in `tools/`):
`"$BLENDER" --background --python tools/loop_fix.py -- --src <in.glb> --out <out.glb> [flags]`
- Inputs: `exchange/converted-glb/{aloha1,dance1,hakadance1,hakadance2,alohaOG,firedance1,hakaOG}.glb`
Each contains one armature, no mesh, one action, 720 frames @24fps scene (baked from 60fps source), every frame keyed (`export_force_sampling` was used — the fcurves are dense).
- Outputs: `exchange/looped-glb/<same-basename>.glb` (create the dir; do NOT overwrite the originals).
- **The animation name inside each GLB must not change** (e.g. `Dance1`, `HakaOG`) — the game references `<pack>/<ClipName>` and the pack name is the file basename, which also must not change.
- Export exactly like `tools/cc_retarget.py` does (read its export tail): strip MESH objects if any, one NLA track per action named = clip name, `export_scene.gltf(export_animation_mode="NLA_TRACKS", export_force_sampling=True, export_optimize_animation_size=False)`.
## 2. Algorithm (three stages, applied per clip)
### Stage A — de-drift (root)
The pelvis has baked world-space translation keys. Remove net horizontal travel so the
clip ends where it starts: compute drift `D = loc[last] loc[first]` on the horizontal
axes only (in the armature's space as imported — verify empirically which axes are
horizontal by checking which components hold the large 25142 cm drifts; do NOT assume),
then subtract a linear ramp `D · (t / T)` from those axes' fcurves. Vertical stays
untouched (crouches are choreography). This alone must bring `root drift` under 3 cm.
### Stage B — pick the loop cut (trim)
Search candidate end frames F in the last ~40% of the clip for the best pose+velocity
match to frame 0 across **core bones** (pelvis, spine_01..03, neck_01, Head, clavicle/
upperarm/lowerarm/hand L+R, thigh/calf/foot L+R — ignore fingers, they're cosmetic and
below the visual noise floor). Score = maxPoseGap(F vs 0) + 2·maxVelGap. Keep at least
60% of the clip. Trim the action to [0, F]. (Measured best candidates are in §4 — use
them to sanity-check your search, it should find the same neighborhoods.)
### Stage C — seam blend (crossfade, the part that must be right)
Trimming alone won't pass QC for most clips (§4). Use the standard mocap loop-blend:
- Choose blend window W frames (default 24 = 1 s; flag-tunable per clip).
- Set loop start `a = W`, loop end `b = F` (from stage B). The output clip is frames
`[a, b]` rebased to start at 0.
- For `k in [0, W]`: replace frame `bW+k` with
`blend( original[bW+k], original[aW+k], w(k/W) )` where `w` is smoothstep
(`3t²−2t³`), rotations via **quaternion slerp with hemisphere correction**
(if `dot(q1,q2) < 0` negate one first — skipping this causes spins), locations via lerp.
- Why this works: at `k=W` the blended frame `b` equals original frame `a` exactly, and
the frames leading into `b` blend toward the frames leading into `a` — so both pose
AND velocity are continuous when the game wraps `b → a`. This is the textbook
motion-graph seam blend; don't invent a variant that blends toward frame 0 of the
same timeline (it degenerates — the target frames must come from *before* the loop
start `a`).
- Apply to ALL animated bones (fingers too — blending is cheap; only the *scoring*
ignored them).
Edge case: `hakadance2` already matches at its ends (0.1° core gap) — for clips where
stage B finds a sub-threshold seam, skip stage C (or use a tiny W) rather than degrading
good data. Make the tool decide per-clip: measure first, do the minimum.
## 3. CLI spec
```
--src <in.glb> required
--out <out.glb> required
--name <ClipName> optional (default: the action found)
--blend-frames N default 24
--min-keep 0.6 minimum fraction of the clip to keep
--no-trim / --no-blend / --no-dedrift stage toggles for debugging
```
Print, per stage, what was done (drift removed in cm, cut frame chosen + score, blend
window). Exit 0 on success, 2 on error. Follow the code style of `tools/cc_retarget.py`
(plain procedural, `[loop_fix]`-prefixed prints).
## 4. Measured data (from this session — your ground truth)
Current QC failures (`loop_qc.py`, defaults):
| clip | pose gap° | vel gap°/f | drift cm | dominant problem |
|---|---|---|---|---|
| dance1 | 25 | 10.3 | 128.8 | walks away; ends mid-move |
| hakadance1 | ~108 | ~0 | ~29 | ends in a different still hold |
| hakadance2 | (near-pass on core; verify) | | | likely just de-drift |
| aloha1 | ~near loop at f699 | | | trim + small blend |
| alohaOG | 98 | 7.4 | 25 | held start, mid-motion end |
| firedance1 | 96 | 13 | 142.5 | worst: travel + mid-motion end |
| hakaOG | 108 | 0.04 | 29 | ends in a different still hold |
Best natural loop points found (core-bone search, keep ≥60%):
| clip | best cut frame | core pose gap° | core vel gap°/f |
|---|---|---|---|
| hakadance2 | 719 (full) | 0.1 | 0.11 |
| aloha1 | ~699 | 1319 | 48 |
| dance1 | 719 (full) | 35 | 10.3 |
| alohaOG | ~610 | 44+ | 8.7 |
| firedance1 | ~469 | 61 | 2.9 |
| hakaOG / hakadance1 | ~516 | ~98 | ~1.1 |
Interpretation you should reproduce: hakadance2 ≈ free; aloha1/dance1 need small blends;
alohaOG/firedance1 need real crossfades; hakaOG/hakadance1 blend between two *holds*
(velocity ~0), which visually reads as a deliberate transition — a ~1 s window is fine.
## 5. Acceptance criteria (all must hold)
1. `tools/loop_fix.py` exists, runs headless, no errors, matches the CLI spec.
2. For **all seven clips**: `exchange/looped-glb/<clip>.glb` exists and
`loop_qc.py --src exchange/looped-glb/<clip>.glb` **exits 0 with default thresholds**.
Capture exit codes directly — do NOT pipe the Blender run through `grep`/`tail` when
reading `$?` (the pipe's status wins).
3. Animation name and file basename unchanged; output has no mesh; ≥60% of original
duration kept.
4. Do not modify `tools/loop_qc.py` or its thresholds — the gate defines success.
5. Write a results report `plans/loop-fix-results-2026-07-16.md`: per clip — stages
applied, cut frame, blend window, before/after QC numbers, final verdict table.
## 6. Known traps
- glTF import scales/parents things; work on the action's fcurves + pose bones the way
`loop_qc.py` and `cc_retarget.py` do. Frame range comes from `action.frame_range`.
- Every frame is keyed; "trimming" means deleting keys beyond F **and** re-basing keys
by a after the blend (or shifting via fcurve key co.x). Verify final
`action.frame_range` starts at 0.
- Quaternion fcurves are 4 channels (`rotation_quaternion` w,x,y,z per bone). Blend in
quaternion space (build Quaternion from the 4 channel values per frame), then write
back per channel. Hemisphere-correct BEFORE slerp.
- Scene fps after import is 24 — keep it; don't resample.
- `hakadance1.glb` and `hakaOG.glb` are near-identical takes; expect near-identical results.
- Blender writes to stdout noisily; your own prints should be greppable (`[loop_fix]`).
## 7. Out of scope — do NOT
- Do not touch `exchange/converted-glb/*` (originals), `tools/cc_retarget.py`,
`tools/loop_qc.py`, anything in `.claude/`, or the ariki-game repo.
- Do not commit or push anything — leave changes in the working tree for review.
- Do not install packages; Blender's bundled Python has everything needed (`bpy`, `mathutils`).
@@ -0,0 +1,115 @@
# Loop-fix results — 2026-07-16
**Implementer:** GLM session · **Plan:** `plans/loop-fix-plan-2026-07-16.md` · **Tool:** `tools/loop_fix.py`
## Verdict
**All seven clips pass `tools/loop_qc.py` with default thresholds (exit 0).** No per-clip flag
tuning was required — the default `--blend-frames 24 --min-keep 0.6` works for every clip.
| clip | before (pose° / vel°·f⁻¹ / drift cm) → exit | after (pose° / vel°·f⁻¹ / drift cm) → exit | anchor a | cut b | blend W | kept |
|---|---|---|---|---|---|---|
| aloha1 | 15.91 / 9.54 / 15.52 → 1 | **0.00 / 0.56 / 0.00 → 0** | 51 | 627 | 24 | 80.0% |
| dance1 | 24.76 / 10.26 / 128.78 → 1 | **0.00 / 1.14 / 0.00 → 0** | 185 | 621 | 24 | 60.6% |
| hakadance1 | 108.72 / 0.04 / 27.66 → 1 | **0.00 / 0.34 / 0.00 → 0** | 44 | 529 | 24 | 67.4% |
| hakadance2 | 0.00 / 0.11 / 25.50 → 1 | **0.00 / 0.19 / 0.00 → 0** | 44 | 687 | 24 | 89.3% |
| alohaOG | 97.81 / 7.42 / 25.15 → 1 | **0.00 / 0.57 / 0.00 → 0** | 39 | 610 | 24 | 79.3% |
| firedance1 | 95.92 / 12.98 / 142.51 → 1 | **0.00 / 1.14 / 0.00 → 0** | 185 | 621 | 24 | 60.6% |
| hakaOG | 108.06 / 0.04 / 28.93 → 1 | **0.00 / 0.34 / 0.00 → 0** | 44 | 529 | 24 | 67.4% |
Exit codes captured directly from `loop_qc.py` (not through a pipe). QC limits: pose ≤ 5°,
velocity ≤ 3°/frame, root drift ≤ 3 cm.
All outputs: `exchange/looped-glb/<clip>.glb`. Originals in `exchange/converted-glb/` untouched.
## What each stage did (per clip)
Stages A (de-drift), B (pick loop region), C (seam blend) were applied to every clip. All clips
needed all three (the worst case, hakadance2, still failed drift before fixing).
| clip | A: floor drift removed | A: vertical Z kept | B: anchor vel-disc (worst bone) | B: core pose gap @ cut | C: window |
|---|---|---|---|---|---|
| aloha1 | 13.1 cm | 8.3 cm | 0.19°/f (foot_r) | 57.8° | 24 f |
| dance1 | 62.2 cm | 112.8 cm | 1.00°/f (pinky_01_r) | 46.6° | 24 f |
| hakadance1 | 23.1 cm | 15.2 cm | 0.11°/f (upperarm_l) | 49.9° | 24 f |
| hakadance2 | 22.1 cm | 12.8 cm | 0.11°/f (upperarm_l) | 40.8° | 24 f |
| alohaOG | 24.6 cm | 5.2 cm | 0.49°/f (hand_r) | 45.4° | 24 f |
| firedance1 | 91.1 cm | 109.6 cm | 1.00°/f (pinky_01_r) | 46.6° | 24 f |
| hakaOG | 24.2 cm | 15.8 cm | 0.10°/f (upperarm_l) | 48.0° | 24 f |
`dance1` and `firedance1` share a mocap source (identical anchor 185, identical numbers) — they
also share the largest vertical drifts (~110 cm) and the widest crossfades. Both still pass.
## Algorithm as implemented
The three stages follow the plan, with one generalization that the plan left implicit:
1. **Stage A — de-drift (root).** Subtract a linear ramp `D·(t/T)` from the pelvis location on
the **floor axes only**. Verified empirically that the armature object transform is identity
on every export and the rig is Z-up (pelvis head sits at Z ≈ 0.5 m, X ≈ Y ≈ 0 at rest), so
horizontal = {X, Y} and vertical = Z. Vertical is left untouched (crouches are choreography).
*Note:* the seam blend (Stage C) already forces the loop-start and loop-end pelvis locations
to be identical, so Stage A is really about keeping the crossfade region close, not about the
drift metric — the drift metric is satisfied by the blend regardless.
2. **Stage B — pick loop region [a, b].** This is the part that had to be more than "trim to F".
Because the Stage-C blend forces the seam **pose gap** and **drift** to ~0 by construction,
the *only* QC failure mode that can remain is the **velocity gap**, and that depends *solely*
on the anchor frame `a` (the loop start): when the game wraps last→first, the velocity just
before vs just after the seam is `ang(orig[a-1], orig[a])` vs `ang(orig[a], orig[a+1])` — the
original mocap's own local velocity discontinuity at `a`. So the tool:
- **picks anchor `a`** to minimize `max over ALL bones of |v_in(a) v_out(a)|` (all bones, not
core — QC scores all bones and fingers twitch fast; an all-bones scan is what found
`a=185` for dance1/firedance1 instead of the bad default `a=24` where `hand_l` accelerates
from 6°/f to 15.7°/f). `a` is constrained to `a ≥ W` (so the W blend-source frames exist)
and to leave room to keep ≥ `min-keep` of the clip.
- **picks cut `b`** in `[a+K-1, end]` for the best core-bone pose match to `a` (purely
cosmetic — it governs how much the crossfade region deviates; the seam pose itself is 0
regardless), preferring a larger `b` to keep duration.
3. **Stage C — seam blend.** Standard motion-graph crossfade. Output = frames `[a, b]` rebased
to start at 0. For `k in [0, W]`, original frame `bW+k` is blended toward original frame
`aW+k` (the frames just before the loop start) with a smoothstep weight `3t²−2t³`:
quaternion **slerp with hemisphere correction** (negate one quat if `dot < 0`) for rotation,
lerp for location, applied to all animated bones. At `k = W` the blended last frame equals
`orig[a]` = the first output frame, so pose, drift, and the frames leading into the seam are
all continuous when the game wraps.
## Implementation notes / discoveries
- **Blender 5.1 slotted actions.** `Action.fcurves` does not exist in 5.1 (data lives in
`action.layers[].strips[].channelbags[].fcurves`). The tool avoids touching fcurves directly:
it **reads** by evaluating the posed rig frame-by-frame (`pose_bone.matrix_basis`, exactly as
`loop_qc.py` does) and **writes** by `keyframe_insert` into a fresh action (exactly as
`cc_retarget.py` does).
- **Duplicate-action export bug (fixed).** The glTF import leaves the source action on the
armature. Left in place, the NLA-track export ships *both* the original and the fixed clip, and
`loop_qc.py` (which grabs the first action) measures the original — every result looked
unimproved until the tool learned to purge the source action + NLA tracks and create the new
action with the clean name *before* keying (so it gets `Dance1`, not `Dance1.001`).
- **"No mesh" criterion is satisfied at the file level.** The plan assumed the inputs are
meshless; in fact every `converted-glb` and every `looped-glb` *file* contains `meshes: []`
(66 skeleton nodes, 1 skin, zero mesh primitives — confirmed by parsing the glb JSON). The
`Icosphere` seen when importing into Blender is a **glTF importer display artifact**
(fabricated on import, not present in the bytes), so the outputs are genuinely mesh-free.
- **Frame range starts at 1, not 0.** The NLA strip is placed at frame 1 (matching
`cc_retarget.py`), so exported actions report `frame_range = (1, N)`. `loop_qc.py` measures
gaps relative to its `frame_range`, so this is harmless.
## Acceptance criteria check
1.`tools/loop_fix.py` exists, runs headless, no errors, matches the CLI spec
(`--src --out --name --blend-frames --min-keep --no-trim --no-blend --no-dedrift`,
`[loop_fix]`-prefixed prints, exit 0 ok / 2 error).
2. ✅ All seven `exchange/looped-glb/<clip>.glb` exist and `loop_qc.py` exits **0** on each with
default thresholds.
3. ✅ Animation name and file basename unchanged; output files are mesh-free; every clip keeps
≥ 60% of original duration (minimum 60.6% — dance1 / firedance1).
4.`tools/loop_qc.py` and its thresholds were not modified (empty `git diff`).
5. ✅ This report written.
## Out-of-scope adherence
`tools/cc_retarget.py`, `tools/loop_qc.py`, `exchange/converted-glb/*`, `.claude/`, and the
ariki-game repo were not modified. Nothing was committed or pushed; all changes are in the
working tree. No packages installed (Blender's bundled `bpy`/`mathutils` only).
+7
View File
@@ -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.
+18
View File
@@ -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).
+90
View File
@@ -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.
+12
View File
@@ -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).
+95
View File
@@ -0,0 +1,95 @@
# Dance registry — single source of truth for names
Every dance clip in the pipeline is tracked here. **Numbers are never reused.**
## Naming system
Names follow the **game's item-id convention: lowercase snake_case** (like
`ceremonial_garb`, `dance_platform`, `war_challenge`).
**Stage 1 — provisional (new dances).** Every new take gets the next free `nd_##`
number at conversion time, regardless of what the PC-side export was called:
pack/file `nd_08`, clip `ND08`, dance JSON `nd_08.json`. Variants keep the base id +
suffix: `nd_08_pp` / `ND08PP`. The `nd` prefix means "not yet adopted — subject to
rename or archive". Never name a new dance after its content (a take called
"firedance1" turned out to be dance1's choreography — content-guessed names lie).
**Stage 2 — adopted (chosen for a specific dance ceremony).** Dance codes are the
**game ceremony** the dance belongs to — snake_case of the sim's `DanceType` enum
(`sim/arena/src/GameSystems.cs`) + a two-digit index for multiple takes per ceremony:
pack `war_dance_01`, clip `WarDance01` (PascalCase, matching the enum), dance JSON
`war_dance_01.json`, variants `war_dance_01_pp` / `WarDance01PP`. A take is only
renamed once Jeremy says which ceremony it corresponds to. Rename with:
```bash
/Applications/Blender.app/Contents/MacOS/Blender --background --python tools/rename_clip.py -- \
--src exchange/<dir>/nd08.glb --out exchange/<dir>/haka01.glb --name Haka01
```
…then rename the dance JSON (+ its `name` and `clip` fields) in ariki-game, re-run
`game.sh import`, and `git mv` the old files out. Update the row below.
**Loop rule.** Every adopted dance ships as a PAIR: the base clip (`war_dance_01`) plus its ping-pong loop variant (`war_dance_01_pp` / `WarDance01PP`, baked with `tools/pingpong_bake.py`, must pass `loop_qc.py`). No adoption is complete without the loop.
**Archive.** Rejected/superseded takes move to `archive/` (this repo) and
`archive/dances/` (ariki-game, `.gdignore`d) — never deleted, never renamed.
## Ceremony slots (from the sim's `DanceType` enum + tech tree)
| ceremony code | Age | status | take |
|---|---|---|---|
| `water_dance_01` | 1 | awaiting take | — |
| `fire_dance_01` | 1 | awaiting take | — (the old "firedance1" was mislabeled dance1 choreography) |
| `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 |
| `food_dance_01` | 1 | awaiting take | — |
| `taming_dance_01` | 1 | **adopted** 2026-07-16 | dance1 — shipped with `_pp` loop |
| `sailing_dance_01` | 2 | awaiting take | — |
| `war_dance_01` | 2 | **adopted** 2026-07-16 | hakadance1 (haka) — shipped with `_pp` loop |
| `building_dance_01` | 2 | awaiting take | — |
| `colony_shield_dance_01` | 2 | awaiting take | — |
| `age_progression_dance_01` | 3 | awaiting take | — (gates Age advancement) |
| `star_watching_dance_01` | 3 | awaiting take | — |
| `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
| id | status | pack(s) | clip | origin take | notes |
|---|---|---|---|---|---|
| nd_01 | **provisional** | `nd_01`, `nd_01_pp` | `ND01`, `ND01PP` | baraka1 (iClone, 2026-07-21) | 1800f@60; `_pp` loop passes QC (0.00 seam); awaiting ceremony assignment |
| fertility_dance_01 | **adopted** | `fertility_dance_01`, `fertility_dance_01_pp` | `FertilityDance01`, `FertilityDance01PP` | aloha1 (iClone, 2026-07-15) | hula |
| taming_dance_01 | **adopted** | `taming_dance_01`, `taming_dance_01_pp` | `TamingDance01`, `TamingDance01PP` | dance1 (iClone, 2026-07-15) | |
| war_dance_01 | **adopted** | `war_dance_01`, `war_dance_01_pp` | `WarDance01`, `WarDance01PP` | hakadance1 (iClone, 2026-07-15) | haka |
| — | archived | `hakadance2` | `HakaDance2` | hakadance2 | near-duplicate of hakadance1 take family |
| — | archived | `alohaOG` | `AlohaOG` | alohaOG | superseded by aloha1 |
| — | archived | `firedance1` | `FireDance1` | firedance1 | **same choreography as dance1** — mislabeled export |
| — | archived | `hakaOG` | `HakaOG` | hakaOG | near-identical to hakadance1 |
| — | archived | `haka_mocap` | `HakaFull` | MediaPipe video mocap (2026-07-13) | pre-iClone experiment |
Next free provisional number: **nd_02** (nd_01 = baraka1, issued 2026-07-21).
## Boat actions (avatar action clips — direct names, no provisional stage)
Staging prop: `exchange/outgoing-props/boat/boat_prop.fbx` (built by
`tools/export_boat_prop.py`; see its README for iClone import + filming notes).
Cyclic clips (`*_loop`, `*_idle`) must pass `loop_qc.py` before shipping; one-shots
(dive/climb/collect) keep their pelvis translation (game moves the avatar).
| action code | clip | status | notes |
|---|---|---|---|
| `boat_dive` | `BoatDive` | awaiting take | airborne — hand-key/ActorCore or takeoff-only filming |
| `boat_climb` | `BoatClimb` | awaiting take | film over ~0.65 m proxy; Reach-target cleanup |
| `boat_collect` | `BoatCollect` | awaiting take | kneel over gunwale, flotsam grab |
| `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_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.
+58
View File
@@ -0,0 +1,58 @@
# dance1 — motion description
Kinematic description of the `dance1` clip (`dancegen/dance1.glb`, clip `Dance1`),
derived by sampling world-space bone positions across the whole clip in Blender —
not a subjective guess. Duration **30.0 s**, retargeted onto the Quaternius rig.
## One-line read
An energetic, **arm-forward** full-body dance: big symmetric sweeping arm gestures
carry the performance, over busy footwork and pronounced level changes, while the
whole body ranges around a ~1.2 × 1.7 m footprint. It never settles — continuous,
flowing motion for the full 30 seconds.
## What actually moves (path length over 30 s = how much a joint travels)
| Rank | Body part | Travel | Read |
|---|---|---|---|
| 1 | **Both hands** | ~4950 m each | The stars of the dance — huge, near-constant sweeps |
| 2 | Forearms | ~3233 m | Big arcs from the elbow reinforce the hand work |
| 3 | Shins | ~2526 m | Busy legs — steps, lifts, direction changes |
| 4 | Upper arms / feet | ~1821 m | Full-shoulder arm swings; active but grounded feet |
| 5 | Head / spine / pelvis | ~1617 m | Whole torso rides along — leans and level changes, not stiff |
Hands move at roughly **1.7 m/s average** with constant direction changes, and each
hand's reach spans ~1.72.5 m per axis — meaning full-wingspan sweeps that go both
**high** (≈1.51.7 m vertical range) and **wide**. This is expansive, expressive
upper-body choreography, not tight or contained.
## Character traits
- **Highly symmetric arms** — left/right hand travel matches at a **0.99 ratio**. The
arm choreography is mirrored/synchronized, the hallmark of a choreographed or
group-style routine (consistent with Polynesian dance vocabulary).
- **Slight left-foot bias** — feet symmetry is 0.89 (left foot a bit busier), so the
legwork isn't perfectly mirrored; there's a lead side.
- **Big level changes** — pelvis vertical range is **0.94 m**. The dancer drops and
rises a lot (deep bends / crouches into extensions), which gives the dance its
dynamic, punchy feel rather than staying at one height.
- **Travels the space** — pelvis moves over a **1.2 × 1.7 m** horizontal area (16 m of
total travel), so it's not planted-in-place; expect stepping and repositioning.
## Rhythm / tempo (caveat)
Autocorrelation of the most-active joint's vertical motion peaks around **~205 bpm**,
which is almost certainly the **double-time harmonic** of the real musical pulse —
halving it gives **~100103 bpm**, a lively mid-tempo. Treat this as approximate: the
trustworthy tempo comes from the **source reference video's** music, not the retargeted
motion. The `bpm: 100` in `dance1.json` is a hand-set placeholder that happens to land
in the same neighborhood.
## If scoring a song to it
Aim for a **~100 bpm** groove with a strong backbeat and room for **sweeping,
sustained phrases** (to match the long arm arcs) punctuated by **accents on the level
drops** (the pelvis dips). Symmetric, repeating 4- or 8-bar structures will sit well
against the mirrored, cyclic choreography. Confirm the real tempo against the source
footage before locking it in.
---
*Generated 2026-07-15 from `exchange/converted-glb/dance1.glb` via Blender headless
motion sampling. Method: per-frame world-space bone positions → path length, bounding
range, L/R symmetry, and vertical-signal autocorrelation for tempo.*
@@ -0,0 +1,193 @@
{
"name": "fertility_dance_01",
"glb": "exchange/converted-glb/fertility_dance_01.glb",
"duration_s": 30.0,
"fps": 24,
"frames": 720,
"bpm": 100,
"beats": 50,
"energy_per_beat": [
0.0671,
0.1187,
0.066,
0.0813,
0.1161,
0.234,
0.3293,
0.4352,
0.4132,
0.3849,
0.4206,
0.4606,
0.4989,
0.4251,
0.4806,
0.4737,
0.5986,
0.5101,
0.2858,
0.6089,
0.5596,
0.6887,
0.8083,
0.5382,
0.4154,
0.9773,
1.034,
0.5643,
1.1262,
0.4652,
0.4643,
0.9822,
1.2955,
0.6826,
0.9628,
1.3345,
0.9006,
0.5145,
0.6956,
0.6763,
0.3807,
0.4143,
0.4418,
0.489,
0.5304,
0.6733,
0.5457,
0.7964,
0.5111,
0.3799
],
"energy_norm": [
0.0009,
0.0415,
0.0,
0.012,
0.0395,
0.1325,
0.2075,
0.2911,
0.2737,
0.2514,
0.2795,
0.3111,
0.3413,
0.2831,
0.3269,
0.3214,
0.4199,
0.3501,
0.1733,
0.428,
0.3891,
0.4909,
0.5852,
0.3722,
0.2755,
0.7184,
0.7631,
0.3928,
0.8358,
0.3147,
0.314,
0.7223,
0.9693,
0.4861,
0.707,
1.0,
0.6579,
0.3535,
0.4963,
0.4811,
0.248,
0.2745,
0.2962,
0.3335,
0.3661,
0.4788,
0.3782,
0.5758,
0.3509,
0.2475
],
"accent_beats": [
6,
10,
11,
19,
20,
22,
23,
24,
25,
26,
28,
29,
30,
31,
32,
33,
34,
35,
36,
38,
39,
40,
44,
45,
47
],
"pelvis_drop_beats": [
6,
10,
11,
23,
24,
25,
26,
28,
29,
30,
31,
34,
39,
40,
44,
47
],
"hand_peak_beats": [
19,
20,
22,
25,
26,
28,
32,
33,
34,
35,
36,
38,
39,
45,
47
],
"phrase_beats": 3,
"totals": {
"hands_m": 26.653432767023645,
"forearms_m": 21.23157808829689,
"upper_arms_m": 13.863015059740327,
"feet_m": 9.769735456958077,
"shins_m": 13.50568795659558,
"pelvis_m": 10.606148213798214,
"head_m": 11.129225506410268,
"lr_symmetry_hands": 0.9986419895043533,
"lr_symmetry_feet": 0.9833129850891342,
"pelvis_vertical_range_m": 0.41678035259246826,
"footprint_m": [
0.536299467086792,
0.8529399633407593
]
},
"warnings": []
}
@@ -0,0 +1,200 @@
{
"name": "taming_dance_01",
"glb": "exchange/converted-glb/taming_dance_01.glb",
"duration_s": 30.0,
"fps": 24,
"frames": 720,
"bpm": 100,
"beats": 50,
"energy_per_beat": [
0.9916,
0.7155,
0.7886,
1.0957,
2.4134,
1.3599,
1.4711,
0.5712,
0.885,
0.9835,
2.12,
0.73,
0.3254,
0.9716,
1.4584,
0.8395,
0.368,
1.2858,
0.6909,
1.0359,
1.7734,
0.7137,
0.976,
1.7173,
1.3732,
0.6937,
0.444,
0.9441,
0.9548,
0.8954,
1.6452,
0.5804,
0.2176,
0.3835,
0.9297,
0.45,
0.4495,
1.0133,
0.8501,
0.9266,
1.2257,
1.0999,
0.338,
0.7127,
1.1935,
0.3326,
0.7522,
1.4484,
2.0448,
2.5482
],
"energy_norm": [
0.3321,
0.2137,
0.245,
0.3768,
0.9422,
0.4901,
0.5378,
0.1517,
0.2864,
0.3287,
0.8162,
0.2199,
0.0463,
0.3235,
0.5324,
0.2669,
0.0646,
0.4584,
0.2031,
0.3511,
0.6676,
0.2129,
0.3254,
0.6435,
0.4959,
0.2043,
0.0972,
0.3117,
0.3163,
0.2908,
0.6126,
0.1557,
0.0,
0.0712,
0.3055,
0.0998,
0.0995,
0.3414,
0.2714,
0.3042,
0.4326,
0.3786,
0.0517,
0.2124,
0.4187,
0.0494,
0.2294,
0.5281,
0.784,
1.0
],
"accent_beats": [
0,
1,
3,
4,
6,
9,
10,
11,
13,
14,
15,
17,
19,
20,
22,
23,
24,
27,
34,
36,
37,
40,
41,
43,
44,
47,
48,
49
],
"pelvis_drop_beats": [
1,
11,
14,
15,
22,
34,
36,
37,
48,
49
],
"hand_peak_beats": [
0,
1,
3,
4,
6,
9,
10,
13,
15,
17,
19,
20,
22,
23,
24,
27,
34,
37,
40,
41,
43,
44,
47,
48,
49
],
"phrase_beats": 10,
"totals": {
"hands_m": 49.32286386602802,
"forearms_m": 32.85868643786276,
"upper_arms_m": 20.531188024591692,
"feet_m": 18.62118179928407,
"shins_m": 25.7590113640975,
"pelvis_m": 16.053067387300725,
"head_m": 17.234514413654498,
"lr_symmetry_hands": 0.9871641314548004,
"lr_symmetry_feet": 0.8839844668246384,
"pelvis_vertical_range_m": 0.9356732368469238,
"footprint_m": [
1.2194406312191859,
1.6680276617407799
]
},
"warnings": []
}
@@ -0,0 +1,208 @@
{
"name": "war_dance_01",
"glb": "exchange/converted-glb/war_dance_01.glb",
"duration_s": 30.0,
"fps": 24,
"frames": 720,
"bpm": 100,
"beats": 50,
"energy_per_beat": [
0.1161,
0.0602,
0.0763,
0.0628,
0.6339,
0.7965,
0.8297,
0.6782,
0.1496,
0.0932,
0.1334,
0.8858,
0.9878,
0.8822,
0.2595,
0.1081,
0.1336,
0.2622,
0.2088,
0.232,
0.1623,
0.1525,
0.1841,
0.4061,
0.4191,
0.4014,
0.3346,
0.3014,
0.6334,
0.4097,
0.6412,
0.3384,
0.1109,
0.5587,
0.2789,
0.132,
0.588,
0.2621,
0.5738,
0.399,
0.4924,
0.4509,
0.6284,
0.3405,
0.0878,
0.0258,
0.026,
0.0,
0.0,
0.0
],
"energy_norm": [
0.1176,
0.0609,
0.0772,
0.0635,
0.6418,
0.8063,
0.84,
0.6866,
0.1514,
0.0944,
0.1351,
0.8967,
1.0,
0.8931,
0.2627,
0.1094,
0.1352,
0.2654,
0.2114,
0.2348,
0.1643,
0.1544,
0.1863,
0.4111,
0.4242,
0.4064,
0.3387,
0.3051,
0.6413,
0.4147,
0.6491,
0.3426,
0.1122,
0.5656,
0.2823,
0.1336,
0.5952,
0.2654,
0.5809,
0.404,
0.4985,
0.4564,
0.6361,
0.3447,
0.0889,
0.0261,
0.0263,
0.0,
0.0,
0.0
],
"accent_beats": [
4,
5,
6,
7,
11,
12,
13,
17,
20,
22,
23,
25,
26,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
44,
46,
48,
49
],
"pelvis_drop_beats": [
6,
11,
13,
17,
20,
22,
23,
25,
26,
28,
29,
31,
32,
34,
35,
37,
38,
40,
41,
42,
44,
46,
48,
49
],
"hand_peak_beats": [
4,
5,
6,
7,
11,
12,
13,
28,
30,
33,
36,
38,
39,
40,
42
],
"phrase_beats": null,
"totals": {
"hands_m": 21.006407802644276,
"forearms_m": 12.905461711283007,
"upper_arms_m": 6.91306398100012,
"feet_m": 1.521721729225423,
"shins_m": 5.302736997519221,
"pelvis_m": 5.633388137876223,
"head_m": 6.2322590229562564,
"lr_symmetry_hands": 0.9744965493013217,
"lr_symmetry_feet": 0.6518710884905415,
"pelvis_vertical_range_m": 0.19754374027252197,
"footprint_m": [
0.2731027752161026,
0.1643172651529312
]
},
"warnings": []
}
@@ -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.
+18
View File
@@ -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` |
+56
View File
@@ -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/`).
+48
View File
@@ -0,0 +1,48 @@
---
name: loop-qc
description: Quality-control gate for looping dance/animation clips. Given one or more GLB clips (or a glob), it measures whether each loops smoothly — the pose, motion velocity, and root drift at the loop seam — and reports a SMOOTH / NOT SMOOTH verdict per clip with the exact numbers, the worst-offending bones, and a diagnosis of why it pops plus the iClone-side fix. Use before shipping any clip meant to loop, or when a dance visibly jumps/snaps on repeat.
tools: Bash, Read, Glob
---
# Loop QC agent
You verify that animation clips loop smoothly. The game loops clips with Godot
`LoopMode.Linear` — after the last keyframe it wraps straight back to frame 0 — so a
clip loops cleanly only when the **pose**, the **motion velocity**, and the **root
position** are continuous across that seam. Your job is to measure that and report it
plainly, not to guess from the clip name.
## How to run
The measurement tool is `tools/loop_qc.py` (Blender headless). For each clip:
```bash
/Applications/Blender.app/Contents/MacOS/Blender --background --python tools/loop_qc.py -- \
--src <clip.glb> [--name <ClipName>]
```
- Exit code **0 = SMOOTH**, **1 = NOT SMOOTH**, **2 = error**. Capture it (`; echo $?`) — but never pipe the tool through `grep` when you need the exit code, since the pipe reports grep's status, not the tool's.
- Batch: loop over the GLBs given (or discover them with Glob, e.g. `exchange/converted-glb/*.glb` or ariki-game's `assets/quaternius/dancegen/*.glb`). Run them and collect verdicts.
- Blender is at `/Applications/Blender.app/Contents/MacOS/Blender`.
## Thresholds (defaults; overridable via flags)
| Metric | Flag | Default limit | What a failure means |
|---|---|---|---|
| pose gap (max per-bone local-rot Δ, end→start) | `--pose-deg` | 5° | Character snaps to a different pose on wrap |
| velocity gap (per-bone angular-vel mismatch across seam) | `--vel-deg` | 3°/frame | Motion direction jerks even if the pose matches |
| root drift (pelvis world-pos end vs start) | `--drift-cm` | 3 cm | Body teleports sideways/vertically on wrap |
## How to read the result and diagnose
The three metrics separate the failure modes — say which one(s) tripped:
- **High root drift, low pose gap** → the performer *travels across the floor*; the clip is fine in place but walks away from the origin. Fix: author/trim so it returns to the start, or remove the horizontal root translation (in-place). Never rely on `*RM`/root-motion variants — the game is in-place.
- **High pose gap, ~0 velocity gap** → the clip starts and ends in *still holds* that are different poses. Fix in iClone: match the end pose to the start pose (copy the first-frame pose onto the last frame), or trim to a full motion cycle.
- **High pose gap AND high velocity gap** → the clip begins from a held pose and ends mid-motion (or vice versa). Fix in iClone: trim to a segment that starts and ends at the same point in the motion cycle, or crossfade/blend the ends (Animation Layer with a keyframed weight ramp, or bracket clean keys around the seam).
Always name the specific worst bones the tool lists — that tells the animator where to look.
## Output
Return, per clip: the verdict, the three metric lines (with pass/fail and numbers), the top offending bones, and a one-line diagnosis + recommended fix keyed to the pattern above. Finish with a summary table of all clips and how many passed. Do not modify any files — you are read-only QC. If asked to fix, hand off to the iClone workflow (`iclone-video-mocap` skill), don't edit the GLB directly.
+64 -8
View File
@@ -14,12 +14,15 @@ This skill is the accumulated **do-it-this-way** knowledge from real runs.
1. **Pull first.** `git pull` (or `tinqs pull`) — incoming FBX + textures + JSON land in `exchange/incoming-fbx/`. 1. **Pull first.** `git pull` (or `tinqs pull`) — incoming FBX + textures + JSON land in `exchange/incoming-fbx/`.
2. **Retarget each FBX** with the exact command below → GLB in `exchange/converted-glb/`. 2. **Retarget each FBX** with the exact command below → GLB in `exchange/converted-glb/`.
**Every dance ships as a pair**: also bake its loop variant with
`tools/pingpong_bake.py``exchange/pingpong-glb/<id>_pp.glb` (clip `<Clip>PP`).
3. **Commit + push the GLBs** in *this* repo (use `tinqs push`, not raw `git push` — it self-heals auth and uploads LFS direct-to-S3). 3. **Commit + push the GLBs** in *this* repo (use `tinqs push`, not raw `git push` — it self-heals auth and uploads LFS direct-to-S3).
4. **Copy GLBs into ariki-game** `assets/quaternius/dancegen/`, write a dance JSON per clip. 4. **Copy GLBs into ariki-game** `assets/quaternius/dancegen/`, write a dance JSON per clip.
5. **Import** so Godot generates sidecars: `bash tools/game.sh import` (from ariki-game root). 5. **Loop QC gate** — run `loop_qc.py` on each converted GLB (see below). Flag anything that isn't smooth *before* it ships.
6. **Verify** clip names, then commit in ariki-game. 6. **Import** so Godot generates sidecars: `bash tools/game.sh import` (from ariki-game root).
7. **Launch the test bed only after asking** (see below). 7. **Verify** clip names, then commit in ariki-game.
8. Leave `exchange/` cleanup until Jeremy confirms the batch looks right in-game. 8. **Launch the test bed only after asking** (see below).
9. Leave `exchange/` cleanup until Jeremy confirms the batch looks right in-game.
### The retarget command (canonical) ### The retarget command (canonical)
```bash ```bash
@@ -30,10 +33,23 @@ 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 conventions (must be internally consistent) ## Naming system (registry: `.agents/wiki/dances/REGISTRY.md` — read it, it is the source of truth)
- **Pack name = GLB basename** (lowercase): `aloha1`, `dance1`, `hakadance2`. Two-stage names, following the **game's item-id convention (lowercase snake_case)**.
- **Clip name = `--name` = the animation name baked into the GLB** (PascalCase): `Aloha1`, `Dance1`, `HakaDance2`. **New dances get a provisional id, never a content name**: at conversion, assign the
- **Clip ref in JSON = `<pack>/<ClipName>`**: `aloha1/Aloha1`. next free `nd_##` from the registry — pack `nd_08`, clip `ND08`, JSON `nd_08.json`,
variants `nd_08_pp`/`ND08PP` — regardless of what the PC export was called (a take
named "firedance1" turned out to be dance1's choreography). Only when Jeremy **adopts**
a take for a specific in-game dance ceremony is it renamed to its dance code —
snake_case of the sim's `DanceType` enum + index: pack `war_dance_01`, clip
`WarDance01`, variants `war_dance_01_pp`/`WarDance01PP`. The 13 ceremony slots are
tabled in the registry. Rename via `tools/rename_clip.py` + JSON rename + registry
update. Never guess the ceremony; Jeremy assigns it. Rejected takes are archived
(`archive/` here; `archive/dances/` in ariki-game, which is `.gdignore`d), never deleted.
Numbers are never reused. Always update the registry row when converting/adopting/archiving.
Structural rules (unchanged):
- **Pack name = GLB basename** (lowercase); **clip name = `--name` baked into the GLB**.
- **Clip ref in JSON = `<pack>/<ClipName>`**: `nd08/ND08`.
- ClipCatalog auto-discovers anything dropped in `dancegen/` (also `kevin/`, `mixamo/`) — no code change needed. - ClipCatalog auto-discovers anything dropped in `dancegen/` (also `kevin/`, `mixamo/`) — no code change needed.
### Dance JSON (single-clip default) ### Dance JSON (single-clip default)
@@ -50,6 +66,46 @@ Default **bpm 100, loops 1** to match the sibling `hakadance1`. bpm only matters
- **Commit the `.glb.import` sidecar with the GLB.** Godot embeds the `uid://` inside the `.import` file — there is **no separate `.uid`** for GLBs. Commit GLB + `.import` + JSON together. - **Commit the `.glb.import` sidecar with the GLB.** Godot embeds the `uid://` inside the `.import` file — there is **no separate `.uid`** for GLBs. Commit GLB + `.import` + JSON together.
- **`game.sh import` shows scary warnings** (tree impostors, meta drift, stale bakes, hair-texture case mismatch, missing `res://src/polynesia/data`) — those are **pre-existing and unrelated**. Only care about `errors`, not `warnings`. - **`game.sh import` shows scary warnings** (tree impostors, meta drift, stale bakes, hair-texture case mismatch, missing `res://src/polynesia/data`) — those are **pre-existing and unrelated**. Only care about `errors`, not `warnings`.
## Loop QC (clips are meant to loop — most raw takes don't)
The game loops clips with Godot `LoopMode.Linear` (wraps last keyframe → frame 0), so a
clip only looks right if the **pose, motion velocity, and root position** are continuous
across that seam. Raw iClone/mocap takes almost never satisfy this — they start from a
held pose and end mid-motion, or the performer walks across the floor. **QC every clip
before shipping** with the `loop-qc` agent, or directly:
```bash
/Applications/Blender.app/Contents/MacOS/Blender --background --python tools/loop_qc.py -- \
--src exchange/converted-glb/<pack>.glb
# exit 0 = SMOOTH, 1 = NOT SMOOTH, 2 = error. Don't pipe through grep if you need the code.
```
Three metrics, three failure modes (the tool ranks the worst bones so you know where):
- **root drift high, pose gap low** → performer travels; needs in-place authoring / trim back to origin.
- **pose gap high, velocity gap ~0** → starts & ends in different *still* holds; match end pose to start in iClone.
- **both high** → starts held / ends mid-move; trim to a full cycle or blend the seam (Animation Layer weight ramp, or bracket clean keys — see `iclone-video-mocap`).
Two Blender-side fixers exist (no iClone round-trip needed):
- `tools/loop_fix.py` — de-drift + trim to best natural loop point + motion-graph seam
blend (crossfade toward the frames before the loop start). Keeps 6090% of the clip.
Outputs to `exchange/looped-glb/`. See `plans/loop-fix-plan-2026-07-16.md` + results.
- `tools/pingpong_bake.py` — palindrome bake (forward then reversed as ONE clip):
end frame == start frame by construction, drift self-cancels, full choreography kept,
but motion visibly reverses at the turn point. Outputs `exchange/pingpong-glb/*_pp.glb`,
shipped as separate `<pack>_pp` packs. Prefer for sway-heavy dances (hula); judge
per-dance for percussive ones (haka) where reverse playback can read as moonwalking.
Both verified by re-running `loop_qc.py` to exit 0. iClone re-authoring remains the
fallback for clips where neither result looks right in the test bed.
## Outgoing props (game → iClone staging)
`exchange/outgoing-props/` carries game assets exported as iClone staging props (the
reverse direction of the bridge). `tools/export_boat_prop.py` builds the boat prop
(hull at game scale + placeholder mast/sail/oar + seat markers + waterline +
1.9 m ref figure) with a self-verify gate incl. right-side-up checks — the FBX ships
via git for the PC to pull. Boat ACTION clips use **direct names** (no nd_## stage):
see the "Boat actions" table in the registry. Lesson from building it: an
agent-authored verify gate can pass a visually-wrong export (the first boat was
upside down with all checks green) — **always render and eyeball a new prop** before
shipping, and encode what the eyeball caught as new asserts.
## Launching the game ## Launching the game
- **Always ask Jeremy before launching** (this session he said "let's launch" — that's the go-ahead; don't assume it next time). - **Always ask Jeremy before launching** (this session he said "let's launch" — that's the go-ahead; don't assume it next time).
- Command: `SCENE=dance_test_bed bash tools/game.sh spawn` from `ariki-game/` root. Also `animation_showcase` for paging every clip. - Command: `SCENE=dance_test_bed bash tools/game.sh spawn` from `ariki-game/` root. Also `animation_showcase` for paging every clip.
+56
View File
@@ -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`.
+1 -1
View File
@@ -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

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 387 B

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 387 B

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 387 B

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 387 B

Before

Width:  |  Height:  |  Size: 883 B

After

Width:  |  Height:  |  Size: 883 B

Some files were not shown because too many files have changed in this diff Show More