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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:02:47 -07:00

116 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Loop-fix results — 2026-07-16
**Implementer:** GLM session · **Plan:** `plans/loop-fix-plan-2026-07-16.md` · **Tool:** `tools/loop_fix.py`
## Verdict
**All seven clips pass `tools/loop_qc.py` with default thresholds (exit 0).** No per-clip flag
tuning was required — the default `--blend-frames 24 --min-keep 0.6` works for every clip.
| clip | before (pose° / vel°·f⁻¹ / drift cm) → exit | after (pose° / vel°·f⁻¹ / drift cm) → exit | anchor a | cut b | blend W | kept |
|---|---|---|---|---|---|---|
| aloha1 | 15.91 / 9.54 / 15.52 → 1 | **0.00 / 0.56 / 0.00 → 0** | 51 | 627 | 24 | 80.0% |
| dance1 | 24.76 / 10.26 / 128.78 → 1 | **0.00 / 1.14 / 0.00 → 0** | 185 | 621 | 24 | 60.6% |
| hakadance1 | 108.72 / 0.04 / 27.66 → 1 | **0.00 / 0.34 / 0.00 → 0** | 44 | 529 | 24 | 67.4% |
| hakadance2 | 0.00 / 0.11 / 25.50 → 1 | **0.00 / 0.19 / 0.00 → 0** | 44 | 687 | 24 | 89.3% |
| alohaOG | 97.81 / 7.42 / 25.15 → 1 | **0.00 / 0.57 / 0.00 → 0** | 39 | 610 | 24 | 79.3% |
| firedance1 | 95.92 / 12.98 / 142.51 → 1 | **0.00 / 1.14 / 0.00 → 0** | 185 | 621 | 24 | 60.6% |
| hakaOG | 108.06 / 0.04 / 28.93 → 1 | **0.00 / 0.34 / 0.00 → 0** | 44 | 529 | 24 | 67.4% |
Exit codes captured directly from `loop_qc.py` (not through a pipe). QC limits: pose ≤ 5°,
velocity ≤ 3°/frame, root drift ≤ 3 cm.
All outputs: `exchange/looped-glb/<clip>.glb`. Originals in `exchange/converted-glb/` untouched.
## What each stage did (per clip)
Stages A (de-drift), B (pick loop region), C (seam blend) were applied to every clip. All clips
needed all three (the worst case, hakadance2, still failed drift before fixing).
| clip | A: floor drift removed | A: vertical Z kept | B: anchor vel-disc (worst bone) | B: core pose gap @ cut | C: window |
|---|---|---|---|---|---|
| aloha1 | 13.1 cm | 8.3 cm | 0.19°/f (foot_r) | 57.8° | 24 f |
| dance1 | 62.2 cm | 112.8 cm | 1.00°/f (pinky_01_r) | 46.6° | 24 f |
| hakadance1 | 23.1 cm | 15.2 cm | 0.11°/f (upperarm_l) | 49.9° | 24 f |
| hakadance2 | 22.1 cm | 12.8 cm | 0.11°/f (upperarm_l) | 40.8° | 24 f |
| alohaOG | 24.6 cm | 5.2 cm | 0.49°/f (hand_r) | 45.4° | 24 f |
| firedance1 | 91.1 cm | 109.6 cm | 1.00°/f (pinky_01_r) | 46.6° | 24 f |
| hakaOG | 24.2 cm | 15.8 cm | 0.10°/f (upperarm_l) | 48.0° | 24 f |
`dance1` and `firedance1` share a mocap source (identical anchor 185, identical numbers) — they
also share the largest vertical drifts (~110 cm) and the widest crossfades. Both still pass.
## Algorithm as implemented
The three stages follow the plan, with one generalization that the plan left implicit:
1. **Stage A — de-drift (root).** Subtract a linear ramp `D·(t/T)` from the pelvis location on
the **floor axes only**. Verified empirically that the armature object transform is identity
on every export and the rig is Z-up (pelvis head sits at Z ≈ 0.5 m, X ≈ Y ≈ 0 at rest), so
horizontal = {X, Y} and vertical = Z. Vertical is left untouched (crouches are choreography).
*Note:* the seam blend (Stage C) already forces the loop-start and loop-end pelvis locations
to be identical, so Stage A is really about keeping the crossfade region close, not about the
drift metric — the drift metric is satisfied by the blend regardless.
2. **Stage B — pick loop region [a, b].** This is the part that had to be more than "trim to F".
Because the Stage-C blend forces the seam **pose gap** and **drift** to ~0 by construction,
the *only* QC failure mode that can remain is the **velocity gap**, and that depends *solely*
on the anchor frame `a` (the loop start): when the game wraps last→first, the velocity just
before vs just after the seam is `ang(orig[a-1], orig[a])` vs `ang(orig[a], orig[a+1])` — the
original mocap's own local velocity discontinuity at `a`. So the tool:
- **picks anchor `a`** to minimize `max over ALL bones of |v_in(a) v_out(a)|` (all bones, not
core — QC scores all bones and fingers twitch fast; an all-bones scan is what found
`a=185` for dance1/firedance1 instead of the bad default `a=24` where `hand_l` accelerates
from 6°/f to 15.7°/f). `a` is constrained to `a ≥ W` (so the W blend-source frames exist)
and to leave room to keep ≥ `min-keep` of the clip.
- **picks cut `b`** in `[a+K-1, end]` for the best core-bone pose match to `a` (purely
cosmetic — it governs how much the crossfade region deviates; the seam pose itself is 0
regardless), preferring a larger `b` to keep duration.
3. **Stage C — seam blend.** Standard motion-graph crossfade. Output = frames `[a, b]` rebased
to start at 0. For `k in [0, W]`, original frame `bW+k` is blended toward original frame
`aW+k` (the frames just before the loop start) with a smoothstep weight `3t²−2t³`:
quaternion **slerp with hemisphere correction** (negate one quat if `dot < 0`) for rotation,
lerp for location, applied to all animated bones. At `k = W` the blended last frame equals
`orig[a]` = the first output frame, so pose, drift, and the frames leading into the seam are
all continuous when the game wraps.
## Implementation notes / discoveries
- **Blender 5.1 slotted actions.** `Action.fcurves` does not exist in 5.1 (data lives in
`action.layers[].strips[].channelbags[].fcurves`). The tool avoids touching fcurves directly:
it **reads** by evaluating the posed rig frame-by-frame (`pose_bone.matrix_basis`, exactly as
`loop_qc.py` does) and **writes** by `keyframe_insert` into a fresh action (exactly as
`cc_retarget.py` does).
- **Duplicate-action export bug (fixed).** The glTF import leaves the source action on the
armature. Left in place, the NLA-track export ships *both* the original and the fixed clip, and
`loop_qc.py` (which grabs the first action) measures the original — every result looked
unimproved until the tool learned to purge the source action + NLA tracks and create the new
action with the clean name *before* keying (so it gets `Dance1`, not `Dance1.001`).
- **"No mesh" criterion is satisfied at the file level.** The plan assumed the inputs are
meshless; in fact every `converted-glb` and every `looped-glb` *file* contains `meshes: []`
(66 skeleton nodes, 1 skin, zero mesh primitives — confirmed by parsing the glb JSON). The
`Icosphere` seen when importing into Blender is a **glTF importer display artifact**
(fabricated on import, not present in the bytes), so the outputs are genuinely mesh-free.
- **Frame range starts at 1, not 0.** The NLA strip is placed at frame 1 (matching
`cc_retarget.py`), so exported actions report `frame_range = (1, N)`. `loop_qc.py` measures
gaps relative to its `frame_range`, so this is harmless.
## Acceptance criteria check
1.`tools/loop_fix.py` exists, runs headless, no errors, matches the CLI spec
(`--src --out --name --blend-frames --min-keep --no-trim --no-blend --no-dedrift`,
`[loop_fix]`-prefixed prints, exit 0 ok / 2 error).
2. ✅ All seven `exchange/looped-glb/<clip>.glb` exist and `loop_qc.py` exits **0** on each with
default thresholds.
3. ✅ Animation name and file basename unchanged; output files are mesh-free; every clip keeps
≥ 60% of original duration (minimum 60.6% — dance1 / firedance1).
4.`tools/loop_qc.py` and its thresholds were not modified (empty `git diff`).
5. ✅ This report written.
## Out-of-scope adherence
`tools/cc_retarget.py`, `tools/loop_qc.py`, `exchange/converted-glb/*`, `.claude/`, and the
ariki-game repo were not modified. Nothing was committed or pushed; all changes are in the
working tree. No packages installed (Blender's bundled `bpy`/`mathutils` only).