Static external-part placement (ship::assemble_ship) is only approximate — a GN_* frame is the mount pivot, and the real offset lives in a runtime detail rig. So capture ground truth from Canary instead. Finding: the guest vertex buffer holds LOCAL coords (byte-identical to the .xpr), so capital-ship parts are placed entirely in the vertex shader. The per-part transform is in the VS float constants — c0..c2 are the WorldViewProjection rows; normalising each by its norm (= projection x/y/z scales, sy/sx = 16:9) yields the rigid WorldView (rows orthonormal, det +1). Expressing every part relative to a reference part cancels the camera: rel_p = WV_ref⁻¹·WV_p, a pure ship-space rigid transform. - examples/correlate_capture.rs: parse xenia_ship_capture.log, decode the ship's parts, match by vertex count, recover each part's ship-relative placement. - docs/re/ship-placement-runtime-capture.md: full method + the capture protocol (Canary branch capture-ship-placement, F10 hotkey) + validated e106 result. Validated on e106 (ADAN Destroyer, Stage_S01): all 7 drawn parts matched by vertex count; bdy_01/bdy_02 recovered as a PORT/STARBOARD pair (X = ∓264) that static had overlapping; engines correctly aft; extent 872×670×2181. Only brg_01 (bridge) missing — culled at that camera angle, needs a bridge-facing capture. HANDOFF: next = re-capture bridge, bake a per-ship placement table the viewer prefers over assemble_ship, then capture the cruiser / ACROPOLIS / TCAF ships. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.1 KiB
Capital-ship part placement — runtime capture (ground truth)
Status: pipeline working end-to-end for one ship (e106 ADAN Destroyer);
correlator recovers exact ship-relative transforms. Not yet baked into the viewer.
Problem
Capital ships ship as split XBG7 parts (hull bodies, bridge, engines, turrets)
in hidden/resource3d/Stage_SNN.xpr. Reconstructing the whole ship needs each
part's placement. Static reverse-engineering of the composite scene graph
(e_rou_<id>, GN_* frames) got the hull right but external parts only
approximately — a GN_* frame is the mount pivot, and the real outboard /
rotational offset lives in a detail sub-rig or in runtime code not recoverable
statically. See sylpheed-formats::ship::assemble_ship (the static best-effort).
Finding
The guest vertex buffer holds LOCAL coordinates — byte-identical to the
.xpr (verified: e106_bdy_04's first vertex (-20.0000,153.1539,45.9997)
matches the decode exactly). So parts are placed entirely in the vertex
shader; the buffer carries no placement. (Contrast: static stage geometry is
pre-transformed into world space and byte-matches the .xpr at file offsets —
that's a different case.)
The per-part transform is in the vertex-shader float constants. For the ship
shader (vs=0xC7F781F4C1D58054), constants c0..c2 are the WorldViewProjection
matrix rows:
- Their norms are
(sx, sy, 1)= the projection x/y scales;sy/sx ≈ 1.78 = 16:9aspect. Dividing each row by its norm gives the rigid WorldView (verified: normalized rows orthonormal to 1e-4,det = +1). - The camera View cancels when expressing every part relative to a reference part:
rel_p = WV_ref⁻¹ · WV_p=(Rᵀ_ref·R_p , Rᵀ_ref·(T_p − T_ref))— a pure ship-space rigid transform. Applyingrel_pto partp's local geometry assembles the ship exactly, in the reference part's frame.
(c4..c14 also vary per part — likely a normal matrix / a second WVP variant /
WorldView split; not needed, c0..c2 suffice.)
Capture instrumentation
Branch capture-ship-placement in the xenia-canary-native worktree (off the
instrument-current-descended native branch, which already has GPU log_draws).
- F10 in the emulator window →
xe::gpu::RequestShipCaptureFrame(). - Dumps the next ≤8000 draws (de-duped by vertex-buffer address) to
xenia_ship_capture.login the binary's dir (build/bin/Linux/Release/). Per draw:DRAW vbase=0x… stride=… vcount=… indices=… prim=… vs=0x… pos: (x,y,z) … up to 64 LOCAL vertex positions (== .xpr) vsconst base=N: c0=(x,y,z,w) c1=… … first 48 VS float4 constants - Files:
src/xenia/gpu/command_processor.{cc,h}(CaptureShipDrawForRE,RequestShipCaptureFrame),src/xenia/app/emulator_window.cc(F10 key). - Run:
run-canary-native.sh(HW Vulkan, interactive). Play into the mission, frame the ship side-on, press F10.
Correlator
sylpheed-formats/examples/correlate_capture.rs:
SYLPHEED_ISO=… cargo run --release --example correlate_capture -- \
<capture.log> <Stage_SNN> <ship_id> [ref_part_substr]
Parses the log, decodes the ship's base parts, matches each part to a draw by
vertex count (unique per part), recovers WorldView from c0..c2, and prints
each part's ship-relative transform (reference-part frame) + assembled extent.
Validated result — e106 ADAN Destroyer (Mission 1 / Stage_S01, side view)
Vertex-count → part map (all matched exactly): bdy_01/02 = 1261, bdy_03 = 815, bdy_04 = 1633, eng_01 = 583, eng_02 = 491, wep_02_01 = 1002, brg_01 = 202.
Ship-relative placement (reference = bdy_04, aft hull):
| part | T (ship-relative) | note |
|---|---|---|
| bdy_04 | (0, 0, 0) | reference, aft hull |
| eng_01 | (131, −133, −132) | aft (−Z), starboard |
| eng_02 | (0, −49, −140) | aft, centre |
| bdy_01 | (−264, −150, 1165) | port hull, forward |
| bdy_02 | (+264, −150, 1165) | starboard hull, forward |
| bdy_03 | (0, −35, 1076) | forward keel/spine |
| wep_02_01 | (0, 49, 1009) | forward weapon |
Assembled extent 872 × 670 × 2181 (a coherent ~2181-long destroyer).
Key wins vs static: bdy_01/bdy_02 are a port/starboard pair (X = ∓264),
which static had overlapping at one point; engines land correctly aft; the ship is
not the 1894-tall tower the static Y put out.
Missing: brg_01 (bridge, vcount 202) was culled/occluded at that camera
angle (0 draws) — needs a second capture framing the superstructure.
Next steps
- Bridge: re-capture with the bridge visible → fills
brg_01. - Bake: promote the correlator to a module; emit a per-ship placement table
(
ship_id → [(part, R, T)]) as a checked-in data file; have the viewer'sbuild_ship_modelprefer the captured table overassemble_shipwhen present. - Other ships: same capture for the cruiser (
e105, Stage_S02), ACROPOLIS (f101), TCAF cruiser/destroyer (f105/f106). One side-on F10 each. - Turrets: the shared
e3NN/e4NNgun models appear as their own draws in the capture too — correlate them to place turrets (static never resolved these).