re: MCOL solved -- a closed triangle collision mesh in a uniform grid

The 0x50 header word, which the first section of this page had dismissed as "a
large value", is two u16 counts: vertices and triangles.  They give the two
remaining blocks their stride, and every derived length is exact in 11/11 --
len(0x54) == align16(12*nv), len(0x58) == align16(6*nt), and nt equals the
bounding-sphere count decoded last iteration.

Checks that cannot pass by accident:

  * sphere i is the TIGHT bounding sphere of triangle i, 4768/4768, with
    max|v-c|/r median 0.99990 (a fixed 1.0001 epsilon), against a 1.32%
    random-triangle control;
  * the mesh is watertight -- every edge shared by exactly two triangles,
    7152/7152, zero degenerate triangles, zero unreferenced vertices;
  * the two smallest objects are 8 vertices and 12 triangles whose positions
    are the eight +-250000 corners of the map bbox: a bare bounding cube.

The cell lists are a correct broad phase: with an exact triangle/box SAT test
only 3 overlapping triangles in 18 577 entries are absent, so a query walking
one cell's list cannot miss a hit.  The 730 conservative extras bracket the
builder's own test between exact-SAT and AABB, which retires the 18 unexplained
"sphere misses" from the previous commit as that same margin.

mcol_probe.py gains `mesh` and `obj`; `verify` now runs all three checks and its
output is recorded in docs/re/data/mcol-verify.txt.
This commit is contained in:
Sylpheed RE agent
2026-08-26 09:12:36 +00:00
parent 4f0d21f50d
commit c0a7033295
4 changed files with 261 additions and 7 deletions

View File

@@ -375,3 +375,114 @@ rather than a decode error, but it is **not explained** and is recorded as open.
these spheres bound. Their lengths are **not** a constant multiple of the sphere
count (`0x58 / n` is ≈6.0 for the large objects but 6.13 and 6.67 for the two
smallest), so at least one of them is variable-stride or has its own count.
## ✅✅ `MCOL` is a **closed triangle collision mesh** — decoded
**2026-08-26.** The header's `0x50` word, which the first section on this page
dismissed as "a large value", is **two `u16` counts**, and they give both
remaining blocks a stride:
0x50 u16 vertex count 0x52 u16 triangle count
Every derived length is exact in **11 / 11** objects:
| | |
|---|---|
| `len(0x54) == align16(12 × vertices)` | **11 / 11** |
| `len(0x58) == align16(6 × triangles)` | **11 / 11** |
| triangle count `==` sphere count (`len(0x5C)/16`) | **11 / 11** |
| object | verts | tris | `0x54` | `0x58` | spheres |
|---|---|---|---|---|---|
| `2cf7eb47` | 34 | 60 | 416 | 368 | 60 |
| `cbb99d34` | 8 | 12 | 96 | 80 | 12 |
| `d84a95fb` | 111 | 218 | 1 344 | 1 312 | 218 |
| `db066592` | 405 | 790 | 4 864 | 4 752 | 790 |
| `db61c506` | 73 | 134 | 880 | 816 | 134 |
| `dc44fe0c` | 89 | 174 | 1 072 | 1 056 | 174 |
| `dc4b0896` | 502 | 924 | 6 032 | 5 552 | 924 |
| `dd89a110` | 632 | 1 164 | 7 584 | 6 992 | 1 164 |
| `df4628c2` | 134 | 260 | 1 616 | 1 568 | 260 |
| `e084c13c` | 8 | 12 | 96 | 80 | 12 |
| `e16460cf` | 554 | 1 020 | 6 656 | 6 128 | 1 020 |
The two smallest objects are the tell on their own: **8 vertices and 12
triangles**, and their `0x54` block is exactly the eight `±250 000` corners of
the map's bounding box. That is a cube as a triangle mesh — a map whose only
collision is its outer wall.
### Sphere *i* bounds triangle *i*, and it is tight
The algebraic test, which cannot pass by accident:
| | |
|---|---|
| all three vertices of triangle *i* inside sphere *i* | **4 768 / 4 768 = 100.00 %** |
| …and the sphere is **tight** (`max‖vc‖ / r` within 1 %) | **4 768 / 4 768 = 100.00 %** |
| sphere *i* encloses a *random* triangle (control) | 63 / 4 768 = **1.32 %** |
`max‖vc‖ / r` has median **0.99990** across every triangle of every object, so
the radius is the enclosing radius times a fixed **1.0001** epsilon. That is the
same 1.0001 visible by hand on the cube, where the sphere of a box-face triangle
came out as `250 000·√2 × 1.0001`.
### And the mesh is closed
| | |
|---|---|
| edges shared by exactly two triangles | **7 152 / 7 152 = 100.00 %** |
| degenerate triangles | **0** |
| vertices never referenced by a triangle | **0** |
A watertight manifold with no orphans — which is what a collision hull must be,
and a result that a wrong stride or a wrong index width could not produce.
`cbb99d34` has Euler characteristic `V E + F = 8 18 + 12 = 2` (one closed
surface, the box); `2cf7eb47` has 4, i.e. two closed components.
### The cell lists are a correct broad phase
Re-running the completeness test with the *actual triangles* instead of their
bounding spheres, using a separating-axis triangle/box test:
| | |
|---|---|
| cells whose `u16` list is exactly the overlapping triangle set | 3 973 / 4 488 = **88.52 %** |
| triangles overlapping a cell but **absent** from its list | **3** |
| triangles listed but not overlapping | 730 (3.9 % of 18 577 entries) |
Three misses in 18 577 entries: the lists are **complete**, which is the property
a broad phase must have — a collision query that walks one cell's list cannot
miss a triangle it should have hit. The 730 extras are conservative and harmless.
Their size identifies the builder's own test as sitting **between** an exact
triangle/box test and a cheap AABB one: replacing the SAT test with "triangle
AABB overlaps cell" gives **0** listed-but-not-overlapping (so every listed
triangle's AABB does reach its cell) but 7 913 missing, so the builder is
stricter than AABB and looser than exact — an exact test with a margin. This
also explains the **18 sphere misses** left open in the section above: they were
never sphere-vs-cell facts, they are that same margin.
### The full format
0x00 magic 'MCOL'
0x04 data size (POF0 table at data_size + 16)
0x10 bbox min f32[4] 0x20 bbox max f32[4] 0x30 extent f32[4]
0x40 cell size f32[3]
0x50 u16 vertex count 0x52 u16 triangle count
0x54 → vertex array f32[3] × nv, padded to 16
0x58 → triangle array u16[3] × nt, padded to 16
0x5C → bounding spheres {centre f32[3], radius f32} × nt (= 0x80)
0x74 → grid cell array 32-byte A records, pointer at +8
position → cell → A {cell index (x,y,z,1), count 1, →B, bounding sphere}
→ B {u32 count, →u16[n] at +4}
→ n triangle indices
Reproduce with `tools/re-capture/mcol_probe.py verify` (recorded in
[`../data/mcol-verify.txt`](../data/mcol-verify.txt)); `mcol_probe.py obj` writes
any object out as a Wavefront OBJ.
❔ Still open: `MCOL` objects are **not name-resolved**, so which map each one
belongs to — and the object-to-object pairing with `REGN` that the header
distributions only hint at — is still unknown. And the runtime consumer has not
been found, the same gap `REGN` has.