capture_verify example: clusters every part's ship-relative transform across
the new multi-snapshot, multi-instance F10 captures (5 snapshots, 43 e106
instances, all angles). Verdict: the dominant clusters match the static
assembly to ~1 unit on EVERY part — including BOTH engine nacelles at
(+-131, -133, -131) — so the "engines inside the hull" appearance is the
game's own placement: the engine geometry sits recessed in the aft hull, and
the visible "thrusters" in-game are exhaust FX drawn at the GN_Jet/GN_SJet
frames (Z ~ -570, past the stern).
To close that perception gap the viewer now draws simple exhaust cones at the
game's own jet frames (ship::exhaust_frames; part of the external-parts
toggle). The jet frames flip Z, so the cone apex is authored at +Z and lands
trailing aft — verified in the offline render.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the two things the user saw in the Ships browser:
- "Can't find the thrusters": show_external defaulted OFF, hiding the engine
rig / bridge / turrets entirely. Externals are EXACT since the node-TRS fix,
so the toggle now defaults ON (relabelled; off = bare hull bodies).
- "Some parts very far away": families without a composite scene graph (fighter
morph sets like f002 whose detached parts are authored far from the origin,
shared turret/prop part families) fell into the stack-at-origin fallback and
rendered as piles with stray pieces. ShipModel now carries has_composite and
the catalog lists only real assembled ships.
Offline audit across ALL stages (ship_audit example): the only far-away
outliers were composite-less f002 morph parts (now filtered); every capital
ship assembles coherently (e108 places its 7 turret instances, f105 its
mirrored shield generators). All primary composites are single-key tracks —
multikey exists only in animation composites (e901 attacks, missile-open),
which assemble_ship does not select.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The runtime capture served its true purpose: as the answer key that exposed the
static encoding. The XBG7 node joint table (node+0x44) holds NINE keyframe
channels [TX TY TZ RY RX RZ SX SY SZ] behind EIGHT pointer slots shifted one
track forward: channel k+1 = f64@(ptr[k]+8), and channel 0 -- TX, which no
pointer names -- sits at ptr[0]-0x48 (tracks are 0x50-byte records, value at
+8). The old 8-slot read took TY as X, RY as Y (usually 0 -- why both hulls
stacked on the centreline) and never saw TX at all (the +-264 hull offsets).
Euler order is Ry(ch3)*Rx(ch4)*Rz(ch5) with angles applied directly, pinned by
the captured engine nacelle Rx(-15)*Rz(30) decomposition. mesh.rs gains
read_trs9/node_rotation; scene_world_nodes and node_transforms both fixed
(saber fin overrides unaffected).
assemble_ship rewritten fully static and exact:
- every composite-node instance placed (alias duplicates collapsed, real
instances kept) including cross-id turret mounts (rou_e303_wep_01_root x2)
- the e_rou_<id>_eng cluster rig mounts at GN_Engine_01 (two mirrored nacelles
+-131 with -+30-degree cant + centre engine) -- world = frame o rig_local
- brg/sld at their exact GN frames (frames were always exact)
- shared-geometry twin pairs at +-TX: X-reflect the instance whose offset
opposes the geometry's dominant side (viewer reverses winding on det<0)
- squeezed node names resolve (wep02 -> wep_02_01)
Ground-truth gate: ship::tests::static_assembly_matches_runtime_capture asserts
static == the baked e106 capture for all 8 parts (T<1.0, R<0.02) plus both
nacelles, both turrets, and the starboard-hull mirror. Viewer now uses pure
static assembly; ship_capture + the baked table remain as the verification
oracle. Renders show a coherent, bilaterally-symmetric destroyer.
81 formats-lib + all disc tests green; viewer builds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two changes after user feedback that externals sit near-but-wrong (shield inset
into the hull, thruster floating close):
- Scale: the joint table's slots 5–7 are per-axis scale (a GN_ShieldG frame ships
at 0.5, GN_Jet FX at 2.4–6.5) and were being ignored, rendering scaled parts at
the wrong size. ScenePart now carries `s` and applies R·(S·v)+T.
- External placement is fundamentally approximate: a GN_* frame is the mount
PIVOT (f105's two GN_ShieldG frames are both on the centreline), while the
part's outboard/rotational offset lives in a detail sub-rig / runtime code this
static pass can't recover. So assemble_ship gains `include_external`: the hull
tier (rou_ nodes) is exact and always drawn; the external tier (bridge / shield
/ engine at GN_ frames) is opt-in via a "Show external parts" checkbox in the
Ships browser, off by default so the clean, correct hull is the default view.
The module doc is corrected to the scene-graph mechanism (the old "draw parts
untransformed" claim was wrong).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sub-composites (`e_rou_<id>_eng`, `_wep_*`) are LOCAL detail rigs: their
nodes are posed in their own frame, not ship-space, so folding them in placed
the engines at ~mid-ship (Z≈99) instead of aft — the "floating in air" parts.
The PRIMARY composite (`e_rou_<id>`, the one with the most nodes) already carries
the full ship-space layout: every hull body AND every `GN_*` hardpoint frame
(GN_Bridge at the stern, GN_Engine aft, GN_ShieldG amidships…). Assemble from
that one composite only; engines/bridge/shield now resolve to their real aft/mid
frames. Robust to the odd single-composite case (e108's `e_rou_e108_Missile_open`
is still picked as its own primary).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first cut drew every part at the origin, overlapping into a mess — the parts'
vertex buffers are authored around the origin, NOT pre-placed. The real world
placement lives in the composite scene-graph resource `e_rou_<id>`, exactly like
node_transforms poses the DeltaSaber's fins within one resource — but here the
nodes reference SEPARATE geometry resources.
Two tiers, matching how the game splits a ship (as the user described):
- Hull — `rou_<id>_*` scene nodes name the hull body resources and carry their
world transform (bodies that together form the hull, not individually
destructible). Composed parent∘child down the first-child/next-sibling tree.
- External hardpoints — `GN_*` frame nodes are the Vessel mounts (`GN_Bridge_01`,
`GN_ShieldG_01`, `GN_Engine_01`, matching the IDXD recipe Frame names). Each
bridge / shield-generator / engine part is placed at its frame by category+idx —
the individually-destructible parts.
- mesh: `scene_world_nodes` — walk a composite, emit every `rou_`/`GN_` node with
its composed world transform (same record layout + TRS convention as
node_transforms, cross-resource). ScenePart{resource,m,t}+apply.
- ship: `assemble_ship(bytes, id)` — resolve hull nodes to the ship's own parts
(shared cross-id turrets excluded — they need the per-mount recipe), match
external parts to GN frames, fall back to a raw draw when a prop has no
composite. is_base_part now also drops `_dNN`/`_lNN`/`_mNN` LOD copies.
- viewer: build_ship_model runs assemble_ship, decodes only referenced resources,
bakes each part's world pose into its vertices (one posed copy per placement),
then assembles via the shared prepare path. RequestShipRender carries the id.
Verified headless: parts now spread into coherent single-ship extents
(e108 185×174×683, f106 703×479×2079, f101 carrier 560×539×1712) instead of
piling at the origin. Turret placement (shared eNNN gun models on many GN_*Gun*
frames) still pending the per-mount recipe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Project Sylpheed never stores a capital ship as a single mesh. Each ship is
modelled, then split into destructible parts (hull segments, bridge, engines,
integral turrets), each shipped as its own XBG7 resource inside a
hidden/resource3d/Stage_SNN.xpr container. The split lets the game hide/replace
a part when the player destroys it. Crucially each part keeps its ship-local
position, so reconstructing the whole ship is: draw every base part of one id
together, untransformed.
New `ship` module: Faction (from the e/f/n id prefix — ADAN/TCAF/Neutral),
ShipModel, ship_id_of, is_base_part (skips _l/_m/_d LODs, _bNN/_dead/_break
destruction geometry, e_rou_ scene nodes), and ships_in_container to group a
stage's resources into whole ships.
Supporting:
- mesh::xbg7_resource_names — list a container's XBG7 resource names (directory
walk only, no geometry decode).
- Xbg7Model::models_named — decode only the named resources (assemble a ship
from its ~8 parts instead of the whole ~300-resource stage).
- game_data::Vessel gains `model` (the rou_<id> hull stem) to link stats to the
3D part family.
Tests: id/part classification + a disc-gated reconstruction of the ADAN e108
frigate from Stage_S02.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>