[formats] XBG7 meshes: index buffers are triangle STRIPS, not lists
The weapon/prop models (rou_fxxx_wep_nn.xpr) rendered with triangular holes in the hull and misplaced/extra spikes. Root cause: XBG7 sub-mesh index buffers are triangle STRIPS, but the decoder read them as triangle LISTS — a strip of N indices is N-2 triangles, a list is only N/3, so ~2/3 of every hull was missing (the holes) and each list-triple of strip data spanned unrelated vertices (the spikes). The tell: triangles-per-vertex was 0.5-1.0 with 0 unreferenced vertices (a closed surface needs ~2.0). - mesh.rs: expand_triangle_strip() converts strip -> list with alternating winding, skipping degenerate triangles (repeated index = strip restart). Applied in both from_xpr2 (weapons) and read_pool_mesh (stage sub-models). All 71 weapon submeshes now have healthy ratios; hulls fill in (wep_03 cannon, wep_04 pod, wep_11, wep_37 verified coherent). - Module doc updated (strip, not list). - sylpheed-cli: `mesh info` reports per-submesh degenerate/unref/spanning-triangle counts; `mesh render` gains --dist (camera zoom). XMESHDBG env dumps the descriptor index markers. Known residual: an index buffer may concatenate several strips with no degenerate bridge, leaving ~2 spanning "spike" triangles at each restart (index jump to a new vertex region) — <1% of tris on most models. Decoding the restart mechanism is a follow-up (a blanket spanning-filter is unsafe: clean models have legit elongated tip triangles). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,12 +21,20 @@
|
||||
//! the data section is a straight sequence of sub-meshes, each:
|
||||
//!
|
||||
//! ```text
|
||||
//! [ index buffer : idx_count × u16 big-endian ] triangle list
|
||||
//! [ index buffer : idx_count × u16 big-endian ] triangle STRIP
|
||||
//! [ 12-byte vertex-buffer header (contents undecoded) ]
|
||||
//! [ vertex buffer : vtx_count × stride bytes ] (declaration-driven)
|
||||
//! (pad to 16 bytes → next sub-mesh)
|
||||
//! ```
|
||||
//!
|
||||
//! The index buffer is a triangle **strip** (a strip of N indices is N-2
|
||||
//! triangles with alternating winding), NOT a list — reading it as a list
|
||||
//! dropped ~⅔ of every hull (the "triangular holes") and produced spanning
|
||||
//! spikes. `expand_triangle_strip` converts it, skipping degenerate triangles.
|
||||
//! Residual: an index buffer may concatenate several strips with no degenerate
|
||||
//! bridge, leaving ~2 spanning triangles at each restart (an index jump to a new
|
||||
//! vertex region) — a small known artifact on some models.
|
||||
//!
|
||||
//! The vertex layout is **not fixed** — it comes from a **vertex declaration**
|
||||
//! in the descriptor: a table of `{offset, format-code, usage}` triples (usage
|
||||
//! `0x00` POSITION `f32×3`, `0x03` NORMAL `f16×4`, `0x05` TEXCOORD `f16×2`).
|
||||
@@ -145,6 +153,19 @@ impl Xbg7Model {
|
||||
return Err(MeshError::UnsupportedLayout);
|
||||
}
|
||||
|
||||
if std::env::var("XMESHDBG").is_ok() {
|
||||
let d = &bytes[desc..desc_end];
|
||||
eprintln!("[{name}] stride={} records={records:?}", decl.stride);
|
||||
let mut rel = 0usize;
|
||||
while rel + 8 <= d.len() {
|
||||
let (a, c) = (be32(d, rel), be32(d, rel + 4));
|
||||
if c >= 3 && c % 3 == 0 && c < 400_000 && a == c * 2 {
|
||||
eprintln!(" idx-marker @0x{rel:03x}: idx_count={c}");
|
||||
}
|
||||
rel += 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Carve the data section sequentially.
|
||||
let base = header.header_size as usize;
|
||||
let mut off = 0usize; // relative to `base`
|
||||
@@ -166,15 +187,21 @@ impl Xbg7Model {
|
||||
return Err(MeshError::UnsupportedLayout);
|
||||
}
|
||||
|
||||
// Indices (u16 BE), validated against the sub-mesh vertex count.
|
||||
let mut indices = Vec::with_capacity(idx_count);
|
||||
// The index buffer is a triangle STRIP (u16 BE), validated against the
|
||||
// sub-mesh vertex count. Reading it as a triangle *list* dropped ~⅔ of
|
||||
// the surface (the classic "triangular holes in the hull") and produced
|
||||
// spanning spikes; a strip of N indices is N-2 triangles with
|
||||
// alternating winding. Expand to a triangle list here, skipping
|
||||
// degenerate triangles (repeated index = strip restart / zero area).
|
||||
let mut strip = Vec::with_capacity(idx_count);
|
||||
for k in 0..idx_count {
|
||||
let i = be16(bytes, ib + k * 2) as u32;
|
||||
if i >= vtx_count as u32 {
|
||||
return Err(MeshError::UnsupportedLayout);
|
||||
}
|
||||
indices.push(i);
|
||||
strip.push(i);
|
||||
}
|
||||
let indices = expand_triangle_strip(&strip);
|
||||
|
||||
// Vertices per the declaration. The .xpr stores each element in
|
||||
// naive big-endian component order (f32 / f16 read at consecutive
|
||||
@@ -531,9 +558,10 @@ fn read_pool_mesh(
|
||||
decl: &VertexDecl,
|
||||
) -> GameMesh {
|
||||
let stride = decl.stride;
|
||||
let indices: Vec<u32> = (0..index_count)
|
||||
let strip: Vec<u32> = (0..index_count)
|
||||
.map(|k| be16(bytes, ib + k * 2) as u32)
|
||||
.collect();
|
||||
let indices = expand_triangle_strip(&strip);
|
||||
|
||||
let mut positions = Vec::with_capacity(vtx_count);
|
||||
let mut normals = Vec::with_capacity(vtx_count);
|
||||
@@ -713,6 +741,25 @@ fn submesh_records(desc: &[u8]) -> Vec<(usize, usize)> {
|
||||
fn align16(x: usize) -> usize {
|
||||
(x + 15) & !15
|
||||
}
|
||||
|
||||
/// Expand a triangle-**strip** index buffer into a triangle **list**, dropping
|
||||
/// degenerate triangles (a repeated index marks a strip restart / zero-area
|
||||
/// triangle). XBG7 sub-mesh index buffers are strips: reading them as lists
|
||||
/// dropped ~⅔ of every hull (triangular holes) and produced spanning spikes.
|
||||
fn expand_triangle_strip(strip: &[u32]) -> Vec<u32> {
|
||||
let mut out = Vec::with_capacity(strip.len().saturating_sub(2) * 3);
|
||||
for w in 0..strip.len().saturating_sub(2) {
|
||||
let (a, b, c) = if w % 2 == 0 {
|
||||
(strip[w], strip[w + 1], strip[w + 2])
|
||||
} else {
|
||||
(strip[w + 1], strip[w], strip[w + 2])
|
||||
};
|
||||
if a != b && b != c && a != c {
|
||||
out.extend_from_slice(&[a, b, c]);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
#[inline]
|
||||
fn be16(b: &[u8], o: usize) -> u16 {
|
||||
u16::from_be_bytes([b[o], b[o + 1]])
|
||||
|
||||
Reference in New Issue
Block a user