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
123 lines
6.3 KiB
Python
123 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Stream selected-LOD tiles and find face windows that pass the ring audit."""
|
|
import argparse
|
|
import array
|
|
import importlib.util
|
|
from pathlib import Path
|
|
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)
|
|
parser.add_argument("--preferred-height-m", type=float,
|
|
help="lock overlapping-face selection near this expected surface height")
|
|
parser.add_argument("--step-m", type=float, default=0.5)
|
|
parser.add_argument("--tile-key", nargs=3, type=int,
|
|
help="audit only this tile's x/y/z key (LOD is supplied separately)")
|
|
args = parser.parse_args()
|
|
audit_path = Path(__file__).with_name("AuditRingTerraceL32.py")
|
|
spec = importlib.util.spec_from_file_location("ring_audit_l32", audit_path)
|
|
audit = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(audit)
|
|
want_up = args.surface == "floor"
|
|
candidates = []
|
|
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):
|
|
header = stream.read(36)
|
|
key = struct.unpack("<4i", header[:16])
|
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
|
"<5I", header[16:])
|
|
payload = vertex_count * 24 + index_count * 4 + mask_count
|
|
if key[3] != args.lod or (args.tile_key and tuple(key[:3]) != tuple(args.tile_key)):
|
|
stream.seek(payload, 1)
|
|
continue
|
|
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()
|
|
triangles = []
|
|
vertex_margin = voxel_um / 1_000_000.0 * 0.5
|
|
for offset in range(0, index_count, 3):
|
|
if masks and masks[offset // 3]:
|
|
continue
|
|
ids = indices[offset:offset + 3]
|
|
if max(ids) >= surface_count:
|
|
continue
|
|
verts = []
|
|
for index in ids:
|
|
p = index * 3
|
|
verts.append(((positions[p], positions[p + 1], positions[p + 2]),
|
|
(normals[p], normals[p + 1], normals[p + 2])))
|
|
average_nz = sum(vertex[1][2] for vertex in verts) / 3.0
|
|
if (want_up and average_nz < 0.35) or (not want_up and average_nz > -0.35):
|
|
continue
|
|
z_values = [vertex[0][2] for vertex in verts]
|
|
mean_z = sum(z_values) / 3.0
|
|
if (mean_z < args.height_min_m or mean_z > args.height_max_m or
|
|
min(z_values) < args.height_min_m - vertex_margin or
|
|
max(z_values) > args.height_max_m + vertex_margin):
|
|
continue
|
|
triangles.append((key[3], average_nz, tuple(verts)))
|
|
if not triangles:
|
|
continue
|
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
|
cx, cy = (key[0] + 0.5) * extent, (key[1] + 0.5) * extent
|
|
radius = extent * 0.5
|
|
bounds = (cx - radius, cx + radius, cy - radius, cy + radius)
|
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << key[3]))
|
|
buckets = audit.build_buckets(triangles, want_up, bounds, cell_size)
|
|
profiles = []
|
|
preferred_height = args.preferred_height_m
|
|
if preferred_height is None:
|
|
preferred_height = args.height_max_m if want_up else args.height_min_m
|
|
for _, points in audit.make_lines(cx, cy, radius, args.step_m):
|
|
profiles.append(audit.audit_profile(points, buckets, want_up,
|
|
0.5, 0.0005, 0.1, cell_size, preferred_height))
|
|
valid = [profile for profile in profiles if profile["valid"]]
|
|
coverage = min(profile["coverage"] for profile in profiles)
|
|
amplitude = max((profile["height_max"] for profile in valid), default=0.0) - \
|
|
min((profile["height_min"] for profile in valid), default=0.0)
|
|
longest = max((run[0] for profile in valid for run in profile["plateaus"]),
|
|
default=0.0)
|
|
max_normal = max((profile["max_angle"] for profile in valid), default=float("inf"))
|
|
passed = (len(valid) == len(profiles) and coverage >= 0.75 and amplitude >= 3.0
|
|
and longest < 0.5 and max_normal <= 20.0)
|
|
candidates.append((passed, amplitude, -max_normal, key, cx, cy,
|
|
len(triangles), coverage, longest, max_normal))
|
|
stream.seek(payload - (vertex_count * 24 + index_count * 4 + mask_count), 1)
|
|
candidates.sort(reverse=True, key=lambda item: (item[0], item[1], item[2]))
|
|
print(f"[RingWindowSearch] lod={args.lod} surface={args.surface} tiles={len(candidates)} "
|
|
f"height_window_m={args.height_min_m:.3f}..{args.height_max_m:.3f} "
|
|
f"step_m={args.step_m:.3f}")
|
|
for passed, amplitude, neg_normal, key, cx, cy, count, coverage, longest, normal in candidates[:40]:
|
|
print(f"[RingWindowCandidate] status={'PASS' if passed else 'FAIL'} key={key} "
|
|
f"center_m=({cx:.3f},{cy:.3f}) triangles={count} coverage={coverage:.3f} "
|
|
f"amplitude_m={amplitude:.6f} longest_flat_m={longest:.6f} "
|
|
f"normal_jump_deg={normal:.6f}")
|
|
print(f"[RingWindowRSS] peak_rss_kib="
|
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|