#!/usr/bin/env python3 """Summarize exported tile geometry and upward-facing ground for L17.""" import array import collections import math import struct import sys def read_exact(stream, size): data = stream.read(size) if len(data) != size: raise EOFError(f"wanted {size} bytes, got {len(data)}") return data path = sys.argv[1] with open(path, "rb") as stream: if read_exact(stream, 8) != b"VFMSH001": raise SystemExit("bad VFMSH001 header") version, voxel_um, tile_count = struct.unpack("<3I", read_exact(stream, 12)) camera = struct.unpack("<3d", read_exact(stream, 24)) yaw, pitch = struct.unpack("<2d", read_exact(stream, 16)) per_lod = collections.defaultdict(lambda: {"tiles": 0, "vertices": 0, "triangles": 0, "surface_triangles": 0, "up_triangles": 0, "up_centroids": [], "tile_spans": [], "empty": 0}) tile_rows = [] for _ in range(tile_count): key = struct.unpack("<4i", read_exact(stream, 16)) surf_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack( "<5I", read_exact(stream, 20)) p = array.array("f") p.frombytes(read_exact(stream, vertex_count * 12)) n = array.array("f") n.frombytes(read_exact(stream, vertex_count * 12)) if sys.byteorder != "little": p.byteswap(); n.byteswap() indices = array.array("I") indices.frombytes(read_exact(stream, index_count * 4)) masks = read_exact(stream, mask_count) if sys.byteorder != "little": indices.byteswap() lod = key[3] row = per_lod[lod] row["tiles"] += 1 row["vertices"] += vertex_count row["triangles"] += index_count // 3 row["surface_triangles"] += index_count // 3 - skirt_count up = [] for tri in range(index_count // 3): if masks and masks[tri] != 0: continue a, b, c = indices[tri * 3:tri * 3 + 3] if max(a, b, c) >= surf_vertices: continue if (n[a * 3 + 2] + n[b * 3 + 2] + n[c * 3 + 2]) / 3.0 < 0.65: continue up.append((p[a * 3 + 2] + p[b * 3 + 2] + p[c * 3 + 2]) / 3.0) row["up_triangles"] += len(up) row["up_centroids"].extend(up) if up: up.sort() span = up[math.floor(0.95 * (len(up) - 1))] - up[math.floor(0.05 * (len(up) - 1))] row["tile_spans"].append(span) tile_rows.append((key, vertex_count, index_count // 3, skirt_count, len(up), up[0], up[len(up) // 2], up[-1], span)) else: row["empty"] += 1 tile_rows.append((key, vertex_count, index_count // 3, skirt_count, 0, math.nan, math.nan, math.nan, math.nan)) print(f"version={version} voxel_um={voxel_um} tiles={tile_count} camera_m={camera} yaw={yaw} pitch={pitch}") print("lod tiles mesh_tiles vertices triangles skirt_triangles upward_surface_triangles " "upward_centroid_z_m_p05/median/p95 tile_up_z_span_m_p50/p95/max") for lod in sorted(per_lod): r = per_lod[lod] z = sorted(r["up_centroids"]) s = sorted(r["tile_spans"]) def q(values, fraction): return values[math.floor(fraction * (len(values) - 1))] if values else math.nan print(f"{lod} {r['tiles']} {r['tiles']-r['empty']} {r['vertices']} {r['triangles']} " f"{sum(row[3] for row in tile_rows if row[0][3] == lod)} {r['up_triangles']} " f"{q(z,.05):.4f}/{q(z,.5):.4f}/{q(z,.95):.4f} " f"{q(s,.5):.4f}/{q(s,.95):.4f}/{max(s) if s else math.nan:.4f}") print("near_camera_up_tiles (sorted by XY tile-centre distance):") near = [] for row in tile_rows: key = row[0] lod = key[3] extent = 32 * (1 << lod) * voxel_um / 1_000_000.0 cx = (key[0] + 0.5) * extent cy = (key[1] + 0.5) * extent near.append((math.hypot(cx-camera[0], cy-camera[1]), row)) for distance, row in sorted(near)[:30]: key, verts, tris, skirts, ups, lo, med, hi, span = row if ups: print(f"key={key} xy_distance_m={distance:.2f} vertices={verts} triangles={tris} " f"skirt_triangles={skirts} upward_triangles={ups} z_m={lo:.3f}/{med:.3f}/{hi:.3f} " f"tile_span_p05_p95_m={span:.3f}")