ship: diagnostics — baked-table renderer + evidence the placement IS static
ship_render example: assemble a ship from the baked capture table (or --static) and write orthographic top/side PPM renders + per-part world bounds — the offline eye for placement bugs. Findings recorded (docs to follow with the fix): - e106 baked eng_01 is CROSS-INSTANCE contamination: the F10 capture de-dups by vertex-buffer address alone, so for a part drawn by several fleet ships only the FIRST instance's transform survives; eng_01's 30-degree rotation and below-hull position belong to a different destroyer. Fleet formation made it reproduce across captures, defeating the cross-validation. - The static composite DOES carry the exact placement: BE-f64 +-264.0 (the lateral hull pair offset the static assembler loses) and -164.044 (the bridge Z we captured as -164.037) sit in e_rou_e106's node joint tables. The static decode is incomplete, not the data — full static assembly is achievable, with captures demoted to a verification oracle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
125
crates/sylpheed-formats/examples/ship_render.rs
Normal file
125
crates/sylpheed-formats/examples/ship_render.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
//! 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]
|
||||
);
|
||||
}
|
||||
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}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user