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>
11 KiB
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.45–8.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.
-
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 againstBoatRenderer.BuildCanoeGlbHull):bpy.ops.import_scene.gltfconverts the GLB's Y-up authoring to Blender's Z-up frame itself, leaving that rotation onmatrix_world.transform_applybakes 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 isRotationDegrees=(0,-90,0)(Rot_Y(−90°): X→+Z),Scale=BoatLength, then a Y-only lift (Position=(0,-hullBottomY,0)). In Blender that is exactly: oneRot_Z(−90°)(+X→−Y), uniform ×8.5, Z-lift to keel=0. →orient_hullapplies only that single Z-yaw + scale + Z-lift. The earlierC3 @ 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_hullnowtransform_applys (instead of resettingmatrix_worldto identity), so the importer's rotation is baked and_coords_worldreads clean Z-up vertices. -
The hull is a symmetric double-ended canoe — there is no offset ama in the mesh. Cross-sections of
Boat__PolynesianCanoe_hull.glbshow 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'sPosition=(0,-hullBottomY,0)) — Defect 2's real invariant is satisfied; see §“Fix round” for why the fix doc's asymmetry assertion was replaced. -
foreach_getis the only trustworthy vertex accessor on this mesh. Iteratingfor v in mesh.vertices: v.coreturns stale / mis-ordered data, andbound_boxreports Y/Z-swapped extents. Every measurement (AABB, COM, baked-stub, waterline, the new right-side-up checks) reads the raw buffer viamesh.vertices.foreach_get("co").transform_applyIS reliable for baking the importer rotation (used inimport_hull); the per-vertex bake inorient_hullstill usesmesh.data.transform. -
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°). -
Texture embed required no manual unpack. The hull jpg is packed; the proactive
unpack_packed_imagesattempt logsImage "" not available. Keeping packed image(harmless — it can't save an image with no filepath). Blender'sexport_scene.fbxwithpath_mode='COPY', embed_textures=Truecarries the packed image into the FBX anyway (verified). Noimage.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_applys (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.glbis 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, newrender_snapshots()+--renderflag.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.