ship: assemble capital ships from the composite scene graph (real placement)

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>
This commit is contained in:
MechaCat02
2026-07-23 21:44:25 +02:00
parent 720a5fec27
commit a8b5691d1c
4 changed files with 354 additions and 17 deletions

View File

@@ -1593,7 +1593,7 @@ fn draw_ships_ui(
let ShipBrowser { rows, filter, faction, selected, .. } = &mut *ships;
let needle = filter.to_lowercase();
let mut to_render: Option<(String, String, Vec<String>, String)> = None;
let mut to_render: Option<(String, String, String)> = None;
egui::ScrollArea::vertical().show(ui, |ui| {
let mut last_section = String::new();
@@ -1667,7 +1667,6 @@ fn draw_ships_ui(
to_render = Some((
row.id.clone(),
row.stage_file.clone(),
row.parts.clone(),
format!("{} ({})", row.name, row.id),
));
}
@@ -1675,9 +1674,10 @@ fn draw_ships_ui(
}
});
if let Some((id, file, parts, label)) = to_render {
if let Some((id, file, label)) = to_render {
let id2 = id.clone();
*selected = Some(id);
render.send(RequestShipRender { file, parts, label });
render.send(RequestShipRender { file, id: id2, label });
}
});
ships.open &= open;