Files
VoxelForge/Standalone/Research/WorldAssemblerM3/Linux31/MeasureRingL31.py
T
Fr0zkaandClaude Opus 5.5 28069f0d98 wip(assembler): round L31 -- FI recipe column audit corrected; recipe-stamped audit runner
On the real FloatingIslands recipe, column caps sit inside anchor-island rock and native bedrock;
the old check sampled the route foot. The audit now samples each planned cap against the native
field (negative control: a cap moved into air is detected). The audit runner records requested vs
actual recipe and exits on mismatch. All 16 passage rows (4 styles x 2 recipes x 2 compilers) pass.
The readable shader uses a smooth tint. Interim: FI Gradient displacement capped at width/256
(flat floor, no contour rings); restoring real undulation is L32. Evaluator 19.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
2026-10-21 14:13:18 +02:00

242 lines
10 KiB
Python

#!/usr/bin/env python3
"""Rasterize upward mesh triangles into a top-down field height map."""
import argparse
import array
import json
import math
import struct
import sys
Q16 = 1 << 16
MASK64 = (1 << 64) - 1
def trunc_div(numerator, denominator):
return numerator // denominator if numerator >= 0 else -((-numerator) // denominator)
def transition_hash(seed, interface, x, y, z, salt):
value = 14695981039346656037
for word in (seed & 0xffffffff, interface & 0xffffffff,
x & MASK64, y & MASK64, z & MASK64, salt):
for byte in range(8):
value ^= (word >> (byte * 8)) & 0xff
value = (value * 1099511628211) & MASK64
value ^= value >> 30
value = (value * 0xbf58476d1ce4e5b9) & MASK64
value ^= value >> 27
value = (value * 0x94d049bb133111eb) & MASK64
value ^= value >> 31
return ((value >> 32) >> 15) - Q16
def transition_fade(value):
value = max(0, min(Q16, value))
square = value * value // Q16
return square * (3 * Q16 - 2 * value) // Q16
def gradient_height_um(layout, x_um, y_um):
upper = next(item for item in layout["strates"] if item["order"] == 0)
lower = next(item for item in layout["strates"] if item["order"] == 1)
width = upper["transition_width_um"]
cell = max(1_000_000, layout["strate_layout_chunk_size_um"] * 4)
ix = x_um // cell
iy = y_um // cell
tx = transition_fade(((x_um - ix * cell) * Q16) // cell)
ty = transition_fade(((y_um - iy * cell) * Q16) // cell)
def at(dx, dy):
noise = transition_hash(0, upper["strate_index"], ix + dx, iy + dy, 0,
0x47524144)
return noise
def lerp(a, b, t):
return a + trunc_div((b - a) * t, Q16)
low = lerp(lerp(at(0, 0), at(1, 0), tx),
lerp(at(0, 1), at(1, 1), tx), ty)
amplitude_divisor = 256 if upper.get("generator_kind") == "FloatingIslands" else 8
displacement = trunc_div(low * (width // amplitude_divisor), Q16)
seam = upper["bedrock"]["bottom_um"]
thickness = upper["bedrock"]["top_um"] - seam
return seam + displacement + thickness
def read_scene(path):
triangles = []
with open(path, "rb") as stream:
if stream.read(8) != b"VFMSH001":
raise SystemExit(f"{path}: invalid scene header")
version, voxel_um, count = struct.unpack("<3I", stream.read(12))
stream.read(40)
if version != 1 or count > 100000:
raise SystemExit("unsupported scene")
for _ in range(count):
key = struct.unpack("<4i", stream.read(16))
surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
"<5I", stream.read(20))
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()
for index in range(0, index_count, 3):
if masks and masks[index // 3]:
continue
a, b, c = indices[index:index + 3]
if max(a, b, c) >= surface_vertices:
continue
nz = (normals[a * 3 + 2] + normals[b * 3 + 2]
+ normals[c * 3 + 2]) / 3.0
if nz < 0.35:
continue
vertices = tuple((positions[v * 3], positions[v * 3 + 1],
positions[v * 3 + 2]) for v in (a, b, c))
triangles.append(vertices)
return voxel_um, triangles
def rasterize(triangles, bounds, width, height):
x0, x1, y0, y1 = bounds
values = [None] * (width * height)
sx, sy = width / (x1 - x0), height / (y1 - y0)
for tri in triangles:
(ax, ay, az), (bx, by, bz), (cx, cy, cz) = tri
den = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
if abs(den) < 1.0e-12:
continue
left = max(0, int(math.floor((min(ax, bx, cx) - x0) * sx)))
right = min(width - 1, int(math.ceil((max(ax, bx, cx) - x0) * sx)))
top = max(0, int(math.floor((y1 - max(ay, by, cy)) * sy)))
bottom = min(height - 1, int(math.ceil((y1 - min(ay, by, cy)) * sy)))
for py in range(top, bottom + 1):
y = y1 - (py + 0.5) / sy
row = py * width
for px in range(left, right + 1):
x = x0 + (px + 0.5) / sx
u = ((by - cy) * (x - cx) + (cx - bx) * (y - cy)) / den
v = ((cy - ay) * (x - cx) + (ax - cx) * (y - cy)) / den
w = 1.0 - u - v
if min(u, v, w) < -1.0e-6:
continue
z = u * az + v * bz + w * cz
idx = row + px
if values[idx] is None or z > values[idx]:
values[idx] = z
return values
def main():
parser = argparse.ArgumentParser()
parser.add_argument("scene")
parser.add_argument("output_ppm")
parser.add_argument("--bounds", nargs=4, type=float, required=True,
metavar=("XMIN", "XMAX", "YMIN", "YMAX"))
parser.add_argument("--size", nargs=2, type=int, default=(640, 640))
parser.add_argument("--range", nargs=2, type=float, default=(-122.0, -118.0),
metavar=("ZMIN", "ZMAX"))
parser.add_argument("--ring-center", nargs=2, type=float,
metavar=("X", "Y"))
parser.add_argument("--probe-max-radius", type=int, default=16)
parser.add_argument("--layout")
args = parser.parse_args()
voxel_um, triangles = read_scene(args.scene)
width, height = args.size
sx, sy = width / (args.bounds[1] - args.bounds[0]), height / (args.bounds[3] - args.bounds[2])
values = rasterize(triangles, args.bounds, width, height)
finite = sorted(value for value in values if value is not None)
if not finite:
raise SystemExit("no upward-facing field samples in bounds")
low, high = args.range
if high <= low:
raise SystemExit("height range must increase")
stops = ((0.00, (31, 52, 92)), (0.24, (41, 119, 153)),
(0.50, (85, 177, 143)), (0.73, (226, 207, 105)),
(1.00, (181, 71, 49)))
with open(args.output_ppm, "wb") as output:
output.write(f"P6\n{width} {height}\n255\n".encode())
for value in values:
if value is None:
output.write(bytes((18, 22, 30)))
continue
t = max(0.0, min(1.0, (value - low) / (high - low)))
for index in range(len(stops) - 1):
if t <= stops[index + 1][0]:
ta, ca = stops[index]
tb, cb = stops[index + 1]
blend = (t - ta) / (tb - ta)
rgb = tuple(round(ca[c] + blend * (cb[c] - ca[c]))
for c in range(3))
break
else:
rgb = stops[-1][1]
output.write(bytes(rgb))
def quantile(p):
return finite[round((len(finite) - 1) * p)]
print(f"[RingFieldRaster] voxel_um={voxel_um} triangles={len(triangles)} "
f"pixels={len(finite)}/{len(values)} height_min_m={finite[0]:.6f} "
f"height_p05_m={quantile(.05):.6f} height_median_m={quantile(.5):.6f} "
f"height_p95_m={quantile(.95):.6f} height_max_m={finite[-1]:.6f} "
f"height_range_m={finite[-1]-finite[0]:.6f} "
f"map={args.output_ppm}")
if args.ring_center:
cx, cy = args.ring_center
def at_world(x, y):
fx = (x - args.bounds[0]) * sx - 0.5
fy = (args.bounds[3] - y) * sy - 0.5
ix, iy = math.floor(fx), math.floor(fy)
tx, ty = fx - ix, fy - iy
samples = []
for px, py in ((ix, iy), (ix + 1, iy), (ix, iy + 1), (ix + 1, iy + 1)):
if not (0 <= px < width and 0 <= py < height):
return None
value = values[py * width + px]
if value is None:
return None
samples.append(value)
return ((samples[0] * (1 - tx) + samples[1] * tx) * (1 - ty)
+ (samples[2] * (1 - tx) + samples[3] * tx) * ty)
print(f"[RingRadialProbe] center_m=({cx:.3f},{cy:.3f}) "
"radius_m mean_m angular_sd_m min_m max_m samples")
for radius in range(1, args.probe_max_radius + 1):
row = [at_world(cx + radius * math.cos(a * math.tau / 32),
cy + radius * math.sin(a * math.tau / 32))
for a in range(32)]
row = [value for value in row if value is not None]
if row:
mean = sum(row) / len(row)
sd = math.sqrt(sum((value - mean) ** 2 for value in row) / len(row))
print(f" {radius:>2} {mean:.6f} {sd:.6f} {min(row):.6f} "
f"{max(row):.6f} {len(row)}")
if args.layout:
layout = json.load(open(args.layout, encoding="utf-8"))
if next(item for item in layout["strates"] if item["order"] == 0)["transition_mode"] != "Gradient":
raise SystemExit("--layout must describe a Gradient owner interface")
errors = []
for index, actual in enumerate(values):
if actual is None:
continue
px, py = index % width, index // width
x = args.bounds[0] + (px + 0.5) / sx
y = args.bounds[3] - (py + 0.5) / sy
expected = gradient_height_um(layout, round(x * 1_000_000),
round(y * 1_000_000)) / 1_000_000.0
errors.append(actual - expected)
absolute = sorted(abs(error) for error in errors)
rms = math.sqrt(sum(error * error for error in errors) / len(errors))
print(f"[RingGradientMatch] samples={len(errors)} rms_m={rms:.6f} "
f"p95_abs_m={absolute[round((len(absolute)-1)*.95)]:.6f} "
f"max_abs_m={absolute[-1]:.6f} "
"expected=owner-resolved-2d-hash-value-noise")
if __name__ == "__main__":
main()