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

@@ -4,9 +4,11 @@
`MCOL` (`hidden/MiscBin.pak`, 11 objects) shares `REGN`'s container, so the
`POF0` reader is imported from `regn_decode.py` rather than duplicated.
./mcol_probe.py stride <MiscBin.pak> # the 0x5C block is stride 16, not 12
./mcol_probe.py cells <MiscBin.pak> # the u16s name spheres in their cell
./mcol_probe.py verify <MiscBin.pak> # both, with pass/fail
./mcol_probe.py stride <MiscBin.pak> # the 0x5C block is stride 16, not 12
./mcol_probe.py cells <MiscBin.pak> # the u16s name spheres in their cell
./mcol_probe.py mesh <MiscBin.pak> # vertices, triangles, bounding spheres
./mcol_probe.py obj <MiscBin.pak> <hash> <out.obj>
./mcol_probe.py verify <MiscBin.pak> # all checks, with pass/fail
The `cells` check is the powered one: a `u16` is reached *through* a specific
grid cell, so the sphere it names must reach that cell. A bound-check ("is it
@@ -24,9 +26,16 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from regn_decode import pak_entries, pof0_pointer_slots, FIXUP_BASE
SPHERE = 16 # stride of the 0x5C record: centre f32[3] then radius f32
VERTEX = 12 # stride of the 0x54 record: f32[3]
TRI = 6 # stride of the 0x58 record: u16[3]
def align16(n):
return (n + 15) // 16 * 16
def u32(b, o): return struct.unpack_from(">I", b, o)[0]
def u16(b, o): return struct.unpack_from(">H", b, o)[0]
def f32(b, o): return struct.unpack_from(">f", b, o)[0]
def ptr(b, o): return u32(b, o) + FIXUP_BASE
@@ -42,9 +51,19 @@ class Mcol:
for o in (0x54, 0x58, 0x5C, 0x74))
self.sphere_bytes = self.p54 - self.p5c
self.n = self.sphere_bytes // SPHERE
self.nv = u16(blob, 0x50) # vertex count
self.nt = u16(blob, 0x52) # triangle count == sphere count
self.bbox_min = [f32(blob, 0x10 + 4 * i) for i in range(3)]
self.cell_size = [f32(blob, 0x40 + 4 * i) for i in range(3)]
def vertex(self, i):
o = self.p54 + VERTEX * i
return [f32(self.b, o + 4 * k) for k in range(3)]
def triangle(self, i):
o = self.p58 + TRI * i
return [u16(self.b, o + 2 * k) for k in range(3)]
def sphere(self, i):
o = self.p5c + SPHERE * i
return [f32(self.b, o + 4 * k) for k in range(3)], f32(self.b, o + 12)
@@ -120,6 +139,69 @@ def cmd_cells(pak, seed=12345):
return hit / tot > 0.99 and ctl / tot < 0.25
def cmd_mesh(pak):
"""The 0x50 counts give both blocks a stride, and sphere i bounds triangle i."""
import collections
print(f"{'object':>8} {'verts':>6} {'tris':>6} {'len 0x54':>9} {'len 0x58':>9} {'spheres':>8}")
n = sv = st = ss = 0
enc = tight = ctl = tri_total = 0
edges2 = edges = 0
random.seed(7)
for h, blob in objects(pak):
m = Mcol(blob)
l54, l58 = m.p58 - m.p54, m.p74 - m.p58
n += 1
sv += align16(VERTEX * m.nv) == l54
st += align16(TRI * m.nt) == l58
ss += m.nt == m.n
print(f"{h:08x} {m.nv:6d} {m.nt:6d} {l54:9d} {l58:9d} {m.n:8d}")
V = [m.vertex(i) for i in range(m.nv)]
T = [m.triangle(i) for i in range(m.nt)]
ec = collections.Counter()
for i, t in enumerate(T):
c, r = m.sphere(i)
far = max(math.dist(V[j], c) for j in t)
tri_total += 1
enc += far <= r * 1.0001
tight += abs(far / r - 1) < 0.01
tj = T[random.randrange(m.nt)]
ctl += max(math.dist(V[j], c) for j in tj) <= r * 1.0001
for k in range(3):
a, b_ = t[k], t[(k + 1) % 3]
ec[(min(a, b_), max(a, b_))] += 1
edges += len(ec)
edges2 += sum(1 for v in ec.values() if v == 2)
print(f"\nlen(0x54) == align16(12 * verts) : {sv}/{n}")
print(f"len(0x58) == align16(6 * tris) : {st}/{n}")
print(f"triangle count == sphere count : {ss}/{n}")
print(f"sphere i encloses triangle i : {enc}/{tri_total} = {100*enc/tri_total:.2f}%")
print(f" ...and is tight to within 1% : {tight}/{tri_total} = {100*tight/tri_total:.2f}%")
print(f"sphere i encloses a random triangle (ctl) : {ctl}/{tri_total} = {100*ctl/tri_total:.2f}%")
print(f"edges shared by exactly two triangles : {edges2}/{edges} = {100*edges2/edges:.2f}%")
return (sv == st == ss == n and enc == tri_total and edges2 == edges
and ctl / tri_total < 0.10)
def cmd_obj(pak, want, out):
"""Export one object as a Wavefront OBJ, so the decode can be looked at."""
want = int(want, 16)
for h, blob in objects(pak):
if h != want:
continue
m = Mcol(blob)
with open(out, "w") as fh:
fh.write(f"# MCOL {h:08x} -- {m.nv} vertices, {m.nt} triangles\n")
for i in range(m.nv):
fh.write("v %.4f %.4f %.4f\n" % tuple(m.vertex(i)))
for i in range(m.nt):
a, b_, c = m.triangle(i)
fh.write(f"f {a+1} {b_+1} {c+1}\n")
print(f"{out}: {m.nv} vertices, {m.nt} triangles")
return True
raise SystemExit(f"no MCOL object {want:08x}")
def main():
if len(sys.argv) < 3:
raise SystemExit(__doc__)
@@ -128,11 +210,17 @@ def main():
ok = cmd_stride(pak)
elif cmd == "cells":
ok = cmd_cells(pak)
elif cmd == "mesh":
ok = cmd_mesh(pak)
elif cmd == "obj":
ok = cmd_obj(pak, sys.argv[3], sys.argv[4])
elif cmd == "verify":
a = cmd_stride(pak)
print()
b = cmd_cells(pak)
ok = a and b
print()
c = cmd_mesh(pak)
ok = a and b and c
else:
raise SystemExit(__doc__)
print("\nPASS" if ok else "\nFAIL")