Files
animation/.agents/plans/boat-prop-export-results-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

11 KiB
Raw Permalink Blame History

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 mmax(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_applys (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_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.0PASS
  • right-side-up — prow carvings (highest z) near the ends |y|>3.0PASS

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.05PASS (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.pyimport_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.