[formats,viewer] Decode grouped-pool XBG7 models; fix mesh routing + albedo
Revert the mis-guided "triangle STRIP" reading (a937779): XBG7 index
buffers are triangle LISTS (prim=4 in the GPU capture; stored-normal
agreement 1.000 as a list vs ~0.49 as a strip). Reinstate the
winding-consistency gate.
Crack the grouped-pool layout used by the hero ship and ~150 detailed
models. A resource's sub-meshes share one index pool (buffers 4-byte
aligned, in descriptor-marker order) followed by one vertex pool (each
vtx_count*stride, same order); the vertex pool is 4-byte aligned after
the index pool. The whole resource is derived from one anchored pivot:
ib0 = vb0 - span - pad (pad in 0..=3, alignment)
ib[i] = align4(ib[i-1] + idx_count[i-1]*2)
vb[i] = vb[i-1] + vtx_count[i-1]*stride
vb0 is found via the unit-normal vertex-run scan; the alignment is
confirmed by validating the LARGEST sub-mesh (most reliable), after
which the rest are read/validated. A single marker reduces this to the
existing adjacency anchor, so grouped generalises it.
Results (cross-checked against a Canary GPU draw-log capture of
DeltaSaber_T.xpr):
- DeltaSaber_T f001 = body + 7 detail parts = 8650 tris, every sub-mesh
0-degenerate / full-coverage / winding-agreement 1.000.
- All 19 previously-declined weapon models now decode (they hit pad 2
and/or lead with a tiny bracket that broke a markers[0] pivot). Corpus
audit: 0/146 geometry files fail (was 19 -> flat-texture in the viewer).
- Stages unchanged (single-marker path is byte-identical; grouped falls
back to the old anchor on failure). 7/7 disc tests green incl. the
strict stage quality audit; new hero_ship_grouped_pool_decodes test.
Viewer: route by count_xbg7; --only matches an exact model name (so the
neutral pose renders without the mnv*/turn180 animation poses). Fix the
albedo matcher: match a sub-model to its _col map by entity stem
(e007_bdy_01 -> e007_col) instead of a full-name prefix, lifting stage
sub-model texturing from ~25% to ~95% (the rest were flat grey). Colour
correctness (channel order/sRGB) remains a separate dynamic-RE item.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -423,17 +423,30 @@ fn cmd_texture_export(file: &Path, output: &Path) -> Result<()> {
|
||||
/// a single model (weapon / prop) OR a stage's many sub-models — whichever
|
||||
/// yields more geometry.
|
||||
fn decode_models(bytes: &[u8]) -> Vec<sylpheed_formats::mesh::Xbg7Model> {
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
let single = Xbg7Model::from_xpr2(bytes)
|
||||
use sylpheed_formats::mesh::{count_xbg7, Xbg7Model};
|
||||
// Route by container kind, NOT by whichever decoder yields more verts (that
|
||||
// old heuristic let `stage_models`' content-anchoring win on single-model
|
||||
// files, fabricating phantom / duplicate / mis-anchored blocks). A file with
|
||||
// one XBG7 resource is a single model (weapon / prop) → use only the
|
||||
// validated records-based list decode; content-anchoring a single-model file
|
||||
// invents geometry. Many XBG7 resources → a Stage_* collection → anchor them.
|
||||
if count_xbg7(bytes) > 1 {
|
||||
// Multi-resource Stage_* collection → content-anchor every resource
|
||||
// (each anchor now gated by stored-normal agreement).
|
||||
Xbg7Model::stage_models(bytes)
|
||||
} else if let Some(m) = Xbg7Model::from_xpr2(bytes)
|
||||
.ok()
|
||||
.filter(|m| !m.meshes.is_empty());
|
||||
let single_verts = single.as_ref().map(|m| m.totals().0).unwrap_or(0);
|
||||
let stage = Xbg7Model::stage_models(bytes);
|
||||
let stage_verts: usize = stage.iter().map(|m| m.totals().0).sum();
|
||||
if !stage.is_empty() && stage_verts > single_verts {
|
||||
stage
|
||||
.filter(|m| !m.meshes.is_empty())
|
||||
{
|
||||
// Single model the records-based list decode carved (authoritative).
|
||||
vec![m]
|
||||
} else {
|
||||
single.into_iter().collect()
|
||||
// Single model from_xpr2 couldn't locate (its sequential carve missed
|
||||
// the block) — fall back to content-anchoring, which finds it by shape.
|
||||
// Use a STRICT winding-consistency gate (0.85): on a single-model file a
|
||||
// mis-anchor is an obvious phantom / spike-mess and must be declined,
|
||||
// unlike the large stage corpus which keeps the ungated path.
|
||||
Xbg7Model::anchor_models(bytes, 0.85)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,7 +549,14 @@ fn cmd_mesh_render(
|
||||
let bytes = std::fs::read(file).with_context(|| format!("Cannot read {}", file.display()))?;
|
||||
let mut models = decode_models(&bytes);
|
||||
if let Some(sub) = &only {
|
||||
models.retain(|m| m.name.contains(sub.as_str()));
|
||||
// Prefer an exact name match (e.g. `f001` for the neutral ship pose,
|
||||
// excluding the `_rou_f001_mnv*` animation poses that also *contain*
|
||||
// "f001"); fall back to substring when nothing matches exactly.
|
||||
if models.iter().any(|m| m.name == *sub) {
|
||||
models.retain(|m| m.name == *sub);
|
||||
} else {
|
||||
models.retain(|m| m.name.contains(sub.as_str()));
|
||||
}
|
||||
}
|
||||
if models.is_empty() {
|
||||
anyhow::bail!("no decodable XBG7 geometry in {}", file.display());
|
||||
|
||||
Reference in New Issue
Block a user