ship: apply scale + gate approximate external parts behind a toggle

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>
This commit is contained in:
MechaCat02
2026-07-23 22:11:47 +02:00
parent 8c8e2dbeb4
commit 79c78cc7e1
4 changed files with 72 additions and 46 deletions

View File

@@ -1849,15 +1849,19 @@ pub struct ScenePart {
pub m: [[f32; 3]; 3],
/// World translation, `(X, up→Y, fore/aft→Z)`.
pub t: [f32; 3],
/// Per-axis local scale (a shield generator ships at 0.5, a jet FX at 2.4).
pub s: [f32; 3],
}
impl ScenePart {
/// Apply the world transform to a local vertex.
/// Apply the world transform to a local vertex: `R·(S·v) + T` (scale is a
/// leaf-local factor; the composite's structural nodes are all unit-scale).
pub fn apply(&self, v: [f32; 3]) -> [f32; 3] {
let sv = [v[0] * self.s[0], v[1] * self.s[1], v[2] * self.s[2]];
[
self.m[0][0] * v[0] + self.m[0][1] * v[1] + self.m[0][2] * v[2] + self.t[0],
self.m[1][0] * v[0] + self.m[1][1] * v[1] + self.m[1][2] * v[2] + self.t[1],
self.m[2][0] * v[0] + self.m[2][1] * v[1] + self.m[2][2] * v[2] + self.t[2],
self.m[0][0] * sv[0] + self.m[0][1] * sv[1] + self.m[0][2] * sv[2] + self.t[0],
self.m[1][0] * sv[0] + self.m[1][1] * sv[1] + self.m[1][2] * sv[2] + self.t[1],
self.m[2][0] * sv[0] + self.m[2][1] * sv[1] + self.m[2][2] * sv[2] + self.t[2],
]
}
}
@@ -1916,6 +1920,7 @@ pub fn scene_world_nodes(bytes: &[u8], composite_name: &str) -> Vec<ScenePart> {
name: String,
local_m: M3,
local_t: [f32; 3],
local_s: [f32; 3],
sib_end: Option<usize>,
}
let mut recs: Vec<Rec> = Vec::new();
@@ -1939,15 +1944,18 @@ pub fn scene_world_nodes(bytes: &[u8], composite_name: &str) -> Vec<ScenePart> {
let trs = if t44 != 0 && t44 + 32 <= d.len() {
read_trs(t44)
} else {
[0.0; 8]
[0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0]
};
let local_t = [trs[0] as f32, trs[2] as f32, trs[1] as f32];
let local_m = m3_mul(rot_z(trs[4] as f32), rot_x(trs[3] as f32));
// Slots 57 are per-axis scale (a hardpoint part ships at e.g. 0.5).
let sc = |v: f64| if v.abs() < 1e-6 { 1.0 } else { v as f32 };
let local_s = [sc(trs[5]), sc(trs[6]), sc(trs[7])];
// Next sibling begins 4 bytes before its pointer, at a `rou_`/`GN_` name.
let sib_end = sib_ptr.checked_sub(4).filter(|&s| {
s > i && s + 4 <= head_end && (&d[s..s + 4] == b"rou_" || &d[s..s + 3] == b"GN_")
});
recs.push(Rec { name_start: i, name, local_m, local_t, sib_end });
recs.push(Rec { name_start: i, name, local_m, local_t, local_s, sib_end });
i = j.max(i + 1);
}
@@ -1962,7 +1970,7 @@ pub fn scene_world_nodes(bytes: &[u8], composite_name: &str) -> Vec<ScenePart> {
let wm = m3_mul(pm, rec.local_m);
let r = m3_vec(pm, rec.local_t);
let wt = [r[0] + pt[0], r[1] + pt[1], r[2] + pt[2]];
out.push(ScenePart { resource: rec.name.clone(), m: wm, t: wt });
out.push(ScenePart { resource: rec.name.clone(), m: wm, t: wt, s: rec.local_s });
let end = rec
.sib_end
.unwrap_or_else(|| stack.last().map(|s| s.0).unwrap_or(head_end));