161 lines
9.4 KiB
Markdown
161 lines
9.4 KiB
Markdown
|
|
# 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.
|