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

9.4 KiB
Raw Blame History

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:
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.

  1. 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.