[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:
@@ -178,6 +178,77 @@ mission* (where `DeltaSaber` renders) would hand over its exact declaration dire
|
||||
resource by strict index+triangle validation. **5662 sub-models decode across 22 stages** (S07
|
||||
366/378), incl. the previously-declined main bodies `e005` (2566 v) and weapons — ≤2 s on 70 MB.
|
||||
Parser `Xbg7Model::stage_models`, tests `stage_models_{decode,sweep,quality_audit}`.
|
||||
- 2026-07-17 — **triangle-LIST re-confirmed; a strip interlude refuted; winding-consistency gate
|
||||
added.** A 2026-07 change had briefly re-read the index buffers as triangle *strips* (to "fill
|
||||
holes"). Refuted objectively with a new `XVERIFY` diagnostic that compares both readings by
|
||||
**stored-normal agreement** (each triangle's cross-product face normal vs the sum of its vertices'
|
||||
stored normals): the LIST reading gives agreement **1.000** on every clean weapon (`wep_00/03/04/19`
|
||||
= only possible with correct topology + winding), the STRIP reading **~0.49** (random). The strip
|
||||
reading also over-generated ~2.5× the triangles (wep_00: 938 vs 364) — a hole-filling garbage soup.
|
||||
Reverted to LIST in both paths (`from_xpr2`, `read_pool_mesh`), matching the `prim=4` GPU capture.
|
||||
Added an objective **winding-consistency gate** `max(na, 1-na)`: a correct carve is internally
|
||||
consistent (agreement ≈1.0, or ≈0.0 for inverted-but-consistent winding — a real single-sided
|
||||
mesh), a mis-carve scatters to the ≈0.5 middle. `from_xpr2` declines sub-meshes below 0.90 (e.g.
|
||||
`wep_23` na=0.398 → declined instead of a spike-mess); the single-model **content-anchor fallback**
|
||||
gates at 0.85; the large multi-resource **stage** path stays ungated (its enemy meshes span a
|
||||
continuous 0.5–1.0 consistency range — a hard gate there dropped ~48/314 legit S07 blocks).
|
||||
**Routing fixed:** `decode_models` (CLI) and the viewer now route by `count_xbg7` (1 → validated
|
||||
records-based list decode, fallback to strict-gated anchor; >1 → stage anchor) instead of the old
|
||||
"whichever decoder yields more verts" rule — that rule let stage content-anchoring win on
|
||||
single-model weapon files and fabricate **phantom** blocks (a `wep_00` clone appearing inside
|
||||
`wep_19`), duplicates, and spike-mess anchors. Weapons now: 33 clean-decode / 26 declined (declined
|
||||
= genuinely multi-stream or un-carvable, shown as nothing rather than garbage); stage coverage
|
||||
unchanged (S07 314). `expand_triangle_strip` retained as an `XVERIFY`-only diagnostic.
|
||||
- 2026-07-12 — `DeltaSaber_A.xpr` body: data does **not** begin with indices; plain-`f32×3` runs
|
||||
with ship-scale extent (span ≈27–34, matching bbox 30.0) found only at high offsets
|
||||
(`data+0x28634C`, …) → multi-stream, **undecoded**.
|
||||
- 2026-07-18 — **GROUPED-POOL layout cracked → the hero ship (Delta Saber) fully decodes.** The
|
||||
detailed models (`DeltaSaber_*.xpr` + ~100 others) were declined for **location**, not format —
|
||||
their vertex format is the standard stride-24 triangle list. A resource's *several* sub-meshes
|
||||
don't interleave `[idx][vtx]` per block; they share **two grouped pools**: an **index pool**
|
||||
(buffers concatenated in descriptor-marker order, each **4-byte aligned**) followed by a **vertex
|
||||
pool** (each sub-pool `vtx_count × stride`, same order), with the index pool ending **exactly**
|
||||
where the vertex pool begins. So the whole resource pivots on one unknown, the first vertex-pool
|
||||
start `vb0` (= index-pool end, found by the unit-normal vertex-run scan); everything else is
|
||||
derived: `ib0 = vb0 − span`, `ib[i] = align4(ib[i-1] + idx_count[i-1]·2)`,
|
||||
`vb[i] = vb[i-1] + vtx_count[i-1]·stride`. Reversed statically from `DeltaSaber_T.xpr` and
|
||||
**cross-checked against a Canary GPU draw-log capture** (mission ship = `DeltaSaber_T.xpr`, found
|
||||
via the `--log_file_io` kernel hook): `f001` = body (idx@`data+0xC` = 0x5500C, vtx@0x61ACC, 10891 v
|
||||
/ 8187 t) + **7 detail parts** (fins/cockpit/wingtips, markers at descriptor 0x3BEC…0x58FC) =
|
||||
**8650 tris**, and **every sub-mesh decodes at 0 degenerate / full coverage / winding-agreement
|
||||
1.000**. This is the layout the per-block adjacency anchor (`ib = vb − idx_bytes`) rendered as a
|
||||
**spiky phantom** (it read 24561 indices starting 2782 B too late, agree 0.64, 1277 degenerate).
|
||||
Insight: a single index marker reduces the grouped model to `index_end = vb0`, i.e. the existing
|
||||
adjacency `ib = vb − idx_bytes` — so grouped **generalises** the single-block anchor (n=1 is
|
||||
identical). Implemented as `anchor_grouped_meshes` (mesh.rs): `anchor_models` routes resources with
|
||||
>1 index marker to it (validated per sub-mesh; on failure falls back to the old first-marker
|
||||
adjacency anchor so stage coverage never regresses); single-marker stages/props keep the exact
|
||||
prior path. The shared acceptance test is factored into `validate_block` (the connectivity
|
||||
heuristic is relaxed for *derived* grouped parts, which are pinned by in-range + consistency, so
|
||||
small flat fins aren't mis-rejected). Render self-check: `sylpheed-cli mesh render DeltaSaber_T.xpr
|
||||
--only f001` (exact-name match excludes the `_rou_f001_mnv*` animation poses) → clean complete
|
||||
fighter. Test `hero_ship_grouped_pool_decodes`. Colours/UVs still pending the running-game oracle.
|
||||
- 2026-07-18 (refinement) — **4-byte vertex-pool alignment + weapon recovery.** The grouped-pool
|
||||
rule "index pool ends exactly where the vertex pool begins" is really "the vertex pool is **4-byte
|
||||
aligned** after the index pool": `vb0 = align4(ib0 + span)`, so 0..=3 bytes of padding can sit
|
||||
between them. DeltaSaber's index pool ended already-aligned (pad 0), which hid this; **19
|
||||
weapon/`*_hangar` models** (single- and multi-marker: `wep_08/11/34/58/62/69/81/83…`) have pad 2
|
||||
and so decoded to *nothing* — the viewer then showed them as a flat 2D texture instead of a model.
|
||||
Fix: both anchors try `pad ∈ 0..=3` (`ib = vb − idx_bytes − pad` for the single-block adjacency
|
||||
anchor; `ib0 = vb0 − span − pad` for the grouped pivot), validated — a wrong pad reads shifted
|
||||
indices → agreement collapses < 0.85, so only the true pad passes. pad>0 in the ungated stage path
|
||||
is gated at a strict 0.85 to avoid a false anchor; pad 0 keeps its exact prior behaviour (stages
|
||||
unchanged). Result: all 19 now decode as clean models (e.g. `wep_34` 1243 v / 1233 t, a
|
||||
long-barrelled gun-pod; `wep_08` 3 sub-meshes / 478 t). Viewer routing already falls through
|
||||
`from_xpr2` → `anchor_models(0.85)` for single-XBG7 files, so the recovered grouped/padded weapons
|
||||
now preview as meshes.
|
||||
- 2026-07-18 (refinement 2) — **pivot on the largest sub-mesh; all 19 recovered.** Three weapons
|
||||
(`wep_81`, `wep_81_hangar`, `wep_30_hangar`) still declined because the grouped pivot validated
|
||||
`markers[0]`, which for these is a tiny *elongated* lead bracket that fails the connectivity gate
|
||||
even when perfectly placed. Fixed by pivoting the alignment check on the **largest** marker (max
|
||||
index count) — the sub-mesh whose triangle-quality/connectivity signature most reliably confirms
|
||||
`(ib0, vb0)`. Once the pivot validates, markers up to it are read unconditionally (a legitimately
|
||||
tiny/flat lead part may fail the quality gates yet still be real), and markers after it stay
|
||||
validated so a stray trailing marker ends the chain. Result: **all 19 previously-declined weapons
|
||||
decode** (`wep_81` 460 t missile w/ tail fins; `wep_30_hangar` 334 t). DeltaSaber unchanged (its
|
||||
body IS the largest marker → same pivot). 7/7 disc tests green, stage quality audit unchanged.
|
||||
|
||||
Reference in New Issue
Block a user