Compare commits
31 Commits
95331a8c36
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 854dbb5ce4 | |||
| 28fdf940f7 | |||
| 06d950649b | |||
| 078b0b7a07 | |||
| 1219954fdc | |||
| 82266c97bb | |||
| b4d962167f | |||
| 998e306804 | |||
| 228f26e7dc | |||
| cd2e598354 | |||
| 7f4300eae6 | |||
| 9f3f5ac891 | |||
| 528bf1a905 | |||
| b68fa7b920 | |||
| c79d23549a | |||
| 19181157cc | |||
| 3b132ebe9e | |||
| f468d148bc | |||
| 7b44019c9a | |||
| 485c27a04c | |||
| a4ad489229 | |||
| 4d6a4f6006 | |||
| 17ff1f748e | |||
| df2629af16 | |||
| 9b190e742e | |||
| 6a1290b1d1 | |||
| b918d8670f | |||
| da0567b79d | |||
| ac691dee90 | |||
| d8e2f2782c | |||
| d3206f6ce2 |
@@ -0,0 +1,11 @@
|
||||
# .agents/AGENTS.md — registry + delegation
|
||||
|
||||
Detailed layer behind root `AGENTS.md`. This repo has no repo-specific
|
||||
multi-agent registry beyond:
|
||||
|
||||
- **`loop-qc`** (`.claude/agents/loop-qc.md`) — sub-agent for the loop-QC pass
|
||||
on converted GLBs; use it instead of hand-running `tools/loop_qc.py` when
|
||||
checking a batch of clips.
|
||||
|
||||
For cross-repo agent roster, MCP services, and delegation rules, see the hub:
|
||||
`tinqs-ltd/docs/.agents/AGENTS.md`.
|
||||
@@ -0,0 +1,26 @@
|
||||
# SOUL.md — animation repo identity
|
||||
|
||||
Inherits the shared identity, values, and confidentiality rules from the hub:
|
||||
`tinqs-ltd/docs/.agents/SOUL.md`. Read that first if this is your first session
|
||||
on any Tinqs repo.
|
||||
|
||||
## What this repo is
|
||||
|
||||
The animation-pipeline hub and Mac↔PC bridge for Ariki. iClone 8 + Video Mocap
|
||||
run on Jeremy's Windows PC; Blender 5, the retarget tools, and `ariki-game` live
|
||||
on both machines. This repo carries the skills, tools, and files-in-flight that
|
||||
turn PC-side motion capture (iClone renders, video mocap) into game-ready
|
||||
Quaternius-rig GLB clips consumed by `ariki-game`.
|
||||
|
||||
## Scope
|
||||
|
||||
- **In scope:** FBX→GLB retargeting (Blender headless), loop QC and loop-fixing,
|
||||
dance/clip naming and the registry, boat/prop export, the exchange-folder
|
||||
bridge protocol, and the skills that encode iClone/mocap operator knowledge.
|
||||
- **Out of scope:** game-side rendering, ClipCatalog code, dance ceremony
|
||||
design (owned by `ariki-game`) — this repo only produces and ships the assets.
|
||||
|
||||
## Confidentiality
|
||||
|
||||
Same as the hub: this repo is private/internal. No special classified material
|
||||
lives here (Cosmos-related confidentiality does not apply to animation tooling).
|
||||
@@ -0,0 +1,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.45–8.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.45–8.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.
|
||||
@@ -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 25–142 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 `b−W+k` with
|
||||
`blend( original[b−W+k], original[a−W+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 | 13–19 | 4–8 |
|
||||
| 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 `b−W+k` is blended toward original frame
|
||||
`a−W+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).
|
||||
@@ -0,0 +1,7 @@
|
||||
# rules/ — always-on conventions for this repo
|
||||
|
||||
No repo-specific rules beyond what's already always-on in root `AGENTS.md`
|
||||
(git via `tinqs push`/`tinqs pull`, stage-by-explicit-path, naming law) and the
|
||||
shared hub rules at `tinqs-ltd/docs/.agents/rules/`. Add a file here when a
|
||||
convention needs to be enforced repo-wide and doesn't fit in `AGENTS.md`'s
|
||||
thin-entry budget.
|
||||
@@ -0,0 +1,18 @@
|
||||
# skills/ — playbooks
|
||||
|
||||
This repo's operator playbooks are Claude-Code-native skills and live at
|
||||
`.claude/skills/` (auto-discovered by Claude Code), not duplicated here:
|
||||
|
||||
| Skill | Covers |
|
||||
|---|---|
|
||||
| `animation` | Operator playbook — the batch workflow, naming, loop QC, gotchas |
|
||||
| `animation-creation` | Authoring new clips on the shared Quaternius skeleton |
|
||||
| `iclone-video-mocap` | iClone 8 + Video Mocap: filming, cleanup, FBX export |
|
||||
| `pose-estimation` | Video→pose-landmark extraction (MediaPipe) |
|
||||
| `retarget-animations` | Deprecated early retarget notes, kept for history |
|
||||
|
||||
An agent-level playbook (`loop-qc`) lives at `.claude/agents/loop-qc.md`. Follow
|
||||
the per-repo convention (`.agents/skills/` as the canonical location) once this
|
||||
repo's skills are ported off `.claude/`; until then this file is the pointer so
|
||||
there's a single place agents look, per the SoT rule (no second source of
|
||||
truth).
|
||||
@@ -0,0 +1,90 @@
|
||||
# Architecture — animation pipeline
|
||||
|
||||
Entry point to architecture law for this repo. Per-system detail lives in
|
||||
`.agents/wiki/architecture/`.
|
||||
|
||||
## The one-sentence shape
|
||||
|
||||
PC-side motion capture (iClone 8 + Video Mocap) → this repo (Mac↔PC bridge,
|
||||
Blender-headless retargeting, loop QC) → `ariki-game` (Quaternius-rig GLB
|
||||
consumption via ClipCatalog).
|
||||
|
||||
## Machines and where things live
|
||||
|
||||
| Machine | Owns |
|
||||
|---|---|
|
||||
| PC (Windows) | iClone 8 + Video Mocap plugin — authors motion, exports FBX |
|
||||
| Mac (and PC) | Blender 5.1.2, this repo's retarget tools, `ariki-game` checkout |
|
||||
|
||||
Files move machine-to-machine only through `exchange/` in this repo, synced
|
||||
with `tinqs push`/`tinqs pull` (Git LFS for `.fbx/.glb/.mp4/.task` — see
|
||||
`.gitattributes`). Raw `git pull` hangs on LFS in tinqs.com repos.
|
||||
|
||||
## Pipeline stages
|
||||
|
||||
1. **Capture (PC)** — iClone export, Blender preset, Range=All, Preserve Bone
|
||||
Names ON → `exchange/incoming-fbx/`.
|
||||
2. **Retarget (either machine)** — Blender-headless, source-specific retargeter
|
||||
in `tools/`, all bake onto the shared Quaternius rig and export a
|
||||
mesh-stripped GLB with one NLA track per clip:
|
||||
- `cc_retarget.py` — Reallusion CC/iClone `CC_Base_*` FBX (60fps; rejects
|
||||
2-frame `_TempMotion` exports as a guard against motion-less takes)
|
||||
- `mixamo_retarget.py` — Mixamo FBX (prefix auto-detect)
|
||||
- `kevin_retarget.py` — Kevin Iglesias Unity FBX (`B-*` rig; hardcoded
|
||||
target path, non-portable)
|
||||
- `mocap_retarget.py` — MediaPipe pose JSON → GLB (rotation solving, no
|
||||
source armature)
|
||||
→ `exchange/converted-glb/`.
|
||||
3. **Loop QC** — `tools/loop_qc.py` (pre-commit gate: pose/velocity/root-drift
|
||||
continuity across the seam, exit 0 = smooth). Fixers when it fails:
|
||||
`tools/loop_fix.py` (de-drift/trim/seam-blend → `exchange/looped-glb/`) and
|
||||
`tools/pingpong_bake.py` (palindrome bake, seam-perfect but motion reverses
|
||||
at the turn → `exchange/pingpong-glb/`, clip suffix `PP`). Every adopted
|
||||
dance ships as a base clip + `_pp` pair.
|
||||
4. **Ship** — commit + `tinqs push` the GLBs in this repo, then copy into
|
||||
`ariki-game/assets/quaternius/dancegen/` (auto-discovered by ClipCatalog as
|
||||
`<pack>/<ClipName>` — also watches `kevin/`, `mixamo/`), write one dance JSON
|
||||
per clip in `ariki-game/assets/dances/`, `game.sh import`, verify clip names
|
||||
match GLB `animations[].name` exactly, commit in ariki-game (push only when
|
||||
Jeremy asks).
|
||||
5. **Post-ingest QC (game side, out of this repo)** — `ariki-game/tools/anim_qc.py`
|
||||
re-checks world-space seam continuity + renders a seam-cross video for eye
|
||||
review. Two QC tools by design: this repo's gate is pre-commit/local-space,
|
||||
the game's is post-ingest/world-space.
|
||||
|
||||
## Naming law
|
||||
|
||||
Full detail: `.agents/wiki/dances/REGISTRY.md` (source of truth — do not
|
||||
duplicate its content elsewhere). Summary: new takes get a provisional `nd_##`
|
||||
id (never a content-derived name); a take is renamed to its ceremony code
|
||||
(snake_case of the sim's `DanceType` enum + index) only once Jeremy assigns it
|
||||
to a specific ceremony, via `tools/rename_clip.py`. Rejected/superseded takes
|
||||
are archived (`archive/`), never deleted; numbers are never reused.
|
||||
|
||||
## Canonical target rig
|
||||
|
||||
All retargeters bake onto ariki-game's
|
||||
`assets/quaternius/base-characters/.../Godot - UE/Superhero_Male_FullBody.gltf`.
|
||||
Run from this repo, `--target` must be passed explicitly (the tool's default
|
||||
path is ariki-relative and won't resolve here). The rig/pack standard itself
|
||||
(65-bone Quaternius UE-named skeleton, hero body `Ariki_Female_QuatSkin.glb`) is
|
||||
governed by `ariki-game/.humans/animation-bone-sources.html` and
|
||||
`ariki-game/.agents/wiki/quaternius-ual-packs.md` — this repo's iClone/mocap
|
||||
lane is explicitly a separate clip source (not UAL1/UAL2/Kevin/Mixamo), landing
|
||||
in `dancegen/` only.
|
||||
|
||||
## Tool provenance / mirroring
|
||||
|
||||
`tools/cc_retarget.py`, `mixamo_retarget.py`, `kevin_retarget.py`,
|
||||
`mocap_retarget.py` are **mirrors of `ariki-game/tools/`** — the game copies are
|
||||
authoritative; re-copy from ariki-game when they change (they have silently
|
||||
diverged before). `.claude/skills/` here mirror a subset of `ariki-game/.claude/skills/`
|
||||
plus `~/.claude/skills/pose-estimation` — see `README.md` Provenance section.
|
||||
|
||||
## See also
|
||||
|
||||
- `.agents/wiki/architecture/` — per-system detail (currently: this file covers
|
||||
the whole pipeline; split out if a subsystem grows its own doc).
|
||||
- `.agents/wiki/dances/REGISTRY.md` — naming/registry source of truth.
|
||||
- `.agents/wiki/iclone-bridge.md` — PC-lane routing stub.
|
||||
- `.agents/wiki/devops-reports/` — point-in-time audits and convergence reports.
|
||||
@@ -0,0 +1,12 @@
|
||||
# architecture/ — per-system detail
|
||||
|
||||
This repo's pipeline is small enough that `.agents/wiki/ARCHITECTURE.md` covers
|
||||
it end to end today; nothing has grown large enough yet to need its own
|
||||
per-system doc here. Split a topic out into this folder (and link it from
|
||||
`ARCHITECTURE.md`) when it does — likely candidates as the repo grows:
|
||||
|
||||
- Retarget-tool internals (per-source rig mapping, bone-name conventions).
|
||||
- Loop QC/fix algorithms (pose-gap, velocity-gap, root-drift math).
|
||||
- The exchange-folder state machine (`exchange/GUIDE.md` currently owns this —
|
||||
promote here if it needs architecture-level treatment beyond an operator
|
||||
guide).
|
||||
@@ -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.
|
||||
@@ -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** | ~49–50 m each | The stars of the dance — huge, near-constant sweeps |
|
||||
| 2 | Forearms | ~32–33 m | Big arcs from the elbow reinforce the hand work |
|
||||
| 3 | Shins | ~25–26 m | Busy legs — steps, lifts, direction changes |
|
||||
| 4 | Upper arms / feet | ~18–21 m | Full-shoulder arm swings; active but grounded feet |
|
||||
| 5 | Head / spine / pelvis | ~16–17 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.7–2.5 m per axis — meaning full-wingspan sweeps that go both
|
||||
**high** (≈1.5–1.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 **~100–103 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.
|
||||
@@ -0,0 +1,18 @@
|
||||
# iClone bridge — PC-lane overview
|
||||
|
||||
(Stub created 2026-07-23 — this path was referenced by `exchange/GUIDE.md` but never
|
||||
written. The full operational detail lives elsewhere; this page just routes you.)
|
||||
|
||||
The Mac↔PC animation bridge: iClone 8 + Video Mocap on Jeremy's Windows machine authors
|
||||
motion; this repo carries files both directions; ariki-game consumes 65-bone Quaternius
|
||||
GLBs.
|
||||
|
||||
| Need | Go to |
|
||||
|---|---|
|
||||
| Which exchange/ folder to use, when files move/archive | `exchange/GUIDE.md` |
|
||||
| Naming (nd_## → ceremony codes), loop-pair rule, registry | `.agents/wiki/dances/REGISTRY.md` |
|
||||
| Filming rules, iClone/AccuFACE settings, mocap cleanup, FBX export | `iclone-video-mocap` skill (`.claude/skills/` here and in ariki-game) |
|
||||
| CC FBX → Quaternius GLB conversion | `tools/cc_retarget.py` (mirror of ariki-game — game copy is authoritative) |
|
||||
| Looping (`_pp`) + QC gates | `tools/pingpong_bake.py` + `tools/loop_qc.py`; game-side `anim_qc.py` (split documented in REGISTRY.md) |
|
||||
| Game-side landing zone | `ariki-game/assets/quaternius/dancegen/` + `assets/dances/*.json` |
|
||||
| The skeleton/pack standard itself | `ariki-game/.humans/animation-bone-sources.html` + `.agents/wiki/quaternius-ual-packs.md` |
|
||||
@@ -0,0 +1,56 @@
|
||||
# Master plan — animation repo
|
||||
|
||||
What's in flight and recently shipped. See `.agents/plans/` for the closed-out
|
||||
plan/results docs behind these items; see `.agents/wiki/dances/REGISTRY.md` for
|
||||
the live per-clip status table (source of truth, not duplicated here).
|
||||
|
||||
## Recently shipped (closed plans, folded into wiki)
|
||||
|
||||
- **Animation-gen pipeline** (2026-07-13) — the FBX→Blender-retarget→GLB→dance-JSON
|
||||
bridge itself; core of `.agents/wiki/ARCHITECTURE.md` today.
|
||||
- **Animation skills adoption** (2026-07-13) — `.claude/skills/animation*`,
|
||||
`iclone-video-mocap`, `pose-estimation` brought in as repo skills.
|
||||
- **Loop fix** (2026-07-16, plan + results) — `tools/loop_fix.py` (de-drift/trim/
|
||||
seam-blend) and `tools/pingpong_bake.py` shipped; every adopted dance now ships
|
||||
as a base clip + `_pp` pair, gated by `tools/loop_qc.py`.
|
||||
- **Boat prop export** (2026-07-17, plan + fixes + results) — `tools/export_boat_prop.py`
|
||||
staging pipeline for the boat action-clip set (see "Boat actions" below).
|
||||
- **Animation-standard convergence** (2026-07-23/24, `.agents/wiki/devops-reports/`) —
|
||||
full audit against ariki-game's animation-standardization contract. Result:
|
||||
pipeline core already conformant (100% of runtime clip GLBs on the 65-bone
|
||||
Quaternius rig, all `_pp` pairs present, dance JSONs clean). Fixes landed:
|
||||
registry corrections (`tide_dance_01` has no sim enum entry — flag before
|
||||
filming; `rain_dance_01` confirmed in-enum), `iclone-bridge.md` written
|
||||
(previously a dead reference), QC-tool split documented (`loop_qc.py` here =
|
||||
pre-commit gate, `ariki-game/tools/anim_qc.py` = post-ingest gate).
|
||||
- **agents.md migration** (2026-07-24, this change) — repo restructured onto the
|
||||
open agent protocol: root `AGENTS.md` (thin) + `.agents/` (SOUL, wiki, plans,
|
||||
skills-pointer). `docs/` and root `plans/`/`devops-reports/` folded into
|
||||
`.agents/wiki/`.
|
||||
|
||||
## In flight — ceremony dance slots awaiting a take
|
||||
|
||||
Per `.agents/wiki/dances/REGISTRY.md`, adopted so far: `fertility_dance_01`
|
||||
(hula), `taming_dance_01`, `war_dance_01` (haka) — each shipped with its `_pp`
|
||||
loop. One provisional take (`nd_01`, baraka1) is filmed and QC-passed but not
|
||||
yet assigned to a ceremony. Still **awaiting a take**: `water_dance_01`,
|
||||
`fire_dance_01`, `food_dance_01`, `sailing_dance_01`, `building_dance_01`,
|
||||
`colony_shield_dance_01`, `age_progression_dance_01`, `star_watching_dance_01`,
|
||||
`rain_dance_01`. `tide_dance_01` has no confirmed sim `DanceType` entry — resolve
|
||||
with Ozan before filming.
|
||||
|
||||
## In flight — boat action clips
|
||||
|
||||
Staging tooling (`tools/export_boat_prop.py`) is built and the boat prop rig
|
||||
verified; the action-clip set itself is **awaiting takes**: `boat_dive`,
|
||||
`boat_climb`, `boat_collect`, `boat_row_loop`, `boat_fish_loop`,
|
||||
`boat_sail_idle`. Two boat-adjacent clips are already live in-game
|
||||
(`CanoeDismount`/`CanoeSitIdle`, `run_to_dive`) but predate this registry and
|
||||
need a `boat_*` code assigned at the next adoption pass.
|
||||
|
||||
## Open threads
|
||||
|
||||
- `tools/kevin_retarget.py` has a hardcoded, non-portable target path — fine for
|
||||
now, flagged in `AGENTS.md` gotchas.
|
||||
- `.claude/skills/retarget-animations/` is kept only for history (deprecated,
|
||||
superseded by the per-source retargeters in `tools/`).
|
||||
@@ -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.
|
||||
@@ -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/`.
|
||||
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).
|
||||
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).
|
||||
6. **Verify** clip names, then commit in ariki-game.
|
||||
7. **Launch the test bed only after asking** (see below).
|
||||
8. Leave `exchange/` cleanup until Jeremy confirms the batch looks right in-game.
|
||||
5. **Loop QC gate** — run `loop_qc.py` on each converted GLB (see below). Flag anything that isn't smooth *before* it ships.
|
||||
6. **Import** so Godot generates sidecars: `bash tools/game.sh import` (from ariki-game root).
|
||||
7. **Verify** clip names, then commit in ariki-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)
|
||||
```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.
|
||||
|
||||
## Naming conventions (must be internally consistent)
|
||||
- **Pack name = GLB basename** (lowercase): `aloha1`, `dance1`, `hakadance2`.
|
||||
- **Clip name = `--name` = the animation name baked into the GLB** (PascalCase): `Aloha1`, `Dance1`, `HakaDance2`.
|
||||
- **Clip ref in JSON = `<pack>/<ClipName>`**: `aloha1/Aloha1`.
|
||||
## Naming system (registry: `.agents/wiki/dances/REGISTRY.md` — read it, it is the source of truth)
|
||||
Two-stage names, following the **game's item-id convention (lowercase snake_case)**.
|
||||
**New dances get a provisional id, never a content name**: at conversion, assign the
|
||||
next free `nd_##` from the registry — pack `nd_08`, clip `ND08`, JSON `nd_08.json`,
|
||||
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.
|
||||
|
||||
### 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.
|
||||
- **`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 60–90% 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
|
||||
- **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.
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# AGENTS.md — animation repo
|
||||
|
||||
Entry point for AI agents working in `local.repo/animation` (all tools: pi,
|
||||
Claude Code, Cursor). This repo is Jeremy's animation-pipeline hub **and** the
|
||||
Mac↔PC bridge that carries iClone/mocap exports into `ariki-game`. iClone 8 +
|
||||
Video Mocap live on the PC; Blender 5, the retarget tools, and the game repo
|
||||
live on both machines.
|
||||
|
||||
Read `.agents/SOUL.md` at session start, then use the knowledge map below —
|
||||
this file stays thin; the operational detail lives in `.claude/skills/` and
|
||||
`.agents/`.
|
||||
|
||||
## Knowledge map
|
||||
|
||||
| Category | Path | When |
|
||||
|---|---|---|
|
||||
| Identity | `.agents/SOUL.md` | always-on |
|
||||
| Batch workflow, canonical retarget command, naming, loop QC, gotchas, repo etiquette | `.claude/skills/animation/SKILL.md` | always-on — read before any batch or clip work |
|
||||
| Architecture (pipeline stages, canonical rig, tool provenance) | `.agents/wiki/ARCHITECTURE.md` | consult when touching the pipeline |
|
||||
| Per-system architecture detail | `.agents/wiki/architecture/` | consult-when |
|
||||
| In-flight work + recently shipped | `.agents/wiki/master-plan.md` | check before starting new work |
|
||||
| Dance/clip naming registry (source of truth for names) | `.agents/wiki/dances/REGISTRY.md` | always-on when naming or shipping a clip |
|
||||
| PC-lane routing stub | `.agents/wiki/iclone-bridge.md` | consult-when |
|
||||
| Point-in-time audits / convergence reports | `.agents/wiki/devops-reports/` | consult-when |
|
||||
| Open/executing plans | `.agents/plans/` | consult-when |
|
||||
| Other operator playbooks (iClone/mocap, clip authoring, pose estimation) | `.claude/skills/` (index: `.agents/skills/README.md`) | consult-when |
|
||||
| Loop-QC sub-agent | `.claude/agents/loop-qc.md` | consult-when |
|
||||
| Repo-wide conventions | `.agents/rules/` | always-on |
|
||||
| Exchange-folder operator guide (which folder, when files move/archive) | `exchange/GUIDE.md` | consult-when doing a batch |
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
AGENTS.md ← you are here
|
||||
.agents/ ← agent context: SOUL, wiki (architecture/master-plan/registry/
|
||||
devops-reports), plans, rules, skills-pointer
|
||||
.claude/ ← Claude-Code-native skills + agents (auto-discovered)
|
||||
tools/ ← Blender-headless retargeters, loop tools, prop export
|
||||
exchange/ ← files in flight between Mac and PC (see exchange/GUIDE.md)
|
||||
archive/ ← retired takes — never deleted, never renumbered
|
||||
```
|
||||
|
||||
## Git
|
||||
|
||||
- Branch is **`main`**. Sync via **`tinqs push`/`tinqs pull`** (not raw git —
|
||||
this repo uses Git LFS for `.fbx/.glb/.mp4/.task`; a plain `git pull` hangs).
|
||||
Report the commit hash after pushing.
|
||||
- **Stage explicit paths, never `git add -A`** — ariki-game and this repo often
|
||||
carry unrelated in-flight files.
|
||||
- Never commit secrets.
|
||||
|
||||
## The rule that bites most often
|
||||
|
||||
**Green verify ≠ correct.** An agent-authored gate once passed an upside-down
|
||||
boat export. Always render and eyeball a new visual asset before shipping —
|
||||
full gotcha list in `.claude/skills/animation/SKILL.md`.
|
||||
@@ -41,7 +41,7 @@ into the game repo.
|
||||
- `kevin_retarget.py` — Kevin Iglesias Unity FBX (`B-*` rig; hardcoded target path — non-portable)
|
||||
- `mocap_retarget.py` — MediaPipe pose JSON → GLB (rotation solving, no source armature)
|
||||
- `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
|
||||
|
||||
|
||||
|
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 |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |