Files
animation/plans/boat-prop-export-results-2026-07-17.md
T
jeremy 4d6a4f6006 feat(props): export game boat as iClone staging prop (boat_prop.fbx)
tools/export_boat_prop.py (GLM-implemented, 2 rounds): hull at game scale
(8.5 m, keel 0, no recenter) + placeholder mast/sail/steering oar + 5 seat
markers + waterline + 1.9 m ref figure; self-verify incl. right-side-up
gates added after round 1 shipped an upside-down hull with all checks
green. Registry: boat actions table (6 slots, direct names). README
carries CC BY 4.0 attribution + iClone import/filming notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 10:40:37 -07:00

196 lines
11 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.
# 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.458.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.