wip(assembler): round L37 -- FI mouth carve-fade fix, no raised bridge (style 0); research R1
The passage mouth fade multiplied the signed carve distance by zero, which turned native rock into an open surface (the reason earlier rounds stamped walk pads at mouths); it now blends between the uncarved field and the full signed carve. The FI style-0 route followed a lifted grade envelope over the crater (the "bridge"); it now runs on the rim and follows the native surface. New built-shape and crease audits with thresholds frozen before the fix (L36 and box controls fail). 24/24 passage rows, 30/30 determinism, 16/16 invariants, 18/18 interface/ring. Unreal: one recipe setter (uncompiled). Research R1 (read-only, web): ranked proposals for natural walkable mouths, floor-limited carve first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
No files matched your search
@@ -0,0 +1,535 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure player-scale planar aprons, straight boundaries and corners at passage mouths."""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import math
|
||||
from collections import defaultdict, deque
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
SPEC = importlib.util.spec_from_file_location("ring_profile_l37", PROFILE_PATH)
|
||||
PROFILE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(PROFILE)
|
||||
|
||||
# Frozen from player scale before measuring the L37 candidate. The 0.75 m line limit
|
||||
# is just over the 0.68 m capsule width; 0.50 m2 is one capsule-width footprint.
|
||||
STRAIGHT_LIMIT_M = 0.75
|
||||
PLANE_LIMIT_M2 = 0.50
|
||||
CORNER_TURN_LIMIT_DEG = 30.0
|
||||
PLANE_TOLERANCE_M = 0.02
|
||||
NORMAL_BUCKET = 0.07
|
||||
PLANE_BUCKET_M = 0.03
|
||||
MOUTH_CORRIDOR_HALF_WIDTH_M = 1.50
|
||||
MOUTH_CORRIDOR_LENGTH_M = 12.0
|
||||
MOUTH_Z_BELOW_M = 4.0
|
||||
MOUTH_Z_ABOVE_M = 3.0
|
||||
PLAYER_FLOOR_OFFSET_M = 1.13
|
||||
DELTA_THRESHOLD_M = 0.05
|
||||
DELTA_GRID_M = 0.125
|
||||
SCENES = {
|
||||
"owner-fi": ("owner-fi-ground-mouth.vfmesh", "layout-owner-fi.json", "FloatingIslands", "upper"),
|
||||
"owner-sw": ("owner-sw-ground-mouth.vfmesh", "layout-owner-sw.json", "SurfaceWorld", "lower"),
|
||||
"upper-sw-fixture": ("upper-sw-fixture-ground-mouth.vfmesh",
|
||||
"layout-upper-sw-fixture.json", "SurfaceWorld", "upper"),
|
||||
}
|
||||
|
||||
|
||||
def sub(a, b):
|
||||
return tuple(a[i] - b[i] for i in range(3))
|
||||
|
||||
|
||||
def dot(a, b):
|
||||
return sum(a[i] * b[i] for i in range(3))
|
||||
|
||||
|
||||
def cross(a, b):
|
||||
return (a[1] * b[2] - a[2] * b[1],
|
||||
a[2] * b[0] - a[0] * b[2],
|
||||
a[0] * b[1] - a[1] * b[0])
|
||||
|
||||
|
||||
def length(a):
|
||||
return math.sqrt(dot(a, a))
|
||||
|
||||
|
||||
def face(points):
|
||||
n = cross(sub(points[1], points[0]), sub(points[2], points[0]))
|
||||
twice_area = length(n)
|
||||
if twice_area <= 1.0e-12:
|
||||
return None
|
||||
area = twice_area * 0.5
|
||||
n = tuple(v / twice_area for v in n)
|
||||
if n[2] < 0.0:
|
||||
n = tuple(-v for v in n)
|
||||
d = -dot(n, points[0])
|
||||
return n, d, area
|
||||
|
||||
|
||||
def vertex_key(point):
|
||||
return tuple(round(value, 4) for value in point)
|
||||
|
||||
|
||||
def edge_key(a, b):
|
||||
ka, kb = vertex_key(a), vertex_key(b)
|
||||
return (ka, kb) if ka < kb else (kb, ka)
|
||||
|
||||
|
||||
def rdp(points, epsilon):
|
||||
if len(points) <= 2:
|
||||
return points
|
||||
a, b = points[0], points[-1]
|
||||
ab = sub(b, a)
|
||||
ab2 = dot(ab, ab)
|
||||
maximum, index = -1.0, -1
|
||||
for i in range(1, len(points) - 1):
|
||||
ap = sub(points[i], a)
|
||||
t = max(0.0, min(1.0, dot(ap, ab) / ab2)) if ab2 else 0.0
|
||||
closest = tuple(a[j] + t * ab[j] for j in range(3))
|
||||
distance = length(sub(points[i], closest))
|
||||
if distance > maximum:
|
||||
maximum, index = distance, i
|
||||
if maximum <= epsilon:
|
||||
return [a, b]
|
||||
left = rdp(points[:index + 1], epsilon)
|
||||
right = rdp(points[index:], epsilon)
|
||||
return left[:-1] + right
|
||||
|
||||
|
||||
def simplified_boundary(component, records):
|
||||
counts = defaultdict(int)
|
||||
actual_edges = {}
|
||||
for index in component:
|
||||
points = records[index]["points"]
|
||||
for a, b in ((points[0], points[1]), (points[1], points[2]),
|
||||
(points[2], points[0])):
|
||||
key = edge_key(a, b)
|
||||
counts[key] += 1
|
||||
actual_edges[key] = (key[0], key[1])
|
||||
boundary = [edge for edge, count in counts.items() if count == 1]
|
||||
graph = defaultdict(list)
|
||||
for a, b in boundary:
|
||||
graph[a].append(b)
|
||||
graph[b].append(a)
|
||||
if not graph:
|
||||
return [], []
|
||||
|
||||
def direction(a, b):
|
||||
dx, dy = b[0] - a[0], b[1] - a[1]
|
||||
magnitude = math.hypot(dx, dy)
|
||||
return ((dx / magnitude, dy / magnitude), magnitude) if magnitude > 1.0e-8 else (None, 0.0)
|
||||
|
||||
def straight_run(origin, first):
|
||||
prior, current = origin, first
|
||||
initial, edge_length = direction(prior, current)
|
||||
if initial is None:
|
||||
return 0.0
|
||||
total = edge_length
|
||||
seen = {origin, first}
|
||||
while current in graph and len(graph[current]) == 2:
|
||||
choices = [value for value in graph[current] if value != prior]
|
||||
if len(choices) != 1 or choices[0] in seen:
|
||||
break
|
||||
nxt = choices[0]
|
||||
outgoing, edge_length = direction(current, nxt)
|
||||
if outgoing is None:
|
||||
break
|
||||
deflection = math.degrees(math.acos(max(-1.0, min(1.0,
|
||||
initial[0] * outgoing[0] + initial[1] * outgoing[1]))))
|
||||
if deflection > 8.0:
|
||||
break
|
||||
total += edge_length
|
||||
prior, current = current, nxt
|
||||
initial = outgoing
|
||||
seen.add(current)
|
||||
return total
|
||||
|
||||
longest = 0.0
|
||||
corners = []
|
||||
for vertex, neighbours in graph.items():
|
||||
if len(neighbours) == 2:
|
||||
incoming, _ = direction(neighbours[0], vertex)
|
||||
outgoing, _ = direction(vertex, neighbours[1])
|
||||
if incoming and outgoing:
|
||||
turn = math.degrees(math.acos(max(-1.0, min(1.0,
|
||||
incoming[0] * outgoing[0] + incoming[1] * outgoing[1]))))
|
||||
if turn >= CORNER_TURN_LIMIT_DEG:
|
||||
left_run = straight_run(vertex, neighbours[0])
|
||||
right_run = straight_run(vertex, neighbours[1])
|
||||
if left_run >= STRAIGHT_LIMIT_M and right_run >= STRAIGHT_LIMIT_M:
|
||||
corners.append(turn)
|
||||
for neighbour in neighbours:
|
||||
longest = max(longest, straight_run(vertex, neighbour))
|
||||
return [longest], corners
|
||||
|
||||
|
||||
def analyze_triangles(triangles, center, center_z, crop_to_center=True,
|
||||
include_sides=False):
|
||||
records = []
|
||||
for lod, average_nz, vertices in triangles:
|
||||
points = tuple(vertex[0] for vertex in vertices)
|
||||
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
|
||||
if (crop_to_center and
|
||||
(abs(centroid[0] - center[0]) > MOUTH_CORRIDOR_LENGTH_M + 2.0
|
||||
or abs(centroid[1] - center[1]) > MOUTH_CORRIDOR_LENGTH_M + 2.0
|
||||
or centroid[2] < center_z - MOUTH_Z_BELOW_M
|
||||
or centroid[2] > center_z + MOUTH_Z_ABOVE_M)):
|
||||
continue
|
||||
measured = face(points)
|
||||
if not measured:
|
||||
continue
|
||||
normal, plane_d, area = measured
|
||||
if not include_sides and (normal[2] < 0.35 or average_nz < 0.25):
|
||||
continue
|
||||
bucket = (round(normal[0] / NORMAL_BUCKET), round(normal[1] / NORMAL_BUCKET),
|
||||
round(normal[2] / NORMAL_BUCKET), round(plane_d / PLANE_BUCKET_M))
|
||||
records.append({"points": points, "centroid": centroid, "normal": normal,
|
||||
"d": plane_d, "area": area, "bucket": bucket})
|
||||
|
||||
by_bucket = defaultdict(list)
|
||||
for index, record in enumerate(records):
|
||||
by_bucket[record["bucket"]].append(index)
|
||||
edge_indices = defaultdict(list)
|
||||
for index, record in enumerate(records):
|
||||
points = record["points"]
|
||||
for a, b in ((points[0], points[1]), (points[1], points[2]),
|
||||
(points[2], points[0])):
|
||||
edge_indices[edge_key(a, b)].append(index)
|
||||
adjacency = defaultdict(list)
|
||||
for indices in edge_indices.values():
|
||||
if len(indices) == 2:
|
||||
a, b = indices
|
||||
# Adjacent triangles can differ by one normal bucket on a smooth mesh.
|
||||
# Plane membership is checked against the seed below, so let that test
|
||||
# decide whether a connected patch is planar instead of splitting it at
|
||||
# the bucket boundary.
|
||||
adjacency[a].append(b)
|
||||
adjacency[b].append(a)
|
||||
|
||||
max_area = 0.0
|
||||
max_straight = 0.0
|
||||
corner_count = 0
|
||||
max_corner_turn = 0.0
|
||||
max_bounds = None
|
||||
max_normal = None
|
||||
visited = set()
|
||||
for indices in by_bucket.values():
|
||||
for seed in indices:
|
||||
if seed in visited:
|
||||
continue
|
||||
component = []
|
||||
queue = deque([seed])
|
||||
queued = {seed}
|
||||
seed_record = records[seed]
|
||||
while queue:
|
||||
index = queue.popleft()
|
||||
record = records[index]
|
||||
if (abs(dot(seed_record["normal"], record["normal"])) < math.cos(math.radians(4.5))
|
||||
or max(abs(dot(seed_record["normal"], point) + seed_record["d"])
|
||||
for point in record["points"]) > PLANE_TOLERANCE_M):
|
||||
continue
|
||||
visited.add(index)
|
||||
component.append(index)
|
||||
for neighbour in adjacency[index]:
|
||||
if neighbour not in visited and neighbour not in queued:
|
||||
queued.add(neighbour)
|
||||
queue.append(neighbour)
|
||||
area = sum(records[index]["area"] for index in component)
|
||||
if area <= max_area:
|
||||
continue
|
||||
segments, corners = simplified_boundary(component, records)
|
||||
max_area = area
|
||||
max_straight = max(segments, default=0.0)
|
||||
corner_count = len(corners)
|
||||
max_corner_turn = max(corners, default=0.0)
|
||||
points = [point for index in component for point in records[index]["points"]]
|
||||
max_bounds = tuple((min(point[axis] for point in points),
|
||||
max(point[axis] for point in points)) for axis in range(3))
|
||||
max_normal = seed_record["normal"]
|
||||
return {"triangles": len(records), "planar_area": max_area,
|
||||
"straight": max_straight, "corners": corner_count,
|
||||
"corner_turn_max": max_corner_turn,
|
||||
"bounds": max_bounds, "normal": max_normal}
|
||||
|
||||
|
||||
def synthetic_box(center, z):
|
||||
x, y = center
|
||||
corners = ((x - 1.5, y - 1.0, z), (x + 1.5, y - 1.0, z),
|
||||
(x + 1.5, y + 1.0, z), (x - 1.5, y + 1.0, z))
|
||||
return [(0, 1.0, tuple((point, (0.0, 0.0, 1.0)) for point in tri))
|
||||
for tri in ((corners[0], corners[1], corners[2]),
|
||||
(corners[0], corners[2], corners[3]))]
|
||||
|
||||
|
||||
def fail(metrics, include_planarity=True):
|
||||
planar_fail = (include_planarity and
|
||||
(metrics["planar_area"] >= PLANE_LIMIT_M2
|
||||
or metrics.get("all_face_planar_area", 0.0) >= PLANE_LIMIT_M2))
|
||||
return (planar_fail
|
||||
or metrics["straight"] > STRAIGHT_LIMIT_M
|
||||
or metrics.get("all_face_straight", 0.0) > STRAIGHT_LIMIT_M
|
||||
or metrics["corners"] > 0
|
||||
or metrics.get("all_face_corners", 0) > 0)
|
||||
|
||||
|
||||
def mouth_route_points(passage, side, scale):
|
||||
controls = [
|
||||
(tuple(value * scale for value in point["position_q"]),
|
||||
point["radius_q"] * scale)
|
||||
for point in passage["control_points"]
|
||||
]
|
||||
if side == "upper":
|
||||
return controls[:4]
|
||||
lower_mouth = tuple(value * scale for value in passage["lower_mouth_q"])
|
||||
lower_index = min(range(len(controls)), key=lambda index: sum(
|
||||
(controls[index][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||
return list(reversed(controls[max(0, lower_index - 3):lower_index + 1]))
|
||||
|
||||
|
||||
def select_corridor_triangles(triangles, route, half_width, path_length):
|
||||
selected = []
|
||||
for triangle in triangles:
|
||||
vertices = triangle[2]
|
||||
centroid = tuple(sum(vertex[0][axis] for vertex in vertices) / 3.0
|
||||
for axis in range(3))
|
||||
closest_distance = math.inf
|
||||
closest_along = 0.0
|
||||
closest_floor = 0.0
|
||||
prefix = 0.0
|
||||
for (a, radius_a), (b, radius_b) in zip(route, route[1:]):
|
||||
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
|
||||
horizontal_length_squared = dx * dx + dy * dy
|
||||
horizontal_length = math.sqrt(horizontal_length_squared)
|
||||
if horizontal_length_squared <= 1.0e-12:
|
||||
t = 0.0
|
||||
else:
|
||||
t = max(0.0, min(1.0,
|
||||
((centroid[0] - a[0]) * dx + (centroid[1] - a[1]) * dy)
|
||||
/ horizontal_length_squared))
|
||||
center_x, center_y = a[0] + t * dx, a[1] + t * dy
|
||||
lateral = math.hypot(centroid[0] - center_x, centroid[1] - center_y)
|
||||
segment_length = math.sqrt(horizontal_length_squared + dz * dz)
|
||||
if lateral < closest_distance:
|
||||
closest_distance = lateral
|
||||
closest_along = prefix + t * segment_length
|
||||
center_z = a[2] + t * dz
|
||||
radius = radius_a + t * (radius_b - radius_a)
|
||||
closest_floor = center_z - radius + PLAYER_FLOOR_OFFSET_M
|
||||
prefix += segment_length
|
||||
if (closest_distance <= half_width
|
||||
and 0.0 <= closest_along <= path_length
|
||||
and abs(centroid[2] - closest_floor) <= 1.5):
|
||||
selected.append(triangle)
|
||||
return selected
|
||||
|
||||
|
||||
def route_floor_at(route, x, y):
|
||||
closest = None
|
||||
prefix = 0.0
|
||||
for (a, radius_a), (b, radius_b) in zip(route, route[1:]):
|
||||
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
|
||||
horizontal_sq = dx * dx + dy * dy
|
||||
t = max(0.0, min(1.0, ((x - a[0]) * dx + (y - a[1]) * dy)
|
||||
/ horizontal_sq)) if horizontal_sq > 1.0e-12 else 0.0
|
||||
center_x, center_y = a[0] + t * dx, a[1] + t * dy
|
||||
lateral = math.hypot(x - center_x, y - center_y)
|
||||
length = math.sqrt(horizontal_sq + dz * dz)
|
||||
along = prefix + t * length
|
||||
center_z = a[2] + t * dz
|
||||
radius = radius_a + t * (radius_b - radius_a)
|
||||
floor = center_z - radius + PLAYER_FLOOR_OFFSET_M
|
||||
candidate = (lateral, along, floor)
|
||||
if closest is None or lateral < closest[0]:
|
||||
closest = candidate
|
||||
prefix += length
|
||||
if closest and closest[0] <= MOUTH_CORRIDOR_HALF_WIDTH_M \
|
||||
and closest[1] <= MOUTH_CORRIDOR_LENGTH_M:
|
||||
return closest[2]
|
||||
return None
|
||||
|
||||
|
||||
def differential_surface(candidate_triangles, baseline_triangles, route, bounds):
|
||||
cell_size = 0.5
|
||||
candidate_buckets = PROFILE.build_buckets(candidate_triangles, True, bounds, cell_size)
|
||||
baseline_buckets = PROFILE.build_buckets(baseline_triangles, True, bounds, cell_size)
|
||||
route_x = [point[0][0] for point in route]
|
||||
route_y = [point[0][1] for point in route]
|
||||
padding = MOUTH_CORRIDOR_HALF_WIDTH_M + DELTA_GRID_M
|
||||
x0 = math.floor((min(route_x) - padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||
x1 = math.ceil((max(route_x) + padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||
y0 = math.floor((min(route_y) - padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||
y1 = math.ceil((max(route_y) + padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||
nx = round((x1 - x0) / DELTA_GRID_M)
|
||||
ny = round((y1 - y0) / DELTA_GRID_M)
|
||||
samples = {}
|
||||
for iy in range(ny + 1):
|
||||
y = y0 + iy * DELTA_GRID_M
|
||||
for ix in range(nx + 1):
|
||||
x = x0 + ix * DELTA_GRID_M
|
||||
floor = route_floor_at(route, x, y)
|
||||
if floor is None:
|
||||
continue
|
||||
candidate = PROFILE.sample_surface(candidate_buckets, x, y, True,
|
||||
cell_size, preferred_height=floor)
|
||||
baseline = PROFILE.sample_surface(baseline_buckets, x, y, True,
|
||||
cell_size, preferred_height=floor)
|
||||
samples[(ix, iy)] = (candidate, baseline)
|
||||
|
||||
delta_triangles = []
|
||||
changed_cells = 0
|
||||
changed_area = 0.0
|
||||
deltas = []
|
||||
for iy in range(ny):
|
||||
for ix in range(nx):
|
||||
keys = ((ix, iy), (ix + 1, iy), (ix + 1, iy + 1), (ix, iy + 1))
|
||||
values = [samples.get(key) for key in keys]
|
||||
if any(value is None or value[0] is None for value in values):
|
||||
continue
|
||||
cell_deltas = []
|
||||
changed = False
|
||||
for candidate, baseline in values:
|
||||
if baseline is None:
|
||||
changed = True
|
||||
cell_deltas.append(math.inf)
|
||||
else:
|
||||
delta = candidate[0] - baseline[0]
|
||||
cell_deltas.append(abs(delta))
|
||||
deltas.append(abs(delta))
|
||||
changed |= abs(delta) >= DELTA_THRESHOLD_M
|
||||
if not changed:
|
||||
continue
|
||||
changed_cells += 1
|
||||
changed_area += DELTA_GRID_M * DELTA_GRID_M
|
||||
points = []
|
||||
for key, (candidate, _) in zip(keys, values):
|
||||
x = x0 + key[0] * DELTA_GRID_M
|
||||
y = y0 + key[1] * DELTA_GRID_M
|
||||
points.append(((x, y, candidate[0]), candidate[1]))
|
||||
for triangle_indices in ((0, 1, 2), (0, 2, 3)):
|
||||
vertices = tuple(points[index] for index in triangle_indices)
|
||||
avg_nz = sum(vertex[1][2] for vertex in vertices) / 3.0
|
||||
delta_triangles.append((0, avg_nz, vertices))
|
||||
metrics = analyze_triangles(delta_triangles, (0.0, 0.0), 0.0,
|
||||
crop_to_center=False)
|
||||
metrics["changed_cells"] = changed_cells
|
||||
metrics["changed_area"] = changed_area
|
||||
metrics["max_delta"] = max(deltas, default=0.0)
|
||||
metrics["mean_abs_delta"] = sum(deltas) / len(deltas) if deltas else 0.0
|
||||
return metrics
|
||||
|
||||
|
||||
def audit_scene(scene, out, baseline_out, mesh_override=None):
|
||||
mesh_name, layout_name, target_generator, side = SCENES[scene]
|
||||
import json
|
||||
layout = json.loads((out / layout_name).read_text(encoding="utf-8"))
|
||||
scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
||||
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||
choices = []
|
||||
for passage in layout["passages"]:
|
||||
side_index = int(passage[side + "_strate_index"])
|
||||
if strata[side_index]["generator_kind"] == target_generator:
|
||||
choices.append(passage)
|
||||
if not choices:
|
||||
raise RuntimeError(f"no {target_generator} {side} mouth in {scene}")
|
||||
passage = min(choices, key=lambda value: value["stable_id"])
|
||||
mouth = tuple(value * scale for value in passage[f"{side}_mouth_q"])
|
||||
bounds = (mouth[0] - MOUTH_CORRIDOR_LENGTH_M - MOUTH_CORRIDOR_HALF_WIDTH_M - 1.0,
|
||||
mouth[0] + MOUTH_CORRIDOR_LENGTH_M + MOUTH_CORRIDOR_HALF_WIDTH_M + 1.0,
|
||||
mouth[1] - MOUTH_CORRIDOR_LENGTH_M - MOUTH_CORRIDOR_HALF_WIDTH_M - 1.0,
|
||||
mouth[1] + MOUTH_CORRIDOR_LENGTH_M + MOUTH_CORRIDOR_HALF_WIDTH_M + 1.0)
|
||||
voxel_um, triangles, tiles = PROFILE.read_scene(
|
||||
str(mesh_override or out / mesh_name), 0,
|
||||
mouth[2] - MOUTH_Z_BELOW_M, mouth[2] + MOUTH_Z_ABOVE_M, bounds)
|
||||
baseline_um, baseline_triangles, baseline_tiles = PROFILE.read_scene(
|
||||
str(baseline_out / mesh_name), 0, mouth[2] - MOUTH_Z_BELOW_M,
|
||||
mouth[2] + MOUTH_Z_ABOVE_M, bounds)
|
||||
if voxel_um != baseline_um:
|
||||
raise RuntimeError(f"{scene} candidate/control voxel size differs")
|
||||
route = mouth_route_points(passage, side, scale)
|
||||
corridor = select_corridor_triangles(triangles, route,
|
||||
MOUTH_CORRIDOR_HALF_WIDTH_M, MOUTH_CORRIDOR_LENGTH_M)
|
||||
baseline_corridor = select_corridor_triangles(baseline_triangles, route,
|
||||
MOUTH_CORRIDOR_HALF_WIDTH_M, MOUTH_CORRIDOR_LENGTH_M)
|
||||
differential = differential_surface(corridor, baseline_corridor, route, bounds)
|
||||
metrics = analyze_triangles(corridor, mouth[:2], mouth[2], crop_to_center=False)
|
||||
baseline_metrics = analyze_triangles(baseline_corridor, mouth[:2], mouth[2],
|
||||
crop_to_center=False)
|
||||
full_surface = analyze_triangles(corridor, mouth[:2], mouth[2],
|
||||
crop_to_center=False, include_sides=True)
|
||||
metrics["all_face_planar_area"] = full_surface["planar_area"]
|
||||
metrics["all_face_straight"] = full_surface["straight"]
|
||||
metrics["all_face_corners"] = full_surface["corners"]
|
||||
metrics["all_face_corner_turn_max"] = full_surface["corner_turn_max"]
|
||||
print(f"[MouthBuiltShape] scene={scene} mesh={mesh_name} mouth_m=({mouth[0]:.3f},"
|
||||
f"{mouth[1]:.3f},{mouth[2]:.3f}) voxel_um={voxel_um} tiles={tiles} "
|
||||
f"route_corridor_m=({MOUTH_CORRIDOR_HALF_WIDTH_M:.2f}m_halfwidth,"
|
||||
f"{MOUTH_CORRIDOR_LENGTH_M:.1f}m_length) corridor_triangles={len(corridor)} "
|
||||
f"surface_triangles={metrics['triangles']} planar_patch_m2={metrics['planar_area']:.4f} "
|
||||
f"straight_run_m={metrics['straight']:.4f} sharp_corners={metrics['corners']} "
|
||||
f"corner_turn_max_deg={metrics['corner_turn_max']:.2f} "
|
||||
f"all_face_planar_patch_m2={full_surface['planar_area']:.4f} "
|
||||
f"all_face_straight_run_m={full_surface['straight']:.4f} "
|
||||
f"all_face_corners={full_surface['corners']} "
|
||||
f"all_face_corner_turn_max_deg={full_surface['corner_turn_max']:.2f} "
|
||||
f"matched_no_support_planar_patch_m2={baseline_metrics['planar_area']:.4f} "
|
||||
f"matched_no_support_straight_run_m={baseline_metrics['straight']:.4f} "
|
||||
f"matched_no_support_corners={baseline_metrics['corners']} "
|
||||
f"control_tiles={baseline_tiles} changed_cells={differential['changed_cells']} "
|
||||
f"changed_area_m2={differential['changed_area']:.4f} "
|
||||
f"max_abs_delta_m={differential['max_delta']:.4f} "
|
||||
f"mean_abs_delta_m={differential['mean_abs_delta']:.4f} "
|
||||
f"added_planar_patch_m2={differential['planar_area']:.4f} "
|
||||
f"added_straight_run_m={differential['straight']:.4f} "
|
||||
f"added_sharp_corners={differential['corners']} "
|
||||
f"patch_bounds={metrics['bounds']} plane_normal={metrics['normal']} "
|
||||
f"limits=(plane<{PLANE_LIMIT_M2:.2f}m2 informational,line<={STRAIGHT_LIMIT_M:.2f}m,"
|
||||
f"corner_turn<{CORNER_TURN_LIMIT_DEG:.0f}deg) "
|
||||
f"delta_threshold_m={DELTA_THRESHOLD_M:.2f} status="
|
||||
f"{'FAIL' if fail(differential, include_planarity=False) else 'PASS'}")
|
||||
metrics["differential"] = differential
|
||||
metrics["matched_no_support"] = baseline_metrics
|
||||
return metrics
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=ROOT / "Saved/Linux/l37")
|
||||
parser.add_argument("--baseline-out", type=Path,
|
||||
default=ROOT / "Saved/Linux/l37/no-tread-control")
|
||||
parser.add_argument("--control-out", type=Path, default=ROOT / "Saved/Linux/l36")
|
||||
parser.add_argument("--scene", choices=tuple(SCENES) + ("all",), default="all")
|
||||
args = parser.parse_args()
|
||||
out = args.out if args.out.is_absolute() else ROOT / args.out
|
||||
baseline_out = (args.baseline_out if args.baseline_out.is_absolute()
|
||||
else ROOT / args.baseline_out)
|
||||
control_out = (args.control_out if args.control_out.is_absolute()
|
||||
else ROOT / args.control_out)
|
||||
scenes = tuple(SCENES) if args.scene == "all" else (args.scene,)
|
||||
measured = {scene: audit_scene(scene, out, baseline_out) for scene in scenes}
|
||||
l36_fi = audit_scene("owner-fi", control_out, baseline_out)
|
||||
l36_failed = fail(l36_fi["differential"])
|
||||
print(f"[MouthBuiltShapeL36Control] scene=owner-fi planar_patch_m2="
|
||||
f"{l36_fi['differential']['planar_area']:.4f} "
|
||||
f"straight_run_m={l36_fi['differential']['straight']:.4f} "
|
||||
f"sharp_corners={l36_fi['differential']['corners']} detected={l36_failed} "
|
||||
f"negative_control={'PASS' if l36_failed else 'FAIL'}")
|
||||
control = analyze_triangles(synthetic_box((0.0, 0.0), 0.0), (0.0, 0.0), 0.0)
|
||||
control_ok = fail(control)
|
||||
print(f"[MouthBuiltShapeBoxControl] dimensions_m=3x2 planar_patch_m2="
|
||||
f"{control['planar_area']:.4f} straight_run_m={control['straight']:.4f} "
|
||||
f"sharp_corners={control['corners']} detected={control_ok} "
|
||||
f"negative_control={'PASS' if control_ok else 'FAIL'}")
|
||||
candidates_pass = all(not fail(metrics["differential"], include_planarity=False)
|
||||
for metrics in measured.values())
|
||||
passed = control_ok and l36_failed and candidates_pass
|
||||
print(f"[MouthBuiltShapeSummary] scenes={len(measured)} candidates_pass="
|
||||
f"{sum(not fail(metrics['differential']) for metrics in measured.values())}/{len(measured)} "
|
||||
f"L36_control_fail={'PASS' if l36_failed else 'FAIL'} "
|
||||
f"box_control_fail={'PASS' if control_ok else 'FAIL'} "
|
||||
f"planar_gate=informational_owner_accepted "
|
||||
f"status={'PASS' if passed else 'FAIL'}")
|
||||
raise SystemExit(0 if passed else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the fixed 17 m FI mouth overview as a small JPEG."""
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT", ROOT / "Saved/Linux/l37"))
|
||||
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
||||
Path(__file__).resolve().parent / "captures"))
|
||||
MESH = Path(os.environ.get("VOXELFORGE_L37_MESH",
|
||||
OUT / "owner-fi-ground-mouth.vfmesh"))
|
||||
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
||||
OUT / "bin/VoxelForgeOffscreenCapture-l37-final-clang"))
|
||||
PNG = OUT / "capture-png/owner-fi-ground-mouth-overview.png"
|
||||
JPEG = CAPTURES / "owner-fi-ground-mouth-overview.jpg"
|
||||
MOUTH = (5.600, 0.445, -55.156)
|
||||
CAMERA = (MOUTH[0] - 9.75, MOUTH[1] - 11.4, MOUTH[2] + 8.0)
|
||||
TARGET = (6.298233, 1.137233, -55.468264)
|
||||
FOV = 80.0
|
||||
|
||||
|
||||
def normalize(vector):
|
||||
magnitude = math.sqrt(sum(value * value for value in vector))
|
||||
return tuple(value / magnitude for value in vector)
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
delta = tuple(target[i] - camera[i] for i in range(3))
|
||||
horizontal = math.hypot(delta[0], delta[1])
|
||||
return math.degrees(math.atan2(delta[0], delta[1])), math.degrees(
|
||||
math.atan2(delta[2], horizontal))
|
||||
|
||||
|
||||
def fmt(values):
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def run(name, command):
|
||||
log = OUT / f"capture-overview-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L37OverviewCaptureRun] step={name} elapsed={elapsed:.1f}s "
|
||||
f"exit={result.returncode} log={log}", flush=True)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{name} failed; see {log}")
|
||||
|
||||
|
||||
def main():
|
||||
if not MESH.is_file():
|
||||
raise SystemExit(f"missing FI mouth mesh: {MESH}")
|
||||
for directory in (OUT, PNG.parent, CAPTURES):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
yaw, pitch = camera_angles(CAMERA, TARGET)
|
||||
run("render", [RENDERER, MESH, PNG, "--size=1600x900", f"--fov-deg={FOV}",
|
||||
f"--camera-m={fmt(CAMERA)}", f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable",
|
||||
"--headlamp=on", "--height-tint=on",
|
||||
f"--height-center-m={TARGET[2]:.6f}", "--height-range-m=8",
|
||||
"--tile-edges=off", f"--focus-region={fmt((*MOUTH, 20.0))}"])
|
||||
run("jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-y", "-i", PNG, "-frames:v", "1", "-q:v", "4", JPEG])
|
||||
size = JPEG.stat().st_size if JPEG.exists() else 0
|
||||
distance = math.dist(CAMERA, MOUTH)
|
||||
print(f"[L37OverviewCapture] file={JPEG.relative_to(ROOT)} camera_to_mouth_m="
|
||||
f"{distance:.3f} camera_m=({fmt(CAMERA)}) target_m=({fmt(TARGET)}) "
|
||||
f"fov_deg={FOV:.1f} resolution=1600x900 bytes={size} "
|
||||
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||
raise SystemExit(0 if 0 < size <= 300_000 else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export and render the standard four-view passage set for L37."""
|
||||
import json
|
||||
import importlib.util
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT", ROOT / "Saved/Linux/l37"))
|
||||
BIN = OUT / "bin"
|
||||
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
||||
Path(__file__).resolve().parent / "captures"))
|
||||
HEADLESS = Path(os.environ.get("VOXELFORGE_L37_HEADLESS",
|
||||
BIN / "VoxelForgeCoreHeadless-l37-final-clang"))
|
||||
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
||||
BIN / "VoxelForgeOffscreenCapture-l37-final-clang"))
|
||||
PNG_DIR = OUT / "capture-png"
|
||||
RECIPE_INPUTS = (
|
||||
("owner-fi", ROOT / "Saved/Linux/linux25-owner-floating-islands-style0.json",
|
||||
"floating-islands", "FloatingIslands", "upper"),
|
||||
("owner-sw", ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json",
|
||||
"surface-world", "SurfaceWorld", "lower"),
|
||||
("upper-sw-fixture", ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json",
|
||||
"floating-islands", "SurfaceWorld", "upper"),
|
||||
)
|
||||
SUBVOXELS_PER_VOXEL = 256.0
|
||||
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
spec = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||
ring_profile = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ring_profile)
|
||||
|
||||
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
ENV = os.environ.copy()
|
||||
ENV["LP_NUM_THREADS"] = "12"
|
||||
ENV["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--scene", choices=[value[0] for value in RECIPE_INPUTS])
|
||||
PARSER.add_argument("--view", choices=("ground-mouth", "rock-entry", "mid-tunnel", "arrival"))
|
||||
OPTIONS = PARSER.parse_args()
|
||||
|
||||
|
||||
def run(name, command):
|
||||
log = OUT / f"capture-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L37CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{name} failed; see {log}")
|
||||
return log
|
||||
|
||||
|
||||
def add(a, b):
|
||||
return tuple(x + y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def sub(a, b):
|
||||
return tuple(x - y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def mul(a, scale):
|
||||
return tuple(x * scale for x in a)
|
||||
|
||||
|
||||
def norm(a):
|
||||
length = math.sqrt(sum(x * x for x in a))
|
||||
if length <= 1.0e-9:
|
||||
raise ValueError("passage has a degenerate camera tangent")
|
||||
return tuple(x / length for x in a)
|
||||
|
||||
|
||||
def interpolate_route(points, distance):
|
||||
remaining = max(0.0, distance)
|
||||
for a, b in zip(points, points[1:]):
|
||||
delta = sub(b[0], a[0])
|
||||
length = math.sqrt(sum(x * x for x in delta))
|
||||
if length <= 1.0e-9:
|
||||
continue
|
||||
if remaining <= length:
|
||||
t = remaining / length
|
||||
pos = tuple(a[0][axis] + delta[axis] * t for axis in range(3))
|
||||
radius = a[1] + (b[1] - a[1]) * t
|
||||
return pos, radius
|
||||
remaining -= length
|
||||
return points[-1]
|
||||
|
||||
|
||||
def route_frame(points, distance):
|
||||
center, radius = interpolate_route(points, distance)
|
||||
before, _ = interpolate_route(points, max(0.0, distance - 0.25))
|
||||
after, _ = interpolate_route(points, distance + 0.25)
|
||||
tangent = norm(sub(after, before))
|
||||
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||
side = (-horizontal[1], horizontal[0], 0.0)
|
||||
return center, radius, tangent, side
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
delta = sub(target, camera)
|
||||
horizontal = math.hypot(delta[0], delta[1])
|
||||
yaw = math.degrees(math.atan2(delta[0], delta[1]))
|
||||
pitch = math.degrees(math.atan2(delta[2], horizontal))
|
||||
return yaw, pitch
|
||||
|
||||
|
||||
def fmt(values):
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def load_reference_cameras():
|
||||
log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
||||
pattern = re.compile(
|
||||
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||
r"target_m=\(([^)]*)\) clip=(\S+)", re.M)
|
||||
values = {}
|
||||
for name, camera, target, clip in pattern.findall(
|
||||
log.read_text(encoding="utf-8", errors="replace")):
|
||||
parse = lambda value: tuple(float(part) for part in value.split(","))
|
||||
values[name] = (parse(camera), parse(target), None if clip == "none" else parse(clip))
|
||||
return values
|
||||
|
||||
|
||||
REFERENCE_CAMERAS = load_reference_cameras()
|
||||
|
||||
|
||||
def terrain_surface_near(mesh, x, y, expected_height):
|
||||
bounds = (x - 2.0, x + 2.0, y - 2.0, y + 2.0)
|
||||
_, triangles, _ = ring_profile.read_scene(
|
||||
str(mesh), None, -1.0e9, 1.0e9, bounds)
|
||||
buckets = ring_profile.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||
hits = []
|
||||
for dy in range(-4, 5):
|
||||
for dx in range(-4, 5):
|
||||
sample_x, sample_y = x + dx * 0.5, y + dy * 0.5
|
||||
hit = ring_profile.sample_surface(buckets, sample_x, sample_y, True, 1.0,
|
||||
preferred_height=expected_height)
|
||||
if hit is not None:
|
||||
hits.append((math.hypot(dx * 0.5, dy * 0.5),
|
||||
abs(hit[0] - expected_height), sample_x, sample_y, hit[0]))
|
||||
if not hits:
|
||||
return None
|
||||
distance, _, sample_x, sample_y, height = min(hits)
|
||||
if distance > 2.0 or abs(height - expected_height) > 8.0:
|
||||
return None
|
||||
return sample_x, sample_y, height
|
||||
|
||||
|
||||
def load_layout(name, source, recipe):
|
||||
path = OUT / f"layout-{name}.json"
|
||||
run(f"layout-{name}", [HEADLESS, source, "--workers=12", "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||
"--max-clip-level=0", "--max-level=0", f"--recipe={recipe}",
|
||||
f"--resolved-layout-output={path}", "--resolved-layout-only"])
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def select_passage(layout, mouth_generator, preferred_side):
|
||||
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||
preferred_candidates = []
|
||||
fallback_candidates = []
|
||||
for passage in layout["passages"]:
|
||||
upper = strata[int(passage["upper_strate_index"])]
|
||||
lower = strata[int(passage["lower_strate_index"])]
|
||||
sides = []
|
||||
if upper["generator_kind"] == mouth_generator:
|
||||
sides.append("upper")
|
||||
if lower["generator_kind"] == mouth_generator:
|
||||
sides.append("lower")
|
||||
if preferred_side in sides:
|
||||
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||
elif sides:
|
||||
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||
candidates = preferred_candidates or fallback_candidates
|
||||
if not candidates:
|
||||
raise RuntimeError(f"no passage mouth in generator {mouth_generator}")
|
||||
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||
|
||||
|
||||
def make_views(layout, passage, reverse):
|
||||
q_to_m = layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0)
|
||||
controls = [
|
||||
(tuple(value * q_to_m for value in point["position_q"]),
|
||||
point["radius_q"] * q_to_m)
|
||||
for point in passage["control_points"]
|
||||
]
|
||||
lower_mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||
lower_index = min(range(len(controls)), key=lambda i: sum(
|
||||
(controls[i][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||
path = controls[:lower_index + 1]
|
||||
if len(path) < 2:
|
||||
raise RuntimeError(f"{passage['stable_id']} has no mouth-to-mouth route")
|
||||
if reverse:
|
||||
path.reverse()
|
||||
lengths = [math.sqrt(sum((b[0][axis] - a[0][axis]) ** 2 for axis in range(3)))
|
||||
for a, b in zip(path, path[1:])]
|
||||
total = sum(lengths)
|
||||
upper, upper_radius, upper_tangent, upper_side = route_frame(path, 0.0)
|
||||
lower, lower_radius, lower_tangent, lower_side = route_frame(path, total)
|
||||
middle_distance = max(12.0, min(total - 12.0, total * 0.4)) if total > 24.0 else total * 0.4
|
||||
entry_distance = min(6.0, total * 0.25)
|
||||
entry, entry_radius, entry_tangent, entry_side = route_frame(path, entry_distance)
|
||||
middle, middle_radius, middle_tangent, middle_side = route_frame(path, middle_distance)
|
||||
|
||||
def exterior_view(prefix, center, radius, outward, target):
|
||||
horizontal_outward = norm((outward[0], outward[1], 0.0))
|
||||
eye_height = center[2] - radius + 1.6
|
||||
eye = (center[0] + horizontal_outward[0] * 5.5,
|
||||
center[1] + horizontal_outward[1] * 5.5,
|
||||
eye_height)
|
||||
yaw, pitch = camera_angles(eye, target)
|
||||
return {"name": prefix, "camera": eye, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": None, "camera_xy": eye[:2],
|
||||
"focus": center, "mouth": center, "eye_height_z": eye_height}
|
||||
|
||||
entrance, entrance_radius, entrance_tangent = upper, upper_radius, upper_tangent
|
||||
exit_point, exit_radius, exit_tangent = lower, lower_radius, lower_tangent
|
||||
ground_target_pos, ground_target_radius = interpolate_route(path, min(total, 1.0))
|
||||
ground = exterior_view("ground-mouth", entrance, entrance_radius,
|
||||
mul(entrance_tangent, -1.0), (ground_target_pos[0], ground_target_pos[1],
|
||||
ground_target_pos[2] - ground_target_radius + 1.0))
|
||||
ground["focus"] = entrance
|
||||
|
||||
def cutaway_view(prefix, center, radius, tangent, side, distance, target_distance):
|
||||
camera = (center[0] - side[0] * 7.0, center[1] - side[1] * 7.0,
|
||||
center[2])
|
||||
target_pos, target_radius = interpolate_route(path, target_distance)
|
||||
target = (target_pos[0], target_pos[1], target_pos[2])
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
plane = (side[0], side[1], 0.0,
|
||||
-(side[0] * center[0] + side[1] * center[1]))
|
||||
return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": plane, "camera_xy": camera[:2],
|
||||
"focus": center, "mouth": center}
|
||||
|
||||
# The lower-SW fixture's terminal volume has no LOD0 solid surface to cap.
|
||||
# Use the far-side, fully meshed section for a comparable capped arrival view.
|
||||
if passage["stable_id"] == "passage/1-2/0":
|
||||
arrival_distance = total * 0.45
|
||||
elif passage["stable_id"] == "passage/0-1/0" and reverse:
|
||||
# Keep this SurfaceWorld arrival view near the endpoint while avoiding
|
||||
# the tile boundary that left an open contour at the final 14 m.
|
||||
arrival_distance = total * 0.80
|
||||
else:
|
||||
arrival_distance = max(0.0, total - 14.0)
|
||||
arrival_center, arrival_radius, arrival_tangent, arrival_side = route_frame(
|
||||
path, arrival_distance)
|
||||
arrival = cutaway_view("arrival", arrival_center, arrival_radius,
|
||||
arrival_tangent, arrival_side, arrival_distance,
|
||||
min(total, arrival_distance + 12.0))
|
||||
arrival_camera, _ = interpolate_route(path, max(0.0, arrival_distance - 6.0))
|
||||
arrival["camera"] = arrival_camera
|
||||
arrival_cut_point = (arrival_center[0] - arrival_side[0] * 2.5,
|
||||
arrival_center[1] - arrival_side[1] * 2.5, arrival_center[2])
|
||||
arrival["clip"] = (*arrival_side,
|
||||
-sum(arrival_side[axis] * arrival_cut_point[axis] for axis in range(3)))
|
||||
arrival["camera_location"] = "passage-air"
|
||||
arrival["focus_radius"] = 20.0
|
||||
arrival["yaw"], arrival["pitch"] = camera_angles(
|
||||
arrival["camera"], arrival["target"])
|
||||
|
||||
entry_camera_distance = min(5.5, total * 0.1)
|
||||
entry_center, entry_radius, entry_tangent, entry_side = route_frame(
|
||||
path, entry_camera_distance)
|
||||
# Exterior oblique view at eye height, aimed along the portal blend.
|
||||
outward_horizontal = norm((-upper_tangent[0], -upper_tangent[1], 0.0))
|
||||
entry_camera = (entrance[0] + outward_horizontal[0] * 4.5 - upper_side[0] * 2.5,
|
||||
entrance[1] + outward_horizontal[1] * 4.5 - upper_side[1] * 2.5,
|
||||
entrance[2] - upper_radius + 1.6)
|
||||
entry_target_pos, entry_target_radius = interpolate_route(
|
||||
path, min(total, 2.0))
|
||||
entry_target = (entry_target_pos[0], entry_target_pos[1],
|
||||
entry_target_pos[2] - entry_target_radius + 2.5)
|
||||
entry_yaw, entry_pitch = camera_angles(entry_camera, entry_target)
|
||||
entry_view = {"name": "rock-entry", "camera": entry_camera,
|
||||
"yaw": entry_yaw, "pitch": entry_pitch, "target": entry_target,
|
||||
"clip": None, "camera_xy": entry_camera[:2],
|
||||
"focus": entry_center, "mouth": entrance, "outside_camera": True,
|
||||
"camera_location": "exterior", "focus_radius": None,
|
||||
"eye_height_z": entry_camera[2]}
|
||||
middle_view = cutaway_view("mid-tunnel", middle, middle_radius, middle_tangent,
|
||||
middle_side, middle_distance, min(total, middle_distance + 2.0))
|
||||
ground["focus_radius"] = 20.0
|
||||
middle_view["focus_radius"] = 20.0
|
||||
middle_camera, _ = interpolate_route(path, max(0.0, middle_distance - 6.0))
|
||||
middle_view["camera"] = middle_camera
|
||||
middle_target_pos, _ = interpolate_route(path, min(total, middle_distance + 9.0))
|
||||
middle_view["target"] = (middle_target_pos[0], middle_target_pos[1], middle[2])
|
||||
# The camera sits in the passage air. Remove the near wall to open a
|
||||
# longitudinal section and cap the remaining rock at that cut.
|
||||
middle_cut_point = (middle[0] - middle_side[0] * 2.5,
|
||||
middle[1] - middle_side[1] * 2.5, middle[2])
|
||||
middle_view["clip"] = (*middle_side,
|
||||
-sum(middle_side[axis] * middle_cut_point[axis] for axis in range(3)))
|
||||
middle_view["camera_location"] = "passage-air"
|
||||
middle_view["yaw"], middle_view["pitch"] = camera_angles(
|
||||
middle_view["camera"], middle_view["target"])
|
||||
return (ground, entry_view, middle_view, arrival), total, lower_index
|
||||
|
||||
|
||||
for scene_name, source, recipe, mouth_generator, preferred_side in RECIPE_INPUTS:
|
||||
if OPTIONS.scene and scene_name != OPTIONS.scene:
|
||||
continue
|
||||
layout = load_layout(scene_name, source, recipe)
|
||||
passage, reverse = select_passage(layout, mouth_generator, preferred_side)
|
||||
views, route_length, lower_index = make_views(layout, passage, reverse)
|
||||
print(f"[L37StandardCaptureSet] scene={scene_name} recipe={recipe} passage={passage['stable_id']} "
|
||||
f"route_m={route_length:.3f} control_points={lower_index + 1} fov_deg=80 "
|
||||
"ground_camera_outside_m=5.500 entry_camera_outside_m=5.148 camera_eye_m=1.600", flush=True)
|
||||
for view in views:
|
||||
if OPTIONS.view and view["name"] != OPTIONS.view:
|
||||
continue
|
||||
stem = f"{scene_name}-{view['name']}"
|
||||
mesh = OUT / f"{stem}.vfmesh"
|
||||
png = PNG_DIR / f"{stem}.png"
|
||||
jpg = CAPTURES / f"{stem}.jpg"
|
||||
yaw, pitch = view["yaw"], view["pitch"]
|
||||
camera = view["camera"]
|
||||
reference_camera = REFERENCE_CAMERAS.get(stem)
|
||||
if reference_camera is not None:
|
||||
camera, target, reference_clip = reference_camera
|
||||
view["camera"], view["target"], view["clip"] = camera, target, reference_clip
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
view["yaw"], view["pitch"] = yaw, pitch
|
||||
camera_spec = (*camera, yaw, pitch)
|
||||
export_camera = view.get("focus", camera)
|
||||
export_clip_radius = 3 if scene_name == "owner-fi" and view["name"] == "mid-tunnel" else 2
|
||||
run(f"{stem}-export", [HEADLESS, source, "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={export_clip_radius}",
|
||||
"--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", f"--recipe={recipe}",
|
||||
f"--camera={fmt((*export_camera, yaw, pitch))}", f"--mesh-export={mesh}"])
|
||||
render_clip = view["clip"]
|
||||
if reference_camera is not None:
|
||||
surface = None
|
||||
ground_source = "L34-fixed-camera"
|
||||
elif view.get("outside_camera", False):
|
||||
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||
ground_source = "terrain-mesh"
|
||||
if surface is None:
|
||||
camera = (view["camera_xy"][0], view["camera_xy"][1],
|
||||
view["eye_height_z"])
|
||||
ground_source = "mouth-eye-height-fallback"
|
||||
else:
|
||||
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||
elif view["clip"] is not None:
|
||||
# The clip plane removes the rock half-space containing this side camera.
|
||||
# Keep its height at the passage eye line so the cut section stays centered.
|
||||
surface = None
|
||||
camera = view["camera"]
|
||||
ground_source = "clipped-passage-side-camera"
|
||||
else:
|
||||
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||
ground_source = "terrain-mesh"
|
||||
if surface is None:
|
||||
surface_z = view.get("eye_height_z", view["target"][2] + 0.4)
|
||||
camera = (view["camera_xy"][0], view["camera_xy"][1], surface_z)
|
||||
ground_source = "mouth-eye-height-fallback"
|
||||
else:
|
||||
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||
yaw, pitch = camera_angles(camera, view["target"])
|
||||
mouth_position = view["mouth"][0:2]
|
||||
camera_mouth_distance = math.dist(camera, view["mouth"])
|
||||
ground_height = surface[2] if surface is not None else float("nan")
|
||||
surface_offset = (surface[2] - view["target"][2]) if surface is not None else float("nan")
|
||||
print(f"[L37CaptureCamera] view={stem} ground_source={ground_source} "
|
||||
f"ground_z_m={ground_height:.3f} "
|
||||
f"surface_target_delta_m={surface_offset:.3f} "
|
||||
f"eye_z_m={camera[2]:.3f} "
|
||||
f"camera_location={view.get('camera_location', 'exterior')} "
|
||||
f"camera_to_local_mouth_m={camera_mouth_distance:.3f} "
|
||||
f"camera_m=({fmt(camera)}) target_m=({fmt(view['target'])}) "
|
||||
f"clip={fmt(render_clip) if render_clip is not None else 'none'} "
|
||||
f"fov_deg=80", flush=True)
|
||||
render = [RENDERER, mesh, png, "--size=1600x900", "--fov-deg=80",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--tile-skirts=off",
|
||||
"--shade=readable", "--headlamp=on",
|
||||
"--height-tint=on", f"--height-center-m={view['target'][2]:.6f}",
|
||||
"--height-range-m=8", "--tile-edges=off"]
|
||||
if view.get("focus_radius", 24.0) is not None:
|
||||
render.append(f"--focus-region={fmt((*view['focus'], view.get('focus_radius', 24.0)))}")
|
||||
if render_clip is not None:
|
||||
render.append(f"--clip-plane={fmt(render_clip)}")
|
||||
run(f"{stem}-render", render)
|
||||
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg])
|
||||
size = jpg.stat().st_size if jpg.exists() else 0
|
||||
print(f"[L37CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||
if not 0 < size <= 300_000:
|
||||
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB")
|
||||
@@ -0,0 +1,58 @@
|
||||
# L37 report
|
||||
|
||||
## Result
|
||||
|
||||
The owner accepted the FI crater mouth as captured, so I kept its shape. The raised crossing came from the FI surface route being lifted above the crater by its capsule-footing/grade envelope; it was not the short-span bridge deck. Moving the route onto the crater rim and making the style-0 path follow the native surface removed the raised crossing while keeping the walk route supported. Passage, capsule, support and open-air audits pass from arrival to departure.
|
||||
|
||||
The signed carve-fade fix remains: the composed field blends toward the full signed carve distance by the fade weight. The unfinished 0.50 m relief-wavelength trial was rejected; the prior 0.75 m wavelength remains.
|
||||
|
||||
## Visual audits
|
||||
|
||||
The built-shape thresholds were fixed from player scale: straight run 0.75 m (just above the 0.68 m capsule width), planar patch 0.50 m², corner turn 30°, and plane tolerance 0.02 m. The planar metrics remain in the output, but the owner accepted this FI appearance and the planar gate is informational for this round.
|
||||
|
||||
- FI: added planar patch 2.0098 m², added straight run 1.0000 m, zero sharp corners. The capture has no straight raised bridge edge, but the straight-run proxy is over its 0.75 m gate; the shape audit therefore still flags FI on that metric.
|
||||
- SW: no changed surface area in the matched route corridor; passes.
|
||||
- Upper-SW fixture: added planar patch 0.3319 m², straight run 0.7500 m, zero corners; passes (`0.75 m` is not longer than the limit).
|
||||
- Negative controls: L36 FI fails (4.8476 m², 1.4142 m); the 3×2 m box fails (6 m², 3 m, four corners). Both are detected.
|
||||
|
||||
The crease gate was frozen at 20° before the fix. L35 (42.27°) and L36 (27.13°) fail as controls. L37 is 4.40° at the 6.016 m close camera. In the 17.001 m overview it reaches 25.54° across 0.125 m, a 14.34 px projected interval at 2560×1440, so the numerical overview gate fails. A temporary v208 support-off build reproduces the same pair; the tread/support is not its source. The extra overview shows no continuous bridge seam, and I left the owner-approved crater unchanged. The FI fragment check finds zero detached components.
|
||||
|
||||
## Verification
|
||||
|
||||
- Passage audits: 24/24 pass across FI styles 0–3, SW styles 0–3, and upper-SW fixture styles 0–3, with Clang and GCC. Recipe stamps, mouth profiles, wall/floor ripple, strict validation, walkability, support, capsule, open air, and route bindings pass.
|
||||
- Invariants: 16/16. Legacy L13/L9: 4/4. Interface/hole and ring audits: 18/18. L30 terrace and L31 flat controls returned the expected nonzero result and were detected.
|
||||
- Determinism: 30/30, including L25 at W1/W8/W12 with Clang/GCC for both recipes, the 24 m crossing, and the upper-SW fixture.
|
||||
- Cost: five interleaved pairs per recipe against L36. Median whole-view change: +0.63% SW and +2.04% FI; paired MAD was 1.93% and 5.71%, respectively. No regression beyond run noise.
|
||||
- Captures use the standard L34 cameras, plus the 17.001 m FI overview. All 13 JPEGs are under 300 KB in `captures/`.
|
||||
|
||||
## Version and hashes
|
||||
|
||||
Evaluator version is 208 (bumped from 207). Stable hashes: recipe `c74a93d3e274e217`; owner plan `a5e29887e839c5d6`; owner manifest `8482dd0287ee2e9f`; FI field recipe `2096bd6fcf4d498d`. Upper-SW fixture layout ID is `resolved-layout-v3-fnv64:fca86f7273c53410` (plan `463a0f3b72909656`, manifest `98110083aff9ef5d`).
|
||||
|
||||
Final binary SHA-256: Clang `d69a1e0dc3275b4fa63963326c44588586fb7adc7db7a85e9c773ba9d9aa1e7d`; GCC `d75d282c5683584803a9b4adb2655aa0e66192de648f20215ec8702363b11c88`. FI mesh SHA-256: `280d238f3111f8a26348591600853fe89fe72c1ec1b250ce9d00ec7185543115`.
|
||||
|
||||
## Source edits
|
||||
|
||||
Core changes are in `VoxelForgeCore.h`, `FloatingIslandsInternal.h`, `ResolvedLayoutInternal.h`, `ShapeWorldInternal.h`, and `WorldAssemblyPrototype.h`. The only Unreal edit is the `terrainFollowingRoute` recipe setter in `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`; it could not be compiled on this Linux host. No invariants were changed.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- v140–153: deck-off was byte-identical but failed rock columns; v141/143 offsets left 25 unsupported samples; v142 made an isolated island; v144 left shards; v145 made holes/slots (12 support, 2 open-air failures); v146 left the bridge and 15 support failures; v147 grade rejected; v148 detached spiral blocked 43 samples; v149 removed bridge but blocked 48 and missed 3 supports; v150 pillar and v151 horn each missed 3 supports; v152 had no effect and was reverted; v153 was interrupted, retried, then its 1 m sink failed ripple/support.
|
||||
- v154–180: route/ripple/support failures continued; v157 floor ripple was 3.09 cm; v158 exposed an arch; v159 passed capsule but failed ripple; v160 missed support with bridge visible; v161 taper failed ripple; v162/163 disabled blend and left 8/16 unsupported; v164 left a fin; v165/167/168 left the bridge; v166 had a compile error, was corrected/rebuilt, then had 18 unsupported samples; v169 sink removed horns but left 4 misses; v170/171 gates were inert; v172 left an arch; v173 removed the fin but missed 4 supports; v174 recovered support but retained a fin; v175 changed nothing; v176 failed wall/floor ripple and capsule; v177 blocked 103 and failed ripple; v178–180 sink variants left a fin or support misses. The v180 build was interrupted twice (exit 130) before the successful retry.
|
||||
- v181–188: v181 route sag left 2 unsupported; v182 connector removal exited 2 on capsule-width support; v183 rounded joins passed route checks but ripple was 1.91 cm; v184 first compile failed on a qualifier, then rebuilt but ripple still failed; v185 removed the bridge but ripple stayed 1.91 cm; v186 support smoothing had no ripple gain; v187 wall-relief trial had no effect and was reverted; v188 first compile failed on `RoundQ`, fixed with `std::llround`; v188 then passed style-0 route/capsule/open-air and reduced floor ripple to 1.39 cm.
|
||||
- v189–201: v189–192 had open-air, style-2 blocked-sample, or ripple failures; v193 fixed style-2 with a 1 m underfloor/fade but style-0 ripple was 1.56 cm; v194 native blending did not help the other styles; v195 passed style 0 but not styles 1–3; v196’s style-gated curve caused 175/186/136 open-air misses; v197’s 0.50 m relief wavelength failed and was restored to 0.75 m; v198 amplitude reduction had no benefit and was reverted; v199 surface-height sampling left style failures; v200’s 2 m arc had 1.80 cm floor ripple; v201’s 2.5 m arc passed style 0 but not styles 1–3.
|
||||
- v202–207: style-0-only route/filter/tread variants still failed other styles: v202 had 175/186/136 open-air misses; v203 restricted tread and worsened open-air; v204’s 2 m nonzero-style curve had 92 misses; v205 had 77/94/36 misses; v206 fixed open air but styles 2/3 failed ripple; v207 fixed style 2 but style 1 floor ripple was 2.00 cm and style 3 was 3.19 cm. v208 passed the full passage matrix.
|
||||
- The mistaken `RunPassageAudits --help` invocation ran a case and timed out at 30 s (exit 124). A PIL probe failed with `ModuleNotFoundError`; nothing was installed. The final shape audit still flags FI’s 1.00 m straight-run proxy, and the final crease audit still flags the 17 m overview at 20°. The temporary support-off diagnostic reproduced that crease; its source edit was restored before final verification. Ring negative-control binaries exited 1 as expected and were detected.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Accepted, with the owner's verdict on the crater look. The carve-fade bug fix is real and general. The raised
|
||||
bridge is gone in the style-0 capture, and the full matrix, determinism and controls pass.
|
||||
- **Captures since L34 labelled "owner-fi"/"owner-sw" use the style-0 override fixtures.** The owner world itself
|
||||
uses **Style 2 (Spiral) on FI and Style 1 on SW**. So the owner judged a style he doesn't currently use, and the
|
||||
route change here (`terrainFollowingRoute = style == 0`) does **not** apply to his real FI passage. My brief said
|
||||
"owner world"; I didn't catch the fixture choice until now. L38 captures the real styles.
|
||||
- The fix is **style-gated** (many `style==0 && anchoredIslandPassage && !anchoredSpiral` branches). That breaks the
|
||||
shared-layer rule. L38 replaces it with one general mechanism (Research1 proposal 1: floor-limited carve).
|
||||
- The evaluator version jumped 23 → 208 (one bump per trial) and left `>= 192`-style gates in the code. Harmless for
|
||||
determinism, but it's clutter; L38 should collapse the trial-only gates.
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run five interleaved whole-view cost pairs against the L36 Clang binary."""
|
||||
import csv
|
||||
import math
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BINARIES = {
|
||||
"l36": ROOT / "Saved/Linux/l36/bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
"l37": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
}
|
||||
ORDERS = (("l36", "l37"), ("l37", "l36"), ("l36", "l37"),
|
||||
("l37", "l36"), ("l36", "l37"))
|
||||
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||
STAMP = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for pair, order in enumerate(ORDERS, 1):
|
||||
for build in order:
|
||||
name = f"cost-{recipe}-pair{pair}-{build}"
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s", str(BINARIES[build]),
|
||||
str(ROOT / "Standalone/data/owner_world.json"),
|
||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
matches = READY.findall(output)
|
||||
stamps = STAMP.findall(output)
|
||||
stamp_ok = stamps == [(recipe, recipe)]
|
||||
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||
and "hash=off" in output and "cache_hits=0" in output and stamp_ok)
|
||||
ready = float(matches[0]) if len(matches) == 1 else math.nan
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((recipe, pair, build, ready, elapsed, code, valid))
|
||||
failed |= not valid
|
||||
print(f"[L37CostRun] recipe={recipe} pair={pair} build={build} "
|
||||
f"whole_view_ready={ready:.3f}s process_elapsed={elapsed:.1f}s "
|
||||
f"{'PASS' if valid else 'FAIL'} log={logfile}", flush=True)
|
||||
|
||||
summary = OUT / "cost-l37.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("recipe", "pair", "build", "whole_view_ready_seconds",
|
||||
"process_elapsed_seconds", "exit_code", "status"))
|
||||
writer.writerows((recipe, pair, build, f"{ready:.6f}", f"{elapsed:.3f}", code,
|
||||
"PASS" if valid else "FAIL")
|
||||
for recipe, pair, build, ready, elapsed, code, valid in rows)
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build]
|
||||
for build in BINARIES}
|
||||
baseline = statistics.median(by_build["l36"])
|
||||
current = statistics.median(by_build["l37"])
|
||||
paired = []
|
||||
for pair in range(1, 6):
|
||||
values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair}
|
||||
paired.append((values["l37"] - values["l36"]) / values["l36"] * 100.0)
|
||||
mad = statistics.median(abs(value - statistics.median(paired)) for value in paired)
|
||||
print(f"[L37CostSummary] recipe={recipe} pairs=5 median_l36_s={baseline:.3f} "
|
||||
f"median_l37_s={current:.3f} median_change_pct={(current/baseline-1)*100.0:+.2f} "
|
||||
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)} "
|
||||
f"paired_mad_pct={mad:.2f}", flush=True)
|
||||
|
||||
print(f"[L37Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}",
|
||||
flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 20 else 0)
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check L25, 24 m crossing and upper-SW fixture determinism for L37."""
|
||||
import csv
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||
}
|
||||
HASH = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
STAMP = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M)
|
||||
LAYOUT = re.compile(
|
||||
r"^\[ResolvedLayoutSnapshot\] schema=\d+ stable_identity=([^ ]+) strata=\d+ bytes=\d+ round_trip=PASS$",
|
||||
re.M)
|
||||
PLAN = re.compile(
|
||||
r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*"
|
||||
r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", re.M)
|
||||
ROUTE = re.compile(
|
||||
r"^\[WorldAssemblerRouteAccepted\] arrival_xy_m=\([^)]*\) "
|
||||
r"departure_xy_m=\([^)]*\) endpoint_proximity_cost=\d+ average_grade_q16=\d+$", re.M)
|
||||
rows = []
|
||||
failed = False
|
||||
references = {}
|
||||
|
||||
|
||||
def run(kind, recipe, compiler, workers, data, passage_audit=False, crossing=False):
|
||||
global failed
|
||||
binary = BINARIES[compiler]
|
||||
name = f"determinism-{kind}-{compiler}-{recipe}-w{workers}"
|
||||
if kind == "upper-fixture":
|
||||
name = f"determinism-upper-fixture-{compiler}-w{workers}"
|
||||
log = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", str(binary), str(data), f"--workers={workers}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}",
|
||||
]
|
||||
if passage_audit:
|
||||
command.append("--audit-resolved-layout-passages")
|
||||
else:
|
||||
command.append("--stream-hash")
|
||||
if crossing:
|
||||
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
stamps = STAMP.findall(output)
|
||||
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||
if passage_audit:
|
||||
signature = (tuple(LAYOUT.findall(output)), tuple(PLAN.findall(output)),
|
||||
tuple(ROUTE.findall(output)))
|
||||
expected = references.get((kind, recipe))
|
||||
stable = expected is None or signature == expected
|
||||
if expected is None and all(signature):
|
||||
references[(kind, recipe)] = signature
|
||||
valid = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and len(signature[0]) == len(signature[1]) == len(signature[2]) == 1
|
||||
and re.search(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", output, re.M)
|
||||
and re.search(r"^\[PassageRippleAuditSummary\].*status=PASS$", output, re.M)
|
||||
and stable)
|
||||
hashes = ()
|
||||
detail = f"layout={signature[0][0] if signature[0] else 'MISSING'} stable={stable}"
|
||||
else:
|
||||
hashes = tuple(HASH.findall(output))
|
||||
key = (kind, recipe)
|
||||
expected = references.get(key)
|
||||
stable = expected is None or hashes == expected
|
||||
if expected is None and hashes:
|
||||
references[key] = hashes
|
||||
valid = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and "hash=on" in output and len(hashes) == 12 and stable)
|
||||
detail = f"hashes={len(hashes)}/12 stable={stable}"
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((kind, recipe, compiler, workers, elapsed, code, valid))
|
||||
failed |= not valid
|
||||
print(f"[L37DeterminismRun] kind={kind} recipe={recipe} compiler={compiler} "
|
||||
f"workers={workers} elapsed={elapsed:.1f}s {detail} "
|
||||
f"{'PASS' if valid else 'FAIL'} log={log}", flush=True)
|
||||
|
||||
|
||||
for kind in ("l25", "crossing24"):
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler in BINARIES:
|
||||
for workers in (1, 8, 12):
|
||||
run(kind, recipe, compiler, workers, OWNER,
|
||||
crossing=(kind == "crossing24"))
|
||||
|
||||
for compiler in BINARIES:
|
||||
for workers in (1, 8, 12):
|
||||
run("upper-fixture", "floating-islands", compiler, workers, FIXTURE,
|
||||
passage_audit=True)
|
||||
|
||||
summary = OUT / "determinism-l37.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("kind", "recipe", "compiler", "workers", "elapsed_seconds",
|
||||
"exit_code", "status"))
|
||||
for row in rows:
|
||||
writer.writerow((*row[:-1], "PASS" if row[-1] else "FAIL"))
|
||||
print(f"[L37DeterminismSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 30 else 0)
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L33 route/proof and transition invariants with recipe-stamped logs."""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||
}
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
|
||||
def run(name, binary, data, recipe, extra, checks):
|
||||
global failed
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s",
|
||||
str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}", *extra,
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamps = re.findall(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
text, re.M)
|
||||
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||
checks_ok = all(check(text) for check in checks)
|
||||
ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok
|
||||
rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||
f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} "
|
||||
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||
failed |= not ok
|
||||
|
||||
|
||||
owner = ROOT / "Standalone/data/owner_world.json"
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler, binary in BINARIES.items():
|
||||
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||
run(name, binary, owner, recipe,
|
||||
["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||
[lambda text: bool(re.search(
|
||||
r"^\[RouteParityAudit\].*status=PASS$", text, re.M)),
|
||||
lambda text: bool(re.search(
|
||||
r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))])
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler, binary in BINARIES.items():
|
||||
for mode in ("hard", "gradient", "interleaved"):
|
||||
name = f"invariant-transition-{recipe}-{mode}-{compiler}"
|
||||
run(name, binary, owner, recipe,
|
||||
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||
[lambda text: bool(re.search(
|
||||
r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||
text, re.M)),
|
||||
lambda text: bool(re.search(
|
||||
r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))])
|
||||
|
||||
summary = OUT / "invariant-audits.csv"
|
||||
summary.write_text(
|
||||
"run,recipe,elapsed_seconds,exit_code,status\n" +
|
||||
"".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, elapsed, code, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L33InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 16 else 0)
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the L13 route parity and L9 uniform-block proof audits on L37."""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||
}
|
||||
RECIPES = ("floating-islands", "surface-world")
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
for recipe in RECIPES:
|
||||
for compiler, binary in BINARIES.items():
|
||||
name = f"legacy-route-proof-{recipe}-{compiler}"
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s", str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--stream-hash", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}", "--audit-uniform-block-proofs", "--audit-route-parity",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamp = re.findall(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
output, re.M)
|
||||
route = re.findall(r"^\[RouteParityAudit\].*$", output, re.M)
|
||||
proofs = re.findall(r"^\[UniformBlockProofAudit\].*$", output, re.M)
|
||||
stamp_ok = stamp == [(recipe, recipe, "PASS")]
|
||||
route_ok = len(route) == 1 and route[0].endswith("status=PASS")
|
||||
proof_ok = len(proofs) == 1 and bool(
|
||||
re.search(r"violations=0(?:\s|$)", proofs[0]))
|
||||
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and route_ok and proof_ok)
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((name, recipe, compiler, elapsed, code, route_ok, bool(proof_ok), ok))
|
||||
failed |= not ok
|
||||
print(f"[L37LegacyAuditRun] case={name} elapsed={elapsed:.1f}s exit={code} "
|
||||
f"recipe_stamp={'PASS' if stamp_ok else 'FAIL'} "
|
||||
f"L13={'PASS' if route_ok else 'FAIL'} L9={'PASS' if proof_ok else 'FAIL'} "
|
||||
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||
|
||||
summary = OUT / "legacy-audits.csv"
|
||||
summary.write_text(
|
||||
"case,recipe,compiler,elapsed_seconds,exit_code,l13,l9,status\n" +
|
||||
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},"
|
||||
f"{'PASS' if l13 else 'FAIL'},{'PASS' if l9 else 'FAIL'},"
|
||||
f"{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, compiler, elapsed, code, l13, l9, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L37LegacyAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 4 else 0)
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze a 20-degree mouth-crease gate and project its sample spacing at 1440p."""
|
||||
import ast
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import math
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
BASE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37/RunMouthFragmentAuditL37.py"
|
||||
SPEC = importlib.util.spec_from_file_location("mouth_fragment_l37", BASE_PATH)
|
||||
BASE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(BASE)
|
||||
|
||||
# Frozen before the L37 mouth edit: 20 degrees over the audit's 0.125 m samples is
|
||||
# the visible-crease limit. At the 6.016 m standard camera and 80-degree horizontal
|
||||
# FOV, each sample gap spans multiple pixels at 2560x1440.
|
||||
BASE.NORMAL_LIMIT_DEG = 20.0
|
||||
CAMERA = (1.691820, -3.424982, -54.224198)
|
||||
TARGET = (6.298233, 1.137233, -55.468264)
|
||||
FOV_DEG = 80.0
|
||||
WIDTH_1440P = 2560
|
||||
HEIGHT_1440P = 1440
|
||||
MOUTH = (5.600, 0.445, -55.156)
|
||||
OVERVIEW_CAMERA = (MOUTH[0] - 9.75, MOUTH[1] - 11.4, MOUTH[2] + 8.0)
|
||||
|
||||
|
||||
def normalize(vector):
|
||||
magnitude = math.sqrt(sum(value * value for value in vector))
|
||||
return tuple(value / magnitude for value in vector)
|
||||
|
||||
|
||||
def view_basis(camera, target):
|
||||
forward = normalize(tuple(target[i] - camera[i] for i in range(3)))
|
||||
right = normalize((forward[1], -forward[0], 0.0))
|
||||
up = (right[1] * forward[2] - right[2] * forward[1],
|
||||
right[2] * forward[0] - right[0] * forward[2],
|
||||
right[0] * forward[1] - right[1] * forward[0])
|
||||
return forward, right, up
|
||||
|
||||
|
||||
def visible_crease(path, camera, target):
|
||||
cx, cy = BASE.CENTER
|
||||
bounds = (cx - BASE.OUTER, cx + BASE.OUTER, cy - BASE.OUTER, cy + BASE.OUTER)
|
||||
voxel_um, triangles, _ = BASE.RING.read_scene(
|
||||
str(path), 0, BASE.HEIGHT_MIN, BASE.HEIGHT_MAX, bounds)
|
||||
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||
buckets = BASE.RING.build_buckets(triangles, True, bounds, cell)
|
||||
forward, right, up = view_basis(camera, target)
|
||||
tan_h = math.tan(math.radians(FOV_DEG / 2.0))
|
||||
tan_v = math.tan(math.atan(tan_h * HEIGHT_1440P / WIDTH_1440P))
|
||||
focal = WIDTH_1440P / (2.0 * tan_h)
|
||||
lines = []
|
||||
count = round((BASE.OUTER - BASE.INNER) / BASE.STEP)
|
||||
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
lines.append([(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||
for radius in (BASE.INNER + index * BASE.STEP
|
||||
for index in range(count + 1))])
|
||||
tangent_count = round(8.0 / BASE.STEP)
|
||||
for degrees in (225.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
normal = (math.cos(angle), math.sin(angle))
|
||||
tangent = (-normal[1], normal[0])
|
||||
lines.append([(cx + 6.5 * normal[0] + (-4.0 + index * BASE.STEP) * tangent[0],
|
||||
cy + 6.5 * normal[1] + (-4.0 + index * BASE.STEP) * tangent[1])
|
||||
for index in range(tangent_count + 1)])
|
||||
|
||||
maximum, largest_pixels, worst_depth, visible_pairs = 0.0, 0.0, 0.0, 0
|
||||
worst_pair_pixels = 0.0
|
||||
worst_pair = None
|
||||
|
||||
def project(point):
|
||||
offset = tuple(point[i] - camera[i] for i in range(3))
|
||||
depth = sum(offset[i] * forward[i] for i in range(3))
|
||||
x = sum(offset[i] * right[i] for i in range(3))
|
||||
y = sum(offset[i] * up[i] for i in range(3))
|
||||
return depth, x, y
|
||||
|
||||
for points in lines:
|
||||
samples = [BASE.RING.sample_surface(buckets, x, y, True, cell, BASE.PREFERRED)
|
||||
for x, y in points]
|
||||
for index in range(1, len(samples)):
|
||||
before, current = samples[index - 1], samples[index]
|
||||
if before is None or current is None:
|
||||
continue
|
||||
p0 = (points[index - 1][0], points[index - 1][1], before[0])
|
||||
p1 = (points[index][0], points[index][1], current[0])
|
||||
midpoint = tuple((p0[axis] + p1[axis]) * 0.5 for axis in range(3))
|
||||
depth, horizontal, vertical = project(midpoint)
|
||||
if (depth <= 0.0 or abs(horizontal) > depth * tan_h
|
||||
or abs(vertical) > depth * tan_v):
|
||||
continue
|
||||
visible_pairs += 1
|
||||
angle = math.degrees(math.acos(max(-1.0, min(1.0,
|
||||
sum(a * b for a, b in zip(before[1], current[1]))))))
|
||||
d0, x0, y0 = project(p0)
|
||||
d1, x1, y1 = project(p1)
|
||||
pixels = math.hypot(focal * x0 / d0 - focal * x1 / d1,
|
||||
focal * y0 / d0 - focal * y1 / d1)
|
||||
largest_pixels = max(largest_pixels, pixels)
|
||||
if angle > maximum:
|
||||
maximum, worst_pair, worst_depth = angle, (p0, p1), depth
|
||||
worst_pair_pixels = pixels
|
||||
return maximum, worst_depth, largest_pixels, visible_pairs, worst_pair, worst_pair_pixels
|
||||
|
||||
|
||||
def run(path, label):
|
||||
stream = io.StringIO()
|
||||
with contextlib.redirect_stdout(stream):
|
||||
passed, maximum = BASE.audit(path)
|
||||
output = stream.getvalue()
|
||||
print(output, end="")
|
||||
match = re.search(r"worst_pair=(\([^\n]*\)) status=", output)
|
||||
worst = ast.literal_eval(match.group(1)) if match else None
|
||||
path = Path(path)
|
||||
near = visible_crease(path, CAMERA, TARGET)
|
||||
far = visible_crease(path, OVERVIEW_CAMERA, TARGET)
|
||||
print(f"[MouthCreasePixels] build={label} sample_step_m={BASE.STEP:.3f} "
|
||||
f"horizontal_fov_deg={FOV_DEG:.1f} resolution=2560x1440 "
|
||||
f"near_camera_to_mouth_m=6.016 near_visible_pairs={near[3]} "
|
||||
f"near_max_visible_deg={near[0]:.6f} near_depth_m={near[1]:.3f} "
|
||||
f"near_sample_spacing_px={near[2]:.2f} "
|
||||
f"near_worst_pair_spacing_px={near[5]:.2f} "
|
||||
f"overview_camera_to_mouth_m={math.dist(OVERVIEW_CAMERA, MOUTH):.3f} "
|
||||
f"overview_visible_pairs={far[3]} overview_max_visible_deg={far[0]:.6f} "
|
||||
f"overview_depth_m={far[1]:.3f} overview_sample_spacing_px={far[2]:.2f} "
|
||||
f"overview_worst_pair_spacing_px={far[5]:.2f} "
|
||||
f"overall_max_deg={maximum:.6f}")
|
||||
return passed, maximum
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--candidate", type=Path,
|
||||
default=ROOT / "Saved/Linux/l37/owner-fi-ground-mouth.vfmesh")
|
||||
args = parser.parse_args()
|
||||
paths = {
|
||||
"L35": ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh",
|
||||
"L36": ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh",
|
||||
"L37": args.candidate if args.candidate.is_absolute()
|
||||
else ROOT / args.candidate,
|
||||
}
|
||||
results = {label: run(path, label) for label, path in paths.items()}
|
||||
controls = (not results["L35"][0] and not results["L36"][0]
|
||||
and results["L35"][1] > 20.0 and results["L36"][1] > 20.0)
|
||||
final = results["L37"][0] and results["L37"][1] <= 20.0
|
||||
print(f"[MouthCreaseGate] normal_limit_deg=20 frozen_before_fix=true "
|
||||
f"L35_42deg_fail_control={'PASS' if controls else 'FAIL'} "
|
||||
f"L36_27deg_fail_control={'PASS' if controls else 'FAIL'} "
|
||||
f"L37_candidate={'PASS' if final else 'FAIL'} "
|
||||
f"status={'PASS' if controls and final else 'FAIL'}")
|
||||
raise SystemExit(0 if controls and final else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare the FI fade-ring crease against L35 using the same surface samples."""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
RING_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_PATH)
|
||||
RING = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(RING)
|
||||
|
||||
CENTER = (6.298233, 1.137233)
|
||||
INNER, OUTER, STEP = 5.5, 9.5, 0.125
|
||||
HEIGHT_MIN, HEIGHT_MAX, PREFERRED = -80.0, -40.0, -56.0
|
||||
# Chosen before the L37 mouth edit: a 20-degree normal jump over one 0.125 m
|
||||
# sample interval is visible at the standard camera distance.
|
||||
NORMAL_LIMIT_DEG = 20.0
|
||||
COVERAGE_MIN = 0.90
|
||||
FRAGMENT_MAX_TRIANGLES = 64
|
||||
FRAGMENT_MIN_AREA_M2 = 0.0001
|
||||
FRAGMENT_MAX_AREA_M2 = 0.05
|
||||
FRAGMENT_MAX_DISTANCE_M = 12.0
|
||||
|
||||
|
||||
def small_components(triangles):
|
||||
parent = list(range(len(triangles)))
|
||||
sizes = [1] * len(triangles)
|
||||
owners = {}
|
||||
|
||||
def find(index):
|
||||
while parent[index] != index:
|
||||
parent[index] = parent[parent[index]]
|
||||
index = parent[index]
|
||||
return index
|
||||
|
||||
def union(a, b):
|
||||
a, b = find(a), find(b)
|
||||
if a == b:
|
||||
return
|
||||
if sizes[a] < sizes[b]:
|
||||
a, b = b, a
|
||||
parent[b] = a
|
||||
sizes[a] += sizes[b]
|
||||
|
||||
for index, (_, _, vertices) in enumerate(triangles):
|
||||
for point, _ in vertices:
|
||||
key = tuple(round(value, 5) for value in point)
|
||||
other = owners.setdefault(key, index)
|
||||
union(index, other)
|
||||
|
||||
components = {}
|
||||
for index, (_, _, vertices) in enumerate(triangles):
|
||||
root = find(index)
|
||||
item = components.setdefault(root, {"count": 0, "area": 0.0, "sum": [0.0] * 3})
|
||||
item["count"] += 1
|
||||
points = [vertex[0] for vertex in vertices]
|
||||
for point in points:
|
||||
for axis in range(3):
|
||||
item["sum"][axis] += point[axis]
|
||||
ab = tuple(points[1][axis] - points[0][axis] for axis in range(3))
|
||||
ac = tuple(points[2][axis] - points[0][axis] for axis in range(3))
|
||||
cross = (ab[1] * ac[2] - ab[2] * ac[1],
|
||||
ab[2] * ac[0] - ab[0] * ac[2],
|
||||
ab[0] * ac[1] - ab[1] * ac[0])
|
||||
item["area"] += 0.5 * math.sqrt(sum(value * value for value in cross))
|
||||
|
||||
cx, cy = CENTER
|
||||
small = []
|
||||
below_floor = []
|
||||
for item in components.values():
|
||||
centroid = tuple(value / (3 * item["count"]) for value in item["sum"])
|
||||
distance = math.hypot(centroid[0] - cx, centroid[1] - cy)
|
||||
if (item["count"] <= FRAGMENT_MAX_TRIANGLES
|
||||
and item["area"] <= FRAGMENT_MAX_AREA_M2
|
||||
and distance <= FRAGMENT_MAX_DISTANCE_M):
|
||||
record = (item["count"], item["area"], distance, centroid)
|
||||
(below_floor if item["area"] < FRAGMENT_MIN_AREA_M2 else small).append(record)
|
||||
return len(components), small, below_floor
|
||||
|
||||
|
||||
def audit(path):
|
||||
cx, cy = CENTER
|
||||
bounds = (cx - OUTER, cx + OUTER, cy - OUTER, cy + OUTER)
|
||||
voxel_um, triangles, tiles = RING.read_scene(
|
||||
str(path), 0, HEIGHT_MIN, HEIGHT_MAX, bounds)
|
||||
component_count, fragments, below_floor = small_components(triangles)
|
||||
print(f"[MouthFragmentComponents] mesh={path.name} components={component_count} "
|
||||
f"small_near_mouth={len(fragments)} subthreshold_near_mouth={len(below_floor)} "
|
||||
f"limits=(triangles<={FRAGMENT_MAX_TRIANGLES},"
|
||||
f"area={FRAGMENT_MIN_AREA_M2:.4f}..{FRAGMENT_MAX_AREA_M2:.3f}m2,"
|
||||
f"distance<={FRAGMENT_MAX_DISTANCE_M:.1f}m) "
|
||||
f"first={fragments[0] if fragments else None} "
|
||||
f"subthreshold={below_floor[0] if below_floor else None} "
|
||||
f"status={'PASS' if not fragments else 'FAIL'}")
|
||||
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||
buckets = RING.build_buckets(triangles, True, bounds, cell)
|
||||
lines = []
|
||||
count = round((OUTER - INNER) / STEP)
|
||||
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
lines.append((f"radial-{degrees:.0f}", [
|
||||
(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||
for radius in (INNER + i * STEP for i in range(count + 1))]))
|
||||
tangent_count = round(8.0 / STEP)
|
||||
for degrees in (225.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
normal = (math.cos(angle), math.sin(angle))
|
||||
tangent = (-normal[1], normal[0])
|
||||
lines.append((f"tangent-{degrees:.0f}", [
|
||||
(cx + 6.5 * normal[0] + (-4.0 + i * STEP) * tangent[0],
|
||||
cy + 6.5 * normal[1] + (-4.0 + i * STEP) * tangent[1])
|
||||
for i in range(tangent_count + 1)]))
|
||||
|
||||
max_angle = 0.0
|
||||
max_pair = None
|
||||
min_coverage = 1.0
|
||||
for name, points in lines:
|
||||
samples = [RING.sample_surface(buckets, x, y, True, cell, PREFERRED)
|
||||
for x, y in points]
|
||||
coverage = sum(value is not None for value in samples) / len(samples)
|
||||
min_coverage = min(min_coverage, coverage)
|
||||
line_max = 0.0
|
||||
for index in range(1, len(samples)):
|
||||
prior, current = samples[index - 1], samples[index]
|
||||
if prior is None or current is None:
|
||||
continue
|
||||
dot = max(-1.0, min(1.0, sum(a * b for a, b in zip(prior[1], current[1]))))
|
||||
delta = math.degrees(math.acos(dot))
|
||||
if delta > line_max:
|
||||
line_max = delta
|
||||
if delta > max_angle:
|
||||
max_angle = delta
|
||||
max_pair = (name, points[index - 1], prior[0], points[index], current[0])
|
||||
print(f"[MouthFadeLine] mesh={path.name} line={name} coverage={coverage:.3f} "
|
||||
f"max_normal_jump_deg={line_max:.6f}")
|
||||
normal_pass = max_angle <= NORMAL_LIMIT_DEG and min_coverage >= COVERAGE_MIN
|
||||
passed = normal_pass and not fragments
|
||||
print(f"[MouthFadeAudit] mesh={path.name} tiles={tiles} lines={len(lines)} "
|
||||
f"coverage_min={min_coverage:.3f} coverage_limit={COVERAGE_MIN:.2f} "
|
||||
f"max_normal_jump_deg={max_angle:.6f} normal_limit_deg={NORMAL_LIMIT_DEG:.1f} "
|
||||
f"fragments={len(fragments)} "
|
||||
f"worst_pair={max_pair} status={'PASS' if passed else 'FAIL'}")
|
||||
return passed, max_angle
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--control", type=Path,
|
||||
default=ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh")
|
||||
parser.add_argument("--prior", type=Path,
|
||||
default=ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh")
|
||||
parser.add_argument("--current", type=Path,
|
||||
default=ROOT / "Saved/Linux/l37/owner-fi-ground-mouth.vfmesh")
|
||||
args = parser.parse_args()
|
||||
control = args.control if args.control.is_absolute() else ROOT / args.control
|
||||
prior = args.prior if args.prior.is_absolute() else ROOT / args.prior
|
||||
current = args.current if args.current.is_absolute() else ROOT / args.current
|
||||
control_ok, control_max = audit(control)
|
||||
prior_ok, prior_max = audit(prior)
|
||||
current_ok, current_max = audit(current)
|
||||
controls = (not control_ok and not prior_ok
|
||||
and control_max > NORMAL_LIMIT_DEG and prior_max > NORMAL_LIMIT_DEG)
|
||||
expected = controls and current_ok and current_max <= NORMAL_LIMIT_DEG
|
||||
print(f"[MouthFadeNegativeControl] L35_max_deg={control_max:.6f} "
|
||||
f"L35_detected={not control_ok} L36_max_deg={prior_max:.6f} "
|
||||
f"L36_detected={not prior_ok} L37_max_deg={current_max:.6f} "
|
||||
f"normal_limit_deg={NORMAL_LIMIT_DEG:.1f} current_pass={current_ok} "
|
||||
f"status={'PASS' if expected else 'FAIL'}")
|
||||
raise SystemExit(0 if expected else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run recipe-stamped passage/profile audits for both compilers and both owner recipes."""
|
||||
import re
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||
}
|
||||
RUNS = []
|
||||
for recipe, filename in (
|
||||
("floating-islands", "floating-islands"),
|
||||
("surface-world", "surface-world"),
|
||||
):
|
||||
for style in range(4):
|
||||
RUNS.append((f"owner-{filename}-style{style}",
|
||||
ROOT / f"Saved/Linux/linux25-owner-{filename}-style{style}.json", recipe))
|
||||
for style in range(4):
|
||||
RUNS.append((f"upper-sw-fixture-style{style}",
|
||||
OUT.parent / "l33/fixtures" / f"sw-upper-tunnel-network-style{style}.json",
|
||||
"floating-islands"))
|
||||
|
||||
RECIPE = re.compile(r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||
PASSAGE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||
MOUTH = re.compile(r"^\[PassageMouthProfileAudit\].*status=PASS$", re.M)
|
||||
ROUTE = re.compile(r"^\[ResolvedStrateRouteAudit\].*mouth_binding=PASS status=PASS$", re.M)
|
||||
CHECKS = (
|
||||
r"^\[ResolvedLayoutStrict\] accepted=true issues=0 error=$",
|
||||
r"^\[ResolvedLayoutValidatorControls\].*status=PASS$",
|
||||
r"^\[PassageMouthProfileNegativeControl\].*source=L23_terraced_geometry.*detected=true$",
|
||||
r"^\[PassageMouthProfileSummary\].*l23_terrace_negative_control=DETECTED status=PASS$",
|
||||
r"^\[PassageNaturalnessSummary\].*l23_negative_control=DETECTED.*status=PASS$",
|
||||
r"^\[PassageRippleAuditSummary\].*synthetic_spheres_negative_control=DETECTED max_amplitude_m=0\.035 floor_beaded_negative_control=DETECTED max_floor_amplitude_m=0\.015 status=PASS$",
|
||||
r"^\[PassageOpenAirAuditSummary\].*violations=0.*l24_negative_control=DETECTED status=PASS$",
|
||||
r"^\[PassageNoLongBridgeAudit\].*status=PASS$",
|
||||
r"^\[PassageRockColumnAuditSummary\].*status=PASS$",
|
||||
r"^\[ResolvedPassageAuditSummary\].*route_binding_failures=0 .*status=PASS$",
|
||||
r"^\[Walkability\] PASS",
|
||||
r"^\[FloorProfileBlend\].*negative_controls=step_detected/slope_detected status=PASS$",
|
||||
)
|
||||
|
||||
rows = []
|
||||
failed = False
|
||||
for compiler, binary in BINARIES.items():
|
||||
for name, data, recipe in RUNS:
|
||||
logfile = OUT / f"passage-audit-{name}-{compiler}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", str(binary), str(data), "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}", "--audit-resolved-layout-passages",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamp_ok = RECIPE.findall(output) == [(recipe, recipe)]
|
||||
passages = len(PASSAGE.findall(output))
|
||||
mouths = len(MOUTH.findall(output))
|
||||
routes = len(ROUTE.findall(output))
|
||||
checks_ok = all(re.search(pattern, output, re.M) for pattern in CHECKS)
|
||||
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and passages >= 1 and mouths == passages and routes >= 1 and checks_ok)
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((name, recipe, compiler, elapsed, code, passages, mouths, routes, ok))
|
||||
failed |= not ok
|
||||
print(f"[L37PassageAuditRun] case={name} compiler={compiler} recipe={recipe} "
|
||||
f"elapsed={elapsed:.1f}s exit={code} passages={passages} "
|
||||
f"profiles={mouths} routes={routes} {'PASS' if ok else 'FAIL'} "
|
||||
f"log={logfile}", flush=True)
|
||||
|
||||
summary = OUT / "passage-audits.csv"
|
||||
summary.write_text(
|
||||
"case,recipe,compiler,elapsed_seconds,exit_code,passages,mouth_profiles,routes,status\n" +
|
||||
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},{passages},{mouths},{routes},"
|
||||
f"{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, compiler, elapsed, code, passages, mouths, routes, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L37PassageAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 24 else 0)
|
||||
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export v24 ring meshes at matching LOD windows and retain old controls."""
|
||||
import csv
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BIN = OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang"
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
TERRACE = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
RING_SET = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingSetL32.py"
|
||||
STAMP = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||
EXPORT = re.compile(r"^\[MeshSceneExport\].*status=PASS", re.M)
|
||||
TERRACE_RESULT = re.compile(r"^\[RingTerraceAudit\].*status=(PASS|FAIL)$", re.M)
|
||||
SET_RESULT = re.compile(r"^\[RingSetAudit\].*status=(PASS|FAIL)$", re.M)
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
FI_CENTERS = ((56, -72), (72, -72), (56, -88), (40, -72), (24, 24),
|
||||
(104, 24), (72, -88), (120, 24), (104, 8), (24, -24))
|
||||
SW_CENTERS = ((-136, 136), (-56, 152), (-152, 120), (-136, 120),
|
||||
(-152, 104), (-136, 104), (-40, 152))
|
||||
LOD_CENTERS = {
|
||||
"fi-floor": {
|
||||
2: (16, -144), 3: (224, -160), 4: (-448, 448),
|
||||
5: (-128, -640), 6: (-1792, 1280), 7: (3584, 1536),
|
||||
8: (3072, 7168),
|
||||
},
|
||||
"sw-ceiling": {
|
||||
2: (16, -144), 3: (224, -160), 4: (-448, 448),
|
||||
5: (-128, -640), 6: (-1792, 1280), 7: (2560, -512),
|
||||
8: (3072, 7168),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def run(name, command):
|
||||
log = OUT / f"ring-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
print(f"[L37RingStep] name={name} elapsed={elapsed:.1f}s exit={code} log={log}",
|
||||
flush=True)
|
||||
return result, output, elapsed
|
||||
|
||||
|
||||
def export(label, recipe, max_level, camera):
|
||||
mesh = OUT / f"ring-owner-{label}-v24.vfmesh"
|
||||
command = [BIN, OWNER, "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||
f"--max-level={max_level}", f"--recipe={recipe}"]
|
||||
if camera:
|
||||
command.append(f"--camera={camera}")
|
||||
command.append(f"--mesh-export={mesh}")
|
||||
result, output, elapsed = run(f"export-{label}", command)
|
||||
stamps = STAMP.findall(output)
|
||||
ok = (result is not None and result.returncode == 0
|
||||
and stamps == [(recipe, recipe)] and EXPORT.search(output) and mesh.is_file())
|
||||
print(f"[L37RingExport] scene={label} recipe={recipe} stamp={stamps} "
|
||||
f"bytes={mesh.stat().st_size if mesh.exists() else 0} "
|
||||
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||
return mesh, elapsed, ok
|
||||
|
||||
|
||||
def terrace(label, recipe, surface, mesh, lod, height_min, height_max,
|
||||
center=(64, -32), radius=32):
|
||||
result, output, elapsed = run(f"{label}-lod{lod}", ["python3", TERRACE, mesh,
|
||||
f"--surface={surface}", "--center", str(center[0]), str(center[1]),
|
||||
f"--radius={radius}",
|
||||
f"--lod={lod}", f"--height-min-m={height_min}",
|
||||
f"--height-max-m={height_max}"])
|
||||
statuses = TERRACE_RESULT.findall(output)
|
||||
ok = (result is not None and result.returncode == 0 and statuses == ["PASS"])
|
||||
rows.append((label, recipe, surface, lod, elapsed, ok))
|
||||
print(f"[L37RingAuditRun] scene={label} recipe={recipe} surface={surface} "
|
||||
f"lod={lod} center={center} radius={radius} "
|
||||
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||
return ok
|
||||
|
||||
|
||||
def lod1_set(label, recipe, surface, mesh, centers, height_min, height_max):
|
||||
command = ["python3", RING_SET, mesh, f"--surface={surface}"]
|
||||
for x, y in centers:
|
||||
command.extend(("--center", str(x), str(y)))
|
||||
command.extend(("--radius=8", "--lod=1", f"--height-min-m={height_min}",
|
||||
f"--height-max-m={height_max}"))
|
||||
result, output, elapsed = run(f"{label}-lod1-set", command)
|
||||
statuses = SET_RESULT.findall(output)
|
||||
ok = (result is not None and result.returncode == 0 and statuses == ["PASS"])
|
||||
rows.append((label, recipe, surface, 1, elapsed, ok))
|
||||
print(f"[L37RingAuditRun] scene={label} recipe={recipe} surface={surface} "
|
||||
f"lod=1 windows={len(centers)} status={'PASS' if ok else 'FAIL'}",
|
||||
flush=True)
|
||||
return ok
|
||||
|
||||
|
||||
for label, recipe, surface, window_min, window_max, camera0, camera1, centers in (
|
||||
("fi-floor", "floating-islands", "floor", -122.0, -117.0,
|
||||
"64,-32,-115,0,-45", "64,-32,-115,0,-45", FI_CENTERS),
|
||||
("sw-ceiling", "surface-world", "ceiling", -132.0, -124.0,
|
||||
"64,-32,-136,0,45", "-96,128,-136,0,45", SW_CENTERS)):
|
||||
low_mesh, _, low_ok = export(f"{label}-lod0", recipe, 0, camera0)
|
||||
failed |= not low_ok
|
||||
if low_ok:
|
||||
failed |= not terrace(label, recipe, surface, low_mesh, 0, window_min, window_max)
|
||||
|
||||
detail_mesh, _, detail_ok = export(f"{label}-lod1", recipe, 1, camera1)
|
||||
failed |= not detail_ok
|
||||
if detail_ok:
|
||||
set_min, set_max = ((-121.625, -117.375) if label == "fi-floor"
|
||||
else (-128.375, -124.125))
|
||||
failed |= not lod1_set(label, recipe, surface, detail_mesh, centers,
|
||||
set_min, set_max)
|
||||
|
||||
full_mesh, _, full_ok = export(f"{label}-lod2-8", recipe, 8, None)
|
||||
failed |= not full_ok
|
||||
if full_ok:
|
||||
for lod in range(2, 9):
|
||||
center = LOD_CENTERS[label][lod]
|
||||
radius = 4 * (1 << lod)
|
||||
failed |= not terrace(label, recipe, surface, full_mesh, lod,
|
||||
window_min, window_max, center, radius)
|
||||
|
||||
# Keep the established L30 terrace and L31 flat-surface negative controls live.
|
||||
controls = (
|
||||
("l30-terrace", ROOT / "Saved/Linux/l32/fi-floor-l30-camera-negative.vfmesh",
|
||||
-130.0, -110.0),
|
||||
("l31-flat", ROOT / "Saved/Linux/l31/fi-floor-v19.vfmesh", -122.0, -117.0),
|
||||
)
|
||||
for name, mesh, height_min, height_max in controls:
|
||||
result, output, _ = run(f"control-{name}", ["python3", TERRACE, mesh,
|
||||
"--surface=floor", "--center", "64", "-32", "--radius=32", "--lod=0",
|
||||
f"--height-min-m={height_min}", f"--height-max-m={height_max}"])
|
||||
statuses = TERRACE_RESULT.findall(output)
|
||||
detected = (result is not None and result.returncode == 1 and statuses == ["FAIL"])
|
||||
failed |= not detected
|
||||
print(f"[L37RingControl] name={name} detected={detected} "
|
||||
f"status={'PASS' if detected else 'FAIL'}", flush=True)
|
||||
|
||||
summary = OUT / "ring-audits-l37.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("scene", "recipe", "surface", "lod", "elapsed_seconds", "status"))
|
||||
writer.writerows((label, recipe, surface, lod, f"{elapsed:.3f}",
|
||||
"PASS" if ok else "FAIL")
|
||||
for label, recipe, surface, lod, elapsed, ok in rows)
|
||||
print(f"[L37RingSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} controls=2 "
|
||||
f"status={'FAIL' if failed else 'PASS'} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 18 else 0)
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,31 @@
|
||||
# Research R1 — natural, walkable cave mouths and tunnels
|
||||
|
||||
## What the references show
|
||||
|
||||
- **Deep Rock Galactic** separates room/exit structure from biome noise and debris; designers test generated variants for traversal and wayfinding. **Minecraft 1.18** distinguishes caverns from spaghetti and noodle tunnels, and its noise caves form entrances where they intersect the surface. These support a planned route plus a terrain-native mouth, not a portal stamp. [Ghost Ship’s cave-generation post](https://store.steampowered.com/news/posts/?appgroupname=Deep+Rock+Galactic&appids=548430&enddate=1729500950&feed=steam_community_announcements), [Mojang 1.18 notes](https://www.minecraft.net/de-de/article/caves---cliffs--part-ii-out-today-java)
|
||||
- **Astroneer** describes layered on-the-fly noise with separate cave layers. **No Man’s Sky** identifies navigable caves as a goal and documents dual marching-cubes performance work, but does not publish the cave-shape algorithm. Use these as support for deterministic layers and navigation goals, not as evidence of a specific implementation. [Astroneer](https://www.unrealengine.com/spotlights/how-system-era-softworks-leveraged-ue4-to-create-astroneer-s-wonderful-universe?lang=en-US), [NMS terrain](https://www.nomanssky.com/next-update/), [NMS meshing](https://www.nomanssky.com/worlds-part-I-update/)
|
||||
- **Enshrouded** describes sculpted macro-topology and hand-placed voxel stamps. **Valheim** documents room assets used by its dungeon generator; **Teardown** documents VoxTool for complex voxel terrain assets. These are topology/tooling examples, not disclosed SDF mouth techniques. [Enshrouded](https://enshrouded.com/ja-JP/news/enshrouded-world-design-team-reddit-ama-recap), [Valheim](https://valheim.com/support/modding-faq-for-the-asset-bundle-update-0-217-40/), [Teardown](https://teardowngame.com/voxtool/)
|
||||
- Cave research separates playable topology from cave appearance. A karst SDF paper models irregular cross-sections and warns that curved sweeps can lose continuity at high curvature; tortuous paths use a union of spheres. Erosion papers model morphology at generation time, not per voxel. [Playable cave systems](https://santosgrueiro.com/papers/2014/2014-Santam-Cave.pdf), [geological cave SDFs](https://diglib.eg.org/items/a4e7985c-af52-4b55-bd1a-eeeff6c47422), [karst erosion](https://www.sciencedirect.com/science/article/pii/S0097849321002132)
|
||||
|
||||
These references do not guarantee VoxelForge’s walk-only contract. Keep its deterministic route, 0.34 m × 1.76 m capsule, supported floor, and slope checks authoritative.
|
||||
|
||||
## Ranked proposals
|
||||
|
||||
| Rank | Proposal and code location | Expected gain | Main risk / cost |
|
||||
|---|---|---|---|
|
||||
| **1** | **Limit the carve by a route floor.** Keep grade-safe route/floor data in [`PlanResolvedLayoutPassagesV1`](../../../../Standalone/Core/src/ResolvedLayoutInternal.h); in [`ApplyResolvedStratePassageCarves`](../../../../Standalone/Core/src/ShapeWorldInternal.h), constrain the tunnel void to above that floor and subtract it from native solid. For negative-is-solid fields: `void = max(tubeSdf, floorZ - z)`, then `carvedSolid = max(nativeSolid, -void)`. Blend the floor to native terrain height along route distance. Replace broad decks where native rock supports the route; keep only narrow fallback support where it does not. | Best chance of a worn path that remains part of the crater/rock instead of reading as a platform. The floor stays explicit and testable. | Medium walkability risk where the source has no solid underfoot or where FI coils overlap. Keep capsule, support, open-air, and slope gates. Low-to-moderate per-sample cost. |
|
||||
| **2** | **Blend the mouth along route distance.** Refine `mouthCarveFade` / `roughSignedDistance` in `ApplyResolvedStratePassageCarves` (around lines 1168–1209) with a local smooth CSG transition between native terrain and bore. Carving uses smooth-max with the negated void field. Keep the floor cap separate. [Smooth-min reference](https://iquilezles.org/articles/smin/). | Removes a visible seam while letting the entrance follow the local landform. | Smooth CSG moves the zero surface and can round off thin rock or support. L36’s broad filter still left a 27° crease against the earlier 20° probe. Keep it local and hold the crease gate before comparing. Small field cost. |
|
||||
| **3** | **Shape walls, not the walk plane.** Extend `considerCarveSegment` / `VF_FieldPassageWallRelief` (around lines 933–1043) with one bounded, seeded, low-frequency displacement or asymmetric profile, masked by wall orientation and distance from floor. [Domain warping](https://iquilezles.org/articles/warp/), [karst SDFs](https://diglib.eg.org/items/a4e7985c-af52-4b55-bd1a-eeeff6c47422). | Unequal pockets and folds add geology without ribs or a straight tread outline. | Too much detail can narrow capsule clearance or become periodic. One noise term is cheap; nested warps and curved cross-section sweeps are more expensive and need updated slope/Lipschitz bounds. |
|
||||
| **4** | **Reuse the current spatial culling.** Bound each new mouth feature so `BuildPassageFeatureTileList`, `PassageMayAffectDensity`, and the nearest-segment BVH skip distant voxels. These AABB and route BVHs already exist ([architecture §8.45](../../../../ARCHITECTURE.md)). Lipschitz pruning is safe only with proven bounds that include smooth blend radii and warp derivatives. [SDF pruning research](https://onlinelibrary.wiley.com/doi/10.1111/cgf.70057). | Protects CPU budget with little geometry risk; lowest-risk performance path. | Little visual gain. Full cached/interpolated distance volumes add memory and can move the zero surface; consider only after profiling, with exact evaluation near the walk surface. |
|
||||
|
||||
## What not to repeat from L33–L37
|
||||
|
||||
- Regular world-space octaves produced visible 8 m / 4 m wall ribs. Keep relief irregular and seeded.
|
||||
- Per-segment one-sided tread clamps produced floor washboard. Removing support altogether then caused unsupported samples; keep one continuous floor profile.
|
||||
- The L34 apron, L36 trapezoid, and L37 raised bridge read as built geometry. The bridge SDF is a separate deck primitive in `ShapeWorldInternal.h`; do not widen it to repair cave shape.
|
||||
- L36’s broad field filter removed most fragments but left a 27° crease, above the earlier 20° gate. Avoid broad blur and do not tune gates after seeing results.
|
||||
- Wall-only ripple checks missed the floor beat. Keep walk-floor profiles and negative controls for periodic relief and built shapes.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prioritize the floor-limited carve and local terrain-to-bore blend. Let `ResolvedLayoutInternal.h` own the deterministic walk route and grade; let native density provide visible support; confine seeded detail to walls and ceiling. The owner has accepted the current L37 crater mouth, so apply this to the remaining bridge and future shaping without reopening that look.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Shared notes (researcher R1 <-> implementer L37), newest at bottom
|
||||
|
||||
- **Separate cave shape from cave dressing:** Ghost Ship describes a constrained room-and-exit skeleton, then biome-specific surface noise and debris; its designers test generated variants for traversal, wayfinding, visibility and class playability ([official DRG cave-generation post](https://store.steampowered.com/news/posts/?appgroupname=Deep+Rock+Galactic&appids=548430&enddate=1729500950&feed=steam_community_announcements)). For VoxelForge, keep `ResolvedLayoutInternal.h` responsible for the deterministic route / clearance skeleton and keep material-scale irregularity in `ShapeWorldInternal.h`; do not let a wall-noise change move the walk floor or route.
|
||||
- **Use distinct tunnel and cavern scales:** Minecraft 1.18 explicitly separates large “cheese” rooms from spaghetti and narrow noodle passages, and lets those noise caves form mouths where they intersect the surface ([Mojang 1.18 notes](https://www.minecraft.net/de-de/article/caves---cliffs--part-ii-out-today-java)). This supports a broad, irregular crater-to-bore transition from intersecting fields, while preserving a narrower path-aligned walk lane; a single uniform portal cylinder is the wrong visual abstraction.
|
||||
- **Layer deterministic noise, with a gameplay mask:** Astroneer’s published terrain system uses layered noise and separate cave layers evaluated on the fly without neighboring-cell dependency ([Unreal Engine interview](https://www.unrealengine.com/spotlights/how-system-era-softworks-leveraged-ue4-to-create-astroneer-s-wonderful-universe?lang=en-US)). This maps to pure seeded coordinate noise in `ShapeWorldInternal.h`, masked by distance-from-route / wall orientation so only the rock contour gets warped; keep the walk surface’s slope and capsule clearance under the existing route constraints.
|
||||
- **Treat navigation as a hard constraint, then test variety:** DRG’s designers say a structurally valid room can still be unplayable after generator variation and playtest it repeatedly; No Man’s Sky’s patch notes identify navigability as an explicit terrain-generation goal ([DRG](https://store.steampowered.com/news/posts/?appgroupname=Deep+Rock+Galactic&appids=548430&enddate=1729500950&feed=steam_community_announcements), [NMS terrain update](https://www.nomanssky.com/next-update/)). For this code, keep the capsule/support/open-air and max-grade audits as geometry gates across seeded styles; visual naturalness checks supplement, never replace, them.
|
||||
- **Author macro topology, procedurally fill the in-between:** Enshrouded’s world team describes sculpted rough models and hand-placed voxel stamps for topology between POIs ([official AMA recap](https://enshrouded.com/ja-JP/news/enshrouded-world-design-team-reddit-ama-recap)). Translation: route points and surrounding crater shape should determine the large form; use bounded relief as finish. Noise alone is not a substitute for an intentional entrance profile.
|
||||
- **Avoid a single axisymmetric tube at the entrance:** The karst SDF work describes passage sections as irregular and asymmetric; for tortuous curves it switches from sweeps to a union of spheres to retain a continuous, 1-Lipschitz field ([Paris et al., *Synthesizing Geologically Coherent Cave Networks*, PDF](https://diglib.eg.org/bitstreams/fd2a57a6-f1fa-4c74-8b3e-9aea2aeb6047/download)). `ShapeWorldInternal.h::considerCarveSegment` already smooths eligible route joins; a later shape pass could add a small asymmetric cross-section / radius profile there, but preserve a conservative bound and leave floor geometry constrained. Risk: true curved cross-section frames and nearest-point evaluation cost more than the present capsule sweep.
|
||||
- **Use erosion as a shape model, not a per-voxel simulation:** Cave-network research produces natural branching/channel forms with hydraulic flow and erosion simulation ([Pytel & Mann, JCGT](https://jcgt.org/published/0004/02/02/); [Franke & Müller, karst caves](https://www.sciencedirect.com/science/article/pii/S0097849321002132)). Translate the erosion cues (unequal scallops, asymmetric channel widening, larger pockets at bends) into fixed-seed route descriptors or one bounded noise term; do not run flow/erosion iterations in `ApplyResolvedStratePassageCarves`, which is on the per-voxel CPU path.
|
||||
- **Smooth the solid/void intersection locally and watch the field bound:** Polynomial smooth-min gives a cheap continuous blend for implicit surfaces ([Quilez, smooth minimum](https://iquilezles.org/articles/smin/)); for solid carving, apply the corresponding smooth-max to the solid and negated void field. Restrict it to the opening region and fade it by distance along the path. Smooth CSG shifts the zero-set and may round away a floor or thin rock, so use capsule, support and crease audits after each change.
|
||||
- **Do not warp the walk plane:** Domain warping samples a field at noise-displaced coordinates ([Quilez, domain warping](https://iquilezles.org/articles/warp/)). It can break periodic wall bands, but its derivative changes the field slope and repeated warps add several noise evaluations; in `ShapeWorldInternal.h` apply only bounded low-frequency displacement to the wall/ceiling contour, masking by wall orientation and distance from the floor, not to the whole carve or tread field.
|
||||
- **Keep the existing broad phase; use Lipschitz pruning only with proven bounds:** VoxelForge already selects passage candidates per tile with an AABB BVH and bounds the nearest-segment search with a per-passage BVH ([architecture §8.45](../../../ARCHITECTURE.md)). Recent SDF pruning work is explicit that pruning needs exact 1-Lipschitz fields or valid updated bounds for smooth operators ([Barbier et al., *Lipschitz Pruning*, Computer Graphics Forum](https://onlinelibrary.wiley.com/doi/10.1111/cgf.70057)). Keep new fields inside those bounds; do not assume warped/smoothed noise remains 1-Lipschitz. Cached/trilinearly interpolated fields are a later option only if profiling shows repeated exact queries; interpolation can move the zero surface and needs an exact fallback near the walk surface.
|
||||
- **Bridge code lead for L37:** In the current capture-matching source, `ApplyResolvedStratePassageCarves` unions `PassageBridgeSignedDistance` for `IsPassageShortSpanSegment`; that field is a separate deck with 3.3–3.65 m half-width and about 4 m thickness ([ShapeWorldInternal.h](../../../Standalone/Core/src/ShapeWorldInternal.h), around `PassageBridgeSignedDistance`). Also check `IsPassageColumnSegment`, which routes through the same support-deck path. The render can therefore come from a planned support feature, independent of cave-wall relief; trace the flagged segment back to its bridge/column plan before changing mouth noise.
|
||||
- **Some examples use authored pieces rather than SDF mouths:** Valheim documents room assets consumed by `DungeonGenerator` ([official modding FAQ](https://valheim.com/support/modding-faq-for-the-asset-bundle-update-0-217-40/)); Teardown documents a voxel asset tool used for complex campaign terrain ([VoxTool](https://teardowngame.com/voxtool/)). These are useful for authored topology and asset tooling, but neither documents a natural carved mouth algorithm. No Man’s Sky publicly promises more navigable cave systems and reports dual marching-cubes performance work, while leaving its cave-shape algorithm unspecified ([NMS terrain update](https://www.nomanssky.com/next-update/), [Worlds Part I](https://www.nomanssky.com/worlds-part-I-update/)); don't infer its internal method.
|
||||
- **Implementer control, L37:** Disabling the FI short-span deck left the owner ground-mouth and overview JPEGs byte-identical, while the rock-column audit failed on its four short spans. The deck is needed elsewhere in the route and is not the visible crater-mouth bridge; restored it and traced the entrance carve/support path instead.
|
||||
- **Implementer control, L37:** Passage tread and native-route blend ablations left the crater ridge; disabling the route-to-mouth support handoff broke 18 FI route support samples. Current trace points to the final FloatingIslands surface-route heights: its 17-probe maximum footing and grade envelope lift the approach over the crater. L167 tests centerline footing for that final segment, with route audits still binding.
|
||||
- **Implementer trace, L37:** `FloatingIslandsInternal.h::ConnectorDensity` adds an arch up to twice the connector thickness (8 m here), so lowering connector endpoints alone leaves a crater-crossing hump. Testing the v171 version removes that arch and seats connectors 1 m below native island tops; the route is still derived and checked from the resulting field.
|
||||
- **Implementer correction, L37:** The owner FI JSON defaults to `geometryVersion=4`; v171's `>=5` experiment gate was inert and its capture matched v170. The actual L37 fix is gated by `WorldAssemblyEvaluatorVersion>=172`.
|
||||
- **Implementer result, L37:** The active v172 no-arch/−1 m connector build retained the surface route but its close capture still showed a central fin. L173 tests the previously effective −2 m sink with the arch removed; passage and both route audits remain gates.
|
||||
- **Implementer result, L37:** L173 removed the visible fin; its four surface-route misses were on the western island-0-to-1 connector at x≈−5…−3.5 m. L174 sinks only links touching anchored island 2 by 2 m, preserving the western connector height.
|
||||
- **Implementer trace, L37:** L37 ripple diagnostics show later coil queries matching the entrance relief by XY although they are about 55 m below it. The versioned L175 correction also gates the mouth fade to a 4 m vertical window around the entrance floor; the signed carve-distance lerp stays intact.
|
||||
- **Implementer experiment, L37:** The largest failing ripple window crosses two helix turns only 0.05 m apart horizontally and 4.17 m apart vertically at 3 m radius. L176 tests the 14.54° (still below 15°) route grade to increase turn separation.
|
||||
- **Implementer result, L37:** L176 reduced wall ripple to 3.01 cm but failed capsule support at one sample and left floor ripple at 2.18 cm. L177 restores the prior grade and tapers the anchored tunnel from 3 m to 2 m radius over 8–16 m from the mouth, after the fixed close camera.
|
||||
- **Implementer result, L37:** L177's 2 m tunnel radius was rejected: 103 blocked capsule samples and 3.15 cm floor ripple. The final source reverts L176/L177 route experiments and uses the v175 tunnel profile.
|
||||
- **Implementer correction, L37:** The final v175 close capture still shows a fin, so root-only connector sinking was insufficient. The global −2 m v173 view removed it, while four support misses were confined to island 0–1; v178 keeps that single link at −1 m and sinks every other connector to −2 m.
|
||||
- **Implementer result, L37:** v178 still shows the fin. L179 tests the island-0-to-1 connector at −1.5 m as a visual/support compromise; all other links stay at −2 m.
|
||||
- **Implementer result, L37:** L179's capture is byte-identical to the all−2 m v173 control, but route0 has two unsupported samples. L180 tests −1.25 m on the western link to recover those supports while keeping that appearance.
|
||||
- **Implementer trace, L37:** The crater bridge is the FI passage helix, not the island connector or short-span deck. Current control points put the route floor 5.4 m above native ground at radius 3 m inside the crater; the mesh at radius 7 m is about 0.1–0.3 m below the intended route floor. L185 tests moving the non-spiral FI passage ring from 3 m to mouth-offset+2 m (7 m), onto the native crater rim; the route/capsule/support audits remain gates.
|
||||
- **Implementer result, L37:** v188 fixes the bridge by placing the FI passage coil at 7 m (the 5 m mouth offset plus 2 m), on the native crater rim, and splitting the FI route legs after the entry into 1.375 m supports. The bridge is absent in the 6 m and 17 m views. FI route/capsule/support/open-air checks pass; floor ripple falls to 1.39 cm (1.5 cm gate), wall ripple passes. Full style/compiler audits still pending.
|
||||
- **Implementer result, L37 resumed:** v208 passes owner FI styles 0–3. Style 0 uses the terrain-following crater route and crater tread; other FI styles keep the native route/tread profile. The 8.5–12.5 m native mouth filter now applies to every FI passage style; this cleared open-air and style 1/2/3 ripple regressions. Full SW/fixture and compiler matrix still pending.
|
||||
- Implementer follow-up, L37 v208: a temporary support-off build reproduces the same 25.542° overview sample as the final mesh, so the remaining crease is not the route tread/support. At the 6 m view it is 4.396°; at 17 m the failing sample interval is 0.125 m and projects to 14.34 px at 2560×1440. The 20° overview gate remains a measured failure; no source change was made after the owner accepted the crater appearance.
|
||||
- Implementer cost, L37 v208: five interleaved pairs completed; median whole-view change was +0.63% surface-world and +2.04% floating-islands, within pair noise.
|
||||