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>
142 lines
6.1 KiB
Rust
142 lines
6.1 KiB
Rust
//! Diagnostic: assemble a ship from the BAKED capture table (or the static
|
||
//! scene graph with --static) and render orthographic PNGs + print per-part
|
||
//! world bounds, to see exactly what the viewer shows.
|
||
//! cargo run --release --example ship_render -- <Stage_SNN.xpr path> <ship_id> <out_prefix> [--static]
|
||
use std::collections::HashSet;
|
||
use sylpheed_formats::mesh::{ScenePart, Xbg7Model};
|
||
use sylpheed_formats::ship::assemble_ship;
|
||
use sylpheed_formats::ship_capture::{embedded_placement, to_scene_parts};
|
||
|
||
fn main() {
|
||
let a: Vec<String> = std::env::args().collect();
|
||
let use_static = a.iter().any(|s| s == "--static");
|
||
let (path, id, out) = (&a[1], &a[2], &a[3]);
|
||
let bytes = std::fs::read(path).unwrap();
|
||
|
||
let placed: Vec<ScenePart> = if use_static {
|
||
assemble_ship(&bytes, id, true)
|
||
} else {
|
||
let cap = embedded_placement(id).expect("ship not in baked table");
|
||
to_scene_parts(&cap)
|
||
};
|
||
let want: HashSet<String> = placed.iter().map(|p| p.resource.clone()).collect();
|
||
let models = Xbg7Model::models_named(&bytes, &want, &|| false);
|
||
|
||
// World triangles + per-part bounds.
|
||
let mut tris: Vec<[[f32; 3]; 3]> = Vec::new();
|
||
for p in &placed {
|
||
let Some(m) = models.iter().find(|m| m.name == p.resource) else { continue };
|
||
let mut lo = [f32::MAX; 3];
|
||
let mut hi = [f32::MIN; 3];
|
||
for sub in &m.meshes {
|
||
let w: Vec<[f32; 3]> = sub.positions.iter().map(|v| p.apply(*v)).collect();
|
||
for v in &w {
|
||
for k in 0..3 {
|
||
lo[k] = lo[k].min(v[k]);
|
||
hi[k] = hi[k].max(v[k]);
|
||
}
|
||
}
|
||
for t in sub.indices.chunks_exact(3) {
|
||
tris.push([w[t[0] as usize], w[t[1] as usize], w[t[2] as usize]]);
|
||
}
|
||
}
|
||
println!(
|
||
"{:20} X[{:8.1},{:8.1}] Y[{:8.1},{:8.1}] Z[{:8.1},{:8.1}]",
|
||
p.resource, lo[0], hi[0], lo[1], hi[1], lo[2], hi[2]
|
||
);
|
||
}
|
||
// Exhaust cones at the GN_Jet/GN_SJet frames (the game's visible thrusters).
|
||
for f in sylpheed_formats::ship::exhaust_frames(&bytes, id) {
|
||
const SEG: usize = 12;
|
||
let (r, len) = (22.0f32, 140.0f32);
|
||
let ring: Vec<[f32; 3]> = (0..SEG)
|
||
.map(|s| {
|
||
let a = s as f32 / SEG as f32 * std::f32::consts::TAU;
|
||
f.apply([a.cos() * r, a.sin() * r, 0.0])
|
||
})
|
||
.collect();
|
||
let apex = f.apply([0.0, 0.0, len]);
|
||
for s in 0..SEG {
|
||
tris.push([apex, ring[(s + 1) % SEG], ring[s]]);
|
||
}
|
||
println!(" exhaust frame {:16} T=[{:8.1}{:8.1}{:8.1}]", f.resource, f.t[0], f.t[1], f.t[2]);
|
||
}
|
||
println!("total {} tris from {} placements", tris.len(), placed.len());
|
||
|
||
// Orthographic z-buffered flat renders: top (X/Z, look down −Y) and side (Z/Y, look down −X).
|
||
for (name, ax, ay, az) in [("top", 0usize, 2usize, 1usize), ("side", 2usize, 1usize, 0usize)] {
|
||
let size = 900usize;
|
||
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
|
||
for t in &tris {
|
||
for v in t {
|
||
for k in 0..3 {
|
||
lo[k] = lo[k].min(v[k]);
|
||
hi[k] = hi[k].max(v[k]);
|
||
}
|
||
}
|
||
}
|
||
let cx = (lo[ax] + hi[ax]) * 0.5;
|
||
let cy = (lo[ay] + hi[ay]) * 0.5;
|
||
let ext = (hi[ax] - lo[ax]).max(hi[ay] - lo[ay]) * 0.55;
|
||
let scale = (size as f32 * 0.5) / ext;
|
||
let mut img = vec![0u8; size * size * 3];
|
||
let mut zbuf = vec![f32::MIN; size * size];
|
||
for t in &tris {
|
||
// screen coords
|
||
let p: Vec<[f32; 3]> = t
|
||
.iter()
|
||
.map(|v| {
|
||
[
|
||
(v[ax] - cx) * scale + size as f32 * 0.5,
|
||
(cy - v[ay]) * scale + size as f32 * 0.5,
|
||
v[az],
|
||
]
|
||
})
|
||
.collect();
|
||
// flat shade by triangle normal Z-ish
|
||
let e1 = [t[1][0] - t[0][0], t[1][1] - t[0][1], t[1][2] - t[0][2]];
|
||
let e2 = [t[2][0] - t[0][0], t[2][1] - t[0][1], t[2][2] - t[0][2]];
|
||
let n = [
|
||
e1[1] * e2[2] - e1[2] * e2[1],
|
||
e1[2] * e2[0] - e1[0] * e2[2],
|
||
e1[0] * e2[1] - e1[1] * e2[0],
|
||
];
|
||
let nl = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt().max(1e-6);
|
||
let lum = (n[az].abs() / nl * 200.0 + 40.0) as u8;
|
||
// bbox raster
|
||
let minx = p.iter().map(|v| v[0]).fold(f32::MAX, f32::min).max(0.0) as usize;
|
||
let maxx = (p.iter().map(|v| v[0]).fold(f32::MIN, f32::max).min(size as f32 - 1.0)) as usize;
|
||
let miny = p.iter().map(|v| v[1]).fold(f32::MAX, f32::min).max(0.0) as usize;
|
||
let maxy = (p.iter().map(|v| v[1]).fold(f32::MIN, f32::max).min(size as f32 - 1.0)) as usize;
|
||
let det = (p[1][0] - p[0][0]) * (p[2][1] - p[0][1]) - (p[2][0] - p[0][0]) * (p[1][1] - p[0][1]);
|
||
if det.abs() < 1e-6 {
|
||
continue;
|
||
}
|
||
for y in miny..=maxy {
|
||
for x in minx..=maxx {
|
||
let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5);
|
||
let w0 = ((p[1][0] - fx) * (p[2][1] - fy) - (p[2][0] - fx) * (p[1][1] - fy)) / det;
|
||
let w1 = ((p[2][0] - fx) * (p[0][1] - fy) - (p[0][0] - fx) * (p[2][1] - fy)) / det;
|
||
let w2 = 1.0 - w0 - w1;
|
||
if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
|
||
continue;
|
||
}
|
||
let z = w0 * p[0][2] + w1 * p[1][2] + w2 * p[2][2];
|
||
let idx = y * size + x;
|
||
if z > zbuf[idx] {
|
||
zbuf[idx] = z;
|
||
img[idx * 3] = lum;
|
||
img[idx * 3 + 1] = lum;
|
||
img[idx * 3 + 2] = lum;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let file = format!("{out}_{name}.ppm");
|
||
let mut ppm = format!("P6\n{size} {size}\n255\n").into_bytes();
|
||
ppm.extend_from_slice(&img);
|
||
std::fs::write(&file, ppm).unwrap();
|
||
println!("wrote {file}");
|
||
}
|
||
}
|