ship: engine placement CONFIRMED by 43-instance capture; exhaust markers added
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>
This commit is contained in:
@@ -3646,6 +3646,40 @@ fn handle_voice_request(
|
||||
});
|
||||
}
|
||||
|
||||
/// A simple exhaust-plume cone: base ring at the frame origin opening toward
|
||||
/// **−Z** (the jet frames face aft), apex trailing behind. Stands in for the
|
||||
/// game's engine-exhaust FX so assembled ships visually read as powered.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn exhaust_cone_mesh() -> sylpheed_formats::mesh::GameMesh {
|
||||
const SEG: usize = 12;
|
||||
const R: f32 = 22.0;
|
||||
const LEN: f32 = 140.0;
|
||||
let mut positions = Vec::with_capacity(SEG + 2);
|
||||
let mut normals = Vec::with_capacity(SEG + 2);
|
||||
positions.push([0.0, 0.0, LEN]); // apex — the jet frame flips Z, sending it aft
|
||||
normals.push([0.0, 0.0, -1.0]);
|
||||
for s in 0..SEG {
|
||||
let a = s as f32 / SEG as f32 * std::f32::consts::TAU;
|
||||
positions.push([a.cos() * R, a.sin() * R, 0.0]);
|
||||
normals.push([a.cos(), a.sin(), 0.3]);
|
||||
}
|
||||
positions.push([0.0, 0.0, 0.0]); // base centre (cap)
|
||||
normals.push([0.0, 0.0, 1.0]);
|
||||
let mut indices = Vec::new();
|
||||
for s in 0..SEG as u32 {
|
||||
let (a, b) = (1 + s, 1 + (s + 1) % SEG as u32);
|
||||
indices.extend_from_slice(&[0, b, a]); // side
|
||||
indices.extend_from_slice(&[(SEG + 1) as u32, a, b]); // cap
|
||||
}
|
||||
sylpheed_formats::mesh::GameMesh {
|
||||
positions,
|
||||
normals,
|
||||
uvs: Vec::new(),
|
||||
indices,
|
||||
name: Some("exhaust".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Duration (seconds) of a PCM WAV from its `fmt`/`data` chunks.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn wav_duration(path: &Path) -> Option<f32> {
|
||||
@@ -4033,6 +4067,26 @@ fn build_ship_model(
|
||||
return PreparedXpr::Info(format!("No geometry decoded for {label}."));
|
||||
}
|
||||
|
||||
// Exhaust markers: the game's visible "thrusters" are FX drawn at the
|
||||
// GN_Jet/GN_SJet frames (the engine GEOMETRY is recessed in the hull —
|
||||
// capture-verified). Draw a simple cone at each frame so the assembled ship
|
||||
// reads correctly.
|
||||
if external {
|
||||
for (i, f) in sylpheed_formats::ship::exhaust_frames(&bytes, id).iter().enumerate() {
|
||||
let mut cone = exhaust_cone_mesh();
|
||||
for v in &mut cone.positions {
|
||||
*v = f.apply(*v);
|
||||
}
|
||||
for nrm in &mut cone.normals {
|
||||
*nrm = rot(&f.m, nrm);
|
||||
}
|
||||
models.push(Xbg7Model {
|
||||
name: format!("__exhaust_{i}"),
|
||||
meshes: vec![cone],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble, then relabel with the ship's display name.
|
||||
match prepare_ship(&bytes, &models) {
|
||||
PreparedXpr::Model {
|
||||
|
||||
Reference in New Issue
Block a user