ship: STATIC assembly is now EXACT — node-TRS off-by-one cracked via the capture
The runtime capture served its true purpose: as the answer key that exposed the static encoding. The XBG7 node joint table (node+0x44) holds NINE keyframe channels [TX TY TZ RY RX RZ SX SY SZ] behind EIGHT pointer slots shifted one track forward: channel k+1 = f64@(ptr[k]+8), and channel 0 -- TX, which no pointer names -- sits at ptr[0]-0x48 (tracks are 0x50-byte records, value at +8). The old 8-slot read took TY as X, RY as Y (usually 0 -- why both hulls stacked on the centreline) and never saw TX at all (the +-264 hull offsets). Euler order is Ry(ch3)*Rx(ch4)*Rz(ch5) with angles applied directly, pinned by the captured engine nacelle Rx(-15)*Rz(30) decomposition. mesh.rs gains read_trs9/node_rotation; scene_world_nodes and node_transforms both fixed (saber fin overrides unaffected). assemble_ship rewritten fully static and exact: - every composite-node instance placed (alias duplicates collapsed, real instances kept) including cross-id turret mounts (rou_e303_wep_01_root x2) - the e_rou_<id>_eng cluster rig mounts at GN_Engine_01 (two mirrored nacelles +-131 with -+30-degree cant + centre engine) -- world = frame o rig_local - brg/sld at their exact GN frames (frames were always exact) - shared-geometry twin pairs at +-TX: X-reflect the instance whose offset opposes the geometry's dominant side (viewer reverses winding on det<0) - squeezed node names resolve (wep02 -> wep_02_01) Ground-truth gate: ship::tests::static_assembly_matches_runtime_capture asserts static == the baked e106 capture for all 8 parts (T<1.0, R<0.02) plus both nacelles, both turrets, and the starboard-hull mirror. Viewer now uses pure static assembly; ship_capture + the baked table remain as the verification oracle. Renders show a coherent, bilaterally-symmetric destroyer. 81 formats-lib + all disc tests green; viewer builds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
89
crates/sylpheed-formats/examples/node_dump.rs
Normal file
89
crates/sylpheed-formats/examples/node_dump.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
//! RE diagnostic: raw-dump a composite scene graph's node records — name, table
|
||||
//! pointers, and the FULL joint-table slot values (not just the 8 the decoder
|
||||
//! currently reads) — to derive the true TRS slot layout against captured
|
||||
//! ground truth.
|
||||
//! cargo run --release --example node_dump -- <Stage.xpr path> <composite name> [slots]
|
||||
use sylpheed_formats::mesh::xbg7_descriptor_range;
|
||||
|
||||
fn be32(d: &[u8], o: usize) -> u32 {
|
||||
if o + 4 > d.len() {
|
||||
return 0;
|
||||
}
|
||||
u32::from_be_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]])
|
||||
}
|
||||
fn be_f64(d: &[u8], o: usize) -> f64 {
|
||||
if o + 8 > d.len() {
|
||||
return f64::NAN;
|
||||
}
|
||||
f64::from_be_bytes(d[o..o + 8].try_into().unwrap())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let bytes = std::fs::read(&a[1]).unwrap();
|
||||
let comp = &a[2];
|
||||
let nslots: usize = a.get(3).and_then(|s| s.parse().ok()).unwrap_or(12);
|
||||
let (desc, desc_end) = xbg7_descriptor_range(&bytes, comp).expect("composite found");
|
||||
let d = &bytes[desc..desc_end];
|
||||
println!("descriptor [{desc:#x}..{desc_end:#x}) len {:#x}", d.len());
|
||||
|
||||
let mut i = 0usize;
|
||||
while i + 3 <= d.len() {
|
||||
let starts = (i + 4 <= d.len() && &d[i..i + 4] == b"rou_")
|
||||
|| (i + 3 <= d.len() && &d[i..i + 3] == b"GN_");
|
||||
if !starts {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let mut j = i;
|
||||
while j < d.len() && d[j] != 0 && d[j].is_ascii_graphic() {
|
||||
j += 1;
|
||||
}
|
||||
let name = std::str::from_utf8(&d[i..j]).unwrap_or("?").to_string();
|
||||
if name.ends_with("_col") || name.ends_with("_spc") || name.ends_with("_gls") || name.ends_with("_lum") {
|
||||
i = j.max(i + 1);
|
||||
continue;
|
||||
}
|
||||
// find the 0xFFFFFFFF end marker
|
||||
let mut ff = i;
|
||||
let scan_end = (i + 0x90).min(d.len().saturating_sub(4));
|
||||
while ff < scan_end && be32(d, ff) != 0xFFFF_FFFF {
|
||||
ff += 4;
|
||||
}
|
||||
if ff >= scan_end || ff < 0x10 {
|
||||
i = j.max(i + 1);
|
||||
continue;
|
||||
}
|
||||
let t44 = be32(d, ff - 0x10) as usize;
|
||||
let t48 = be32(d, ff - 0x0C) as usize;
|
||||
let child = be32(d, ff - 0x08) as usize;
|
||||
let sib = be32(d, ff - 0x04) as usize;
|
||||
// Dump the record's u32s between name end and marker for structure context.
|
||||
let rec_u32: Vec<String> = ((j + 1)..ff)
|
||||
.step_by(4)
|
||||
.map(|o| format!("{:08X}", be32(d, o)))
|
||||
.collect();
|
||||
println!("\nNODE {name} @{i:#x} ff@{ff:#x} t44={t44:#x} t48={t48:#x} child={child:#x} sib={sib:#x}");
|
||||
println!(" rec: {}", rec_u32.join(" "));
|
||||
if t44 != 0 && t44 < d.len() {
|
||||
// Joint table: nslots pointer slots. Each slot record appears to be a
|
||||
// keyframe track: a value ALSO sits 0x48 BEFORE the pointed address
|
||||
// (the rest-pose key A), while the decoder currently reads ptr+8 (B).
|
||||
for k in 0..nslots {
|
||||
let p = be32(d, t44 + k * 4) as usize;
|
||||
if p == 0 || p + 24 > d.len() {
|
||||
continue;
|
||||
}
|
||||
let a = if p >= 0x48 { be_f64(d, p - 0x48) } else { f64::NAN };
|
||||
let b = be_f64(d, p + 8);
|
||||
let head = if p >= 0x50 { be32(d, p - 0x50) } else { 0 };
|
||||
let t_a = if p >= 0x50 { be_f64(d, p - 0x50) } else { f64::NAN };
|
||||
let t_b = be_f64(d, p);
|
||||
println!(
|
||||
" slot{k:2} @{p:#x} head={head:08X}: A={a:12.5} (t={t_a:8.3}) B={b:12.5} (t={t_b:8.3})"
|
||||
);
|
||||
}
|
||||
}
|
||||
i = j.max(i + 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user