Band faces now use signed distance to the displaced solid intervals, continuous across bands, so MC interpolates the interface instead of terracing at density levels. The Gradient amplitude is restored to width/8 (about 4 m peak-to-peak at 16 m). Ring/terrace audit at LOD0-8 on the FI floor and SW ceiling passes; negative controls: L30 (rings) and L31 (flat) both fail. Hole/interface audits, 16 passage rows and 24/24 determinism pass. Evaluator 20. Cost vs L31 SW +1.9%, FI +1.5%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
85 lines
3.7 KiB
Python
85 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Stream one VFMSH tile at a time to locate LOD/interface patches."""
|
|
import argparse
|
|
import array
|
|
import resource
|
|
import struct
|
|
import sys
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("scene")
|
|
parser.add_argument("--lod", type=int, required=True)
|
|
parser.add_argument("--surface", choices=("floor", "ceiling"), required=True)
|
|
parser.add_argument("--height-min-m", type=float, required=True)
|
|
parser.add_argument("--height-max-m", type=float, required=True)
|
|
args = parser.parse_args()
|
|
candidates = []
|
|
scanned = 0
|
|
with open(args.scene, "rb") as stream:
|
|
if stream.read(8) != b"VFMSH001":
|
|
raise SystemExit("invalid VFMSH001 scene")
|
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
|
stream.seek(40, 1)
|
|
if version != 1:
|
|
raise SystemExit("unsupported mesh scene")
|
|
for _ in range(tile_count):
|
|
key = struct.unpack("<4i", stream.read(16))
|
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
|
"<5I", stream.read(20))
|
|
payload = vertex_count * 24 + index_count * 4 + mask_count
|
|
if key[3] != args.lod:
|
|
stream.seek(payload, 1)
|
|
continue
|
|
scanned += 1
|
|
positions = array.array("f")
|
|
positions.frombytes(stream.read(vertex_count * 12))
|
|
normals = array.array("f")
|
|
normals.frombytes(stream.read(vertex_count * 12))
|
|
indices = array.array("I")
|
|
indices.frombytes(stream.read(index_count * 4))
|
|
masks = stream.read(mask_count)
|
|
if sys.byteorder != "little":
|
|
positions.byteswap()
|
|
normals.byteswap()
|
|
indices.byteswap()
|
|
hits = 0
|
|
sum_x = sum_y = sum_z = 0.0
|
|
min_z, max_z = float("inf"), float("-inf")
|
|
want_up = args.surface == "floor"
|
|
for offset in range(0, index_count, 3):
|
|
if masks and masks[offset // 3]:
|
|
continue
|
|
a, b, c = indices[offset:offset + 3]
|
|
if max(a, b, c) >= surface_count:
|
|
continue
|
|
nz = (normals[a * 3 + 2] + normals[b * 3 + 2] + normals[c * 3 + 2]) / 3.0
|
|
if (nz > 0) != want_up:
|
|
continue
|
|
za, zb, zc = positions[a * 3 + 2], positions[b * 3 + 2], positions[c * 3 + 2]
|
|
if ((za + zb + zc) / 3.0 < args.height_min_m
|
|
or (za + zb + zc) / 3.0 > args.height_max_m):
|
|
continue
|
|
hits += 1
|
|
sum_x += (positions[a * 3] + positions[b * 3] + positions[c * 3]) / 3.0
|
|
sum_y += (positions[a * 3 + 1] + positions[b * 3 + 1] + positions[c * 3 + 1]) / 3.0
|
|
z = (za + zb + zc) / 3.0
|
|
sum_z += z
|
|
min_z, max_z = min(min_z, za, zb, zc), max(max_z, za, zb, zc)
|
|
if hits:
|
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
|
candidates.append((hits, key, sum_x / hits, sum_y / hits,
|
|
sum_z / hits, min_z, max_z, extent))
|
|
candidates.sort(reverse=True, key=lambda item: item[0])
|
|
print(f"[VFMeshTileScan] lod={args.lod} surface={args.surface} scanned_tiles={scanned} "
|
|
f"candidate_tiles={len(candidates)} peak_rss_kib="
|
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
|
for hits, key, x, y, z, low, high, extent in candidates[:40]:
|
|
print(f"[VFMeshTile] key={key} triangles={hits} centroid_m=({x:.3f},{y:.3f},{z:.3f}) "
|
|
f"z_m={low:.3f}..{high:.3f} extent_m={extent:.3f}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|