Files
animation/.agents/plans/boat-prop-export-plan-2026-07-17.md
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

161 lines
9.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan: `tools/export_boat_prop.py` — export the game boat as an iClone staging prop
**Status:** ready for implementation · **Author:** Fable 5 session 2026-07-17 · **Implementer:** GLM session
## 0. Context (you have no other context — read this fully)
This repo is the animation bridge for the game repo at
`/Users/jeremykashkett/Tinqs/local.repo/ariki-game` (read-only for you). Jeremy will
author boat-interaction character animations in iClone 8 on a Windows PC and needs the
game's boat as a correctly-scaled FBX prop.
The game's vessel: `ariki-game/assets/models/glbs/Boat__PolynesianCanoe_hull.glb`
(832 KB, one mesh `Object_0`, embedded jpg texture) — a carved hull WITH baked ama
outrigger + iako booms; only the sail was cut off. At runtime,
`ariki-game/src/Viewer/BoatRenderer.cs` scales it ×8.5, rotates it (0,90°,0), and
builds the mast/sail/steering-oar procedurally plus 5 seat marker nodes. Your tool
reproduces that assembled vessel as one static FBX.
Build tool: Blender 5.1.2 headless at `/Applications/Blender.app/Contents/MacOS/Blender`.
Run pattern (match `tools/cc_retarget.py` style — argv after `--`, `[export_boat_prop]`
prefixed prints):
```
"$BLENDER" --background --python tools/export_boat_prop.py -- \
[--hull <path>] [--out exchange/outgoing-props/boat/boat_prop.fbx] [--scale 8.5] \
[--no-ref-figure] [--fbx-scale-mode FBX_SCALE_UNITS] [--no-verify]
```
## 1. Ground-truth constants (harvested from BoatRenderer.cs — trust these)
Game frame: meters, Y-up, bow = +Z, keel at Y=0. BoatLength=8.5, BoatWidth=2.4,
halfLen=4.25. GLB raw ≈1.0 unit long along its local +X.
- Hull: rotate so glb bow (+X) → game +Z; uniform ×8.5; lift so keel (min Y) = 0.
Expected scaled AABB ≈ 8.5 L × 5.64 W (includes ama) × 2.95 H.
- `deckTop = max(0.12 × scaledHeight, 0.4)`**0.40 m** — compute from the measured
box, don't hardcode.
- Seats (name → game (x,y,z) m): Seat_Navigator (0, 0.75, +2.55) · Seat_Lookout
(0, 4.00, 0.20) · Seat_Fisher (1.50, 0.65, 0) · Seat_Rest (0, 0.75, 2.975) ·
Seat_Helm (+0.072, 0.50, 2.72).
- Steering oar (hoe uli): pivot at game (0.384, 0.75, 3.655); pivot rotation
(5°, 10°, 0°) then oar-mesh child rotation (38°, 0, 0) — raked aft. Shaft along the
oar's local Y: grip tip +1.31, pivot 0, blade center ≈ 1.44 (blade ~0.34 wide,
~1.1 long, ~0.05 thick), tip 2.11. Shaft radius ~0.04.
- Sail assembly: pivot (0, 0.40, 0). Mast: radius 0.055, height 4.2, base at deckTop.
Sail: crab-claw, foot width 2.1 spreading toward game **X**, height 3.8, foot at
game y = deckTop+0.45, z = 0.10. Boom: radius 0.045, length 2.205, along game X,
centered (1.05, deckTop+0.45, 0.10).
- **No outrigger placeholder** — the ama is baked into the GLB.
- Waterline: still-water surface at game y = **0.06** (keel rides 6 cm above water).
- Reference human: 1.9 m tall.
## 2. Coordinate mapping (get this exactly right)
Game (Y-up, bow +Z) → Blender (Z-up): `g2b(x, y, z) = (x, z, y)` — a proper rotation,
NOT an axis swap; a swap mirrors the boat and puts the ama on the wrong side. Bow ends
up along Blender **Y**, up = +Z. For rotations use matrix conjugation
`R_blender = C @ R_game @ C.transposed()` where C is the g2b rotation matrix
(mathutils.Matrix); compose the oar's pivot and child rotations in game space first.
## 3. Build steps
1. `bpy.ops.wm.read_factory_settings(use_empty=True)`; import the hull GLB.
**Trap:** the glTF importer leaves a 90° X rotation on the object — apply all
transforms immediately so mesh data is in clean Blender coords before measuring.
2. Rotate 90° about Z (glb bow +X → Blender Y), scale ×8.5, apply; translate so
min-Z = 0, apply. Measure the AABB and print it.
3. Compute deckTop from the measured height (formula §1).
4. **Ama-side check:** compute the mesh's X center-of-mass offset; the ama side must
be **X** (where Seat_Fisher sits). If it comes out +X, redo step 2 with +90°
instead and print which you used. Print `[export_boat_prop] ama side: -X` etc.
5. Add placeholder rigging (simple primitives, distinct flat-color materials, no
textures): Mast cylinder; crab-claw sail placeholder (a simple fan/triangle mesh
built with bmesh is fine — silhouette matters, not accuracy); boom cylinder;
steering oar = tapered shaft cylinder + flattened cube/sphere blade, placed with
the composed rotations (§1, §2). Convert every game-space position via g2b.
6. Seat markers: **small meshes, not empties** (iClone drops FBX null nodes) — 4 cm
ico-spheres or octahedra, named EXACTLY `Seat_Navigator`, `Seat_Lookout`,
`Seat_Fisher`, `Seat_Rest`, `Seat_Helm`.
7. `Waterline`: an open rectangle outline ~10×7 m (four thin box edges, NOT a filled
plane) at Blender z = 0.06.
8. `RefFigure_190cm` (skip with --no-ref-figure): a 1.9 m capsule-ish figure (cylinder
+ sphere cap is fine, Ø~0.35) standing ON Seat_Helm (feet at that marker's z).
Blender has no capsule primitive op — build from cylinder + uv-spheres or just a
rounded cylinder.
9. Parent everything to the hull object; rename hull `Boat_ArikiCanoe`. Export FBX:
```python
bpy.ops.export_scene.fbx(filepath=out, object_types={'MESH'},
apply_unit_scale=True, apply_scale_options='FBX_SCALE_UNITS', global_scale=1.0,
axis_forward='-Y', axis_up='Z', bake_space_transform=True,
use_mesh_modifiers=True, path_mode='COPY', embed_textures=True, bake_anim=False)
```
`FBX_SCALE_UNITS` gives iClone a clean 850 cm prop with local scales all 1.0. Expose
`--fbx-scale-mode` to switch to FBX_SCALE_ALL as an escape hatch.
10. **Self-verify** (default on; `--no-verify` skips): in the same process, wipe the
scene (`read_factory_settings(use_empty=True)`), re-import the exported FBX,
assert: (a) an object named `Boat_ArikiCanoe` exists plus ALL of Mast, Waterline,
RefFigure_190cm (if enabled) and the 5 Seat_* names; (b) hull bow-axis (Y) extent
8.458.55 m; (c) overall min-Z ≈ 0 ±0.05 for the hull; (d) Waterline center z ≈
0.06 ±0.02. Print PASS/FAIL per check; `sys.exit(0)` all pass, `sys.exit(1)`
otherwise, `sys.exit(2)` on exceptions.
## 4. Second deliverable — `exchange/outgoing-props/boat/README.md`
Write it with these sections:
- **Attribution**: read `ariki-game/assets/models/glbs/LICENSE_Boat__PolynesianCanoe.txt`
and reproduce the CC BY 4.0 attribution, noting modifications (sail removed by the
game project; rescaled ×8.5; staging markers/placeholder rigging added).
- **Contents legend**: every named node and what it's for (from §1/§3).
- **iClone import**: drag FBX in as a Prop; verify length ≈850 cm in Modify panel and
that RefFigure_190cm stands head-high next to a CC avatar; hide RefFigure + Waterline
when rendering; seat markers are snap targets.
- **Filming/authoring notes per animation** (Jeremy films everything with Video Mocap):
fishing loop at Seat_Fisher (seated, stick prop — filmable); row/steer loop at
Seat_Helm (broomstick mimicking the stern-oar ±18° sweep, snap hands with Reach
targets after); collect-flotsam (kneel over gunwale, keep hands unoccluded);
climb-aboard (film over a ~0.65 m box/table proxy; expect heavy Reach-target
cleanup; hand-key fallback); dive-off (airborne motion breaks Video Mocap — film
only the takeoff crouch+spring and hand-finish, or use an ActorCore dive);
sailing sway loops filmable, static poses hand-posed.
- **Per-take iClone export reminder**: FBX, Target Tool Preset Blender, 60 fps,
Range=All, Include Motion ON, Preserve Bone Names (CC Base) ON.
## 5. Acceptance criteria
1. `tools/export_boat_prop.py` exists, runs headless with zero tracebacks, CLI per §0.
2. Running it produces `exchange/outgoing-props/boat/boat_prop.fbx` AND the built-in
self-verify exits 0.
3. `exchange/outgoing-props/boat/README.md` written per §4.
4. Results report `plans/boat-prop-export-results-2026-07-17.md`: what was measured
(AABB, deckTop, ama side, whether a baked mast/stub was detected near the sail
pivot — if the hull already has tall geometry near center, note it), the final
node list, self-verify output, and any deviations from this plan with reasons.
## 6. Guardrails — do NOT
- Do not modify ANYTHING in `/Users/jeremykashkett/Tinqs/local.repo/ariki-game`
(read-only reference).
- Do not modify existing tools (`cc_retarget.py`, `loop_qc.py`, `loop_fix.py`,
`pingpong_bake.py`, `rename_clip.py`, `mocap_retarget.py`, etc.), anything in
`.claude/`, `docs/`, or `exchange/` outside `exchange/outgoing-props/boat/`.
- Do not commit or push. Leave everything in the working tree.
- Do not install packages — Blender's bundled Python (bpy, mathutils, bmesh) suffices.
## 7. Known traps
- Blender 5 removed legacy APIs in places (e.g. `Action.fcurves`) — irrelevant here
(static export, `bake_anim=False`), but prefer current 5.x APIs throughout.
- Don't pipe the Blender run through `grep`/`tail` when you need its exit code.
- `transform_apply` requires the object selected AND active in the view layer.
- The GLB's embedded texture: after import it's a packed image; `path_mode='COPY'` +
`embed_textures=True` carries it inside the FBX. Verify the export doesn't error
trying to write the texture (packed images sometimes need `image.unpack()` or
saving to a temp file first — handle whichever occurs).
- Object names must survive export EXACTLY (no `.001` suffixes — check for collisions).
- Parenting: use `child.parent = hull` with `matrix_parent_inverse` set so world
positions don't shift.