#!/usr/bin/env python3 """Render arrival, departure, and mid-tunnel views for the four-strate owner plan.""" import json import importlib.util import argparse import math import os import re import struct import subprocess import time from pathlib import Path ROOT = Path(__file__).resolve().parents[4] OUT = ROOT / "Saved/Linux/l69" BIN = OUT / "bin" CAPTURES = Path(__file__).resolve().parent / "captures" HEADLESS = OUT / "bin/VoxelForgeCoreHeadless-l69" RENDERER = BIN / "VoxelForgeOffscreenCapture-l69" PNG_DIR = OUT / "capture-png" RECIPE_INPUTS = ( ("passage-0-1", ROOT / "Standalone/data/owner_world.json", "surface-world", "SurfaceWorld", "upper"), ("passage-1-2", ROOT / "Standalone/data/owner_world.json", "surface-world", "FloatingIslands", "upper"), ("passage-2-3", ROOT / "Standalone/data/owner_world.json", "surface-world", "TunnelNetwork", "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=("arrival-mouth", "departure-mouth", "mid-tunnel")) OPTIONS = PARSER.parse_args() def run(name, command): log = OUT / f"capture-{name}.log" wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", "timeout", "1740s", *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=1740, check=False) elapsed = time.monotonic() - started print(f"[L69CaptureRun] 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 = {} 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 q_to_meters(layout): return layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0) def q_position(value, scale): return tuple(float(component) * scale for component in value) def q_route(points, scale): return [(q_position(point["position_q"], scale), point["radius_q"] * scale) for point in points] def vector_length(value): return math.sqrt(sum(component * component for component in value)) def unit(value): length = vector_length(value) if length <= 1.0e-9: raise RuntimeError("capture camera direction is degenerate") return tuple(component / length for component in value) def add_scaled(point, direction, scale): return tuple(point[axis] + direction[axis] * scale for axis in range(3)) def make_player_views(layout): scale = q_to_meters(layout) passages = {(int(item["upper_strate_index"]), int(item["lower_strate_index"])): item for item in layout["passages"]} p01, p12, p23 = passages[(0, 1)], passages[(1, 2)], passages[(2, 3)] strate0 = next(item for item in layout["strates"] if int(item["strate_index"]) == 0) surface_anchors = [item["values"] for item in strate0.get("anchors", []) if item.get("kind") == "route_point"] if len(surface_anchors) < 2: raise RuntimeError("strate 0 has no resolved surface arrival route") surface_route = [(q_position(value[:3], scale), value[3] * scale) for value in surface_anchors] route_specs = [ (0, surface_route, True, surface_route[-1][0], p01, "upper", False), (1, q_route(p01["control_points"], scale), True, q_position(p01["lower_mouth_q"], scale), p12, "upper", True), (2, q_route(p12["control_points"], scale), True, q_position(p12["lower_mouth_q"], scale), p23, "upper", False), (3, q_route(p23["control_points"], scale), True, q_position(p23["lower_mouth_q"], scale), p23, "lower", True), ] views = {} for strate, source_route, reverse_route, spawn, departure, departure_end, reverse_departure in route_specs: route = list(reversed(source_route)) if reverse_route else list(source_route) if vector_length(sub(route[0][0], spawn)) > 5.0: raise RuntimeError(f"strate {strate} spawn is {vector_length(sub(route[0][0], spawn)):.2f} m from its arrival route") path_length = sum(vector_length(sub(b[0], a[0])) for a, b in zip(route, route[1:])) arrival_target_pos, arrival_target_radius = interpolate_route(route, min(20.0, path_length)) arrival_camera = (spawn[0], spawn[1], spawn[2] + 1.6) arrival_target = (arrival_target_pos[0], arrival_target_pos[1], arrival_target_pos[2] - arrival_target_radius + 1.8) ayaw, apitch = camera_angles(arrival_camera, arrival_target) views[(strate, "arrival")] = { "camera": arrival_camera, "target": arrival_target, "yaw": ayaw, "pitch": apitch, "fov_deg": 90.0, "description": "player eye at the strate arrival, facing into its route"} route_distance = min(10.0, path_length * 0.2) route_center, route_radius, _, _ = route_frame(route, route_distance) route_camera = (route_center[0], route_center[1], route_center[2] - route_radius + 1.6) route_target_pos, route_target_radius = interpolate_route( route, min(path_length, route_distance + 28.0)) route_target = (route_target_pos[0], route_target_pos[1], route_target_pos[2] - route_target_radius + 1.8) ryaw, rpitch = camera_angles(route_camera, route_target) views[(strate, "route")] = { "camera": route_camera, "target": route_target, "yaw": ryaw, "pitch": rpitch, "fov_deg": 90.0, "description": "player-height view along the passage or surface route"} mouth_path = q_route(departure["control_points"], scale) if departure_end == "lower": mouth_path.reverse() mouth, mouth_radius, inward, _ = route_frame(mouth_path, 0.0) departure_distance = min(7.4, sum(vector_length(sub(b[0], a[0])) for a, b in zip(mouth_path, mouth_path[1:]))) inside, inside_radius, _, _ = route_frame(mouth_path, departure_distance) departure_camera = (inside[0], inside[1], inside[2] - inside_radius + 1.6) outward = mul(inward, -1.0) departure_target = (mouth[0] + outward[0] * 12.0, mouth[1] + outward[1] * 12.0, mouth[2] - mouth_radius + 1.8) dyaw, dpitch = camera_angles(departure_camera, departure_target) views[(strate, "departure-mouth")] = { "camera": departure_camera, "target": departure_target, "yaw": dyaw, "pitch": dpitch, "fov_deg": 90.0, "description": f"inside the {departure_end} mouth looking out"} overview_camera = (spawn[0] - 12.0, spawn[1] - 16.0, spawn[2] + 70.0) overview_target = (spawn[0], spawn[1], spawn[2] - 4.0) oyaw, opitch = camera_angles(overview_camera, overview_target) views[(strate, "overview")] = { "camera": overview_camera, "target": overview_target, "yaw": oyaw, "pitch": opitch, "fov_deg": 90.0, "description": "overview from 70 m above the arrival"} return views def scene_records(path, include_offsets=False): records = [] keys = set() with path.open("rb") as stream: header = stream.read(60) if len(header) != 60 or header[:7] != b"VFMSH00": raise RuntimeError(f"{path} has an invalid mesh header") version, voxel_um, count = struct.unpack_from(" path.stat().st_size: raise RuntimeError(f"{path} ends inside a tile record") keys.add(key) if include_offsets: records.append((key, start, end)) if stream.read(1): raise RuntimeError(f"{path} has trailing data after {count} tiles") return header, voxel_um, keys, records def tile_keys_at(point, keys, voxel_um): voxel_m = voxel_um / 1_000_000.0 found = set() for level in range(9): extent = 32.0 * (1 << level) * voxel_m key = (math.floor(point[0] / extent), math.floor(point[1] / extent), math.floor(point[2] / extent), level) if key in keys: found.add(key) return found def frame_basis(yaw, pitch): yaw, pitch = math.radians(yaw), math.radians(pitch) forward = (math.sin(yaw) * math.cos(pitch), math.cos(yaw) * math.cos(pitch), math.sin(pitch)) right = (math.cos(yaw), -math.sin(yaw), 0.0) up = (-math.sin(yaw) * math.sin(pitch), -math.cos(yaw) * math.sin(pitch), math.cos(pitch)) return unit(forward), unit(right), unit(up) def coverage_audit(camera, yaw, pitch, fov, keys, voxel_um): forward, right, up = frame_basis(yaw, pitch) tan_h = math.tan(math.radians(fov) * 0.5) tan_v = tan_h * 900.0 / 1600.0 uncovered = [] missing_points = [] rays = 0 distance_samples = tuple(range(1, 200, 4)) for row in range(9): sy = (row / 8.0) * 2.0 - 1.0 for column in range(17): sx = (column / 16.0) * 2.0 - 1.0 ray = unit(tuple(forward[axis] + right[axis] * sx * tan_h + up[axis] * sy * tan_v for axis in range(3))) rays += 1 for distance in distance_samples: point = add_scaled(camera, ray, distance) if not tile_keys_at(point, keys, voxel_um): uncovered.append((row, column, distance)) missing_points.append(point) return {"rays": rays, "samples_per_ray": len(distance_samples), "missing_samples": len(uncovered), "first_missing": uncovered[:8], "missing_points": missing_points} def merge_scene_exports(destination, sources): unique = {} reference_header = None reference_version = None reference_voxel_um = None for source in sources: header, voxel_um, _, records = scene_records(source, include_offsets=True) version = struct.unpack_from(" 24.0 else total * 0.4 if (int(passage["upper_strate_index"],) == 1 and int(passage["lower_strate_index"]) == 2 and not reverse): # The old 7 m authored-radius beat sat near the middle of this audit window. middle_distance = min(total - 12.0, 90.8) 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 ground["outside_camera"] = True 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] - radius + 1.6) target_pos, target_radius = interpolate_route(path, target_distance) target = (target_pos[0], target_pos[1], target_pos[2] - target_radius + 1.6) 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} def inside_mouth_view(prefix, at_start): endpoint_distance = 0.0 if at_start else total mouth, _, tangent, mouth_side = route_frame(path, endpoint_distance) camera_distance = min(total, 7.4) if at_start else max(0.0, total - 5.5) center, _, _, side = route_frame(path, camera_distance) camera = (center[0] + side[0] * 2.5, center[1] + side[1] * 2.5, center[2]) outward = mul(tangent, -1.0) if at_start else tangent target = (mouth[0] + outward[0] * 8.0 + mouth_side[0] * 2.5, mouth[1] + outward[1] * 8.0 + mouth_side[1] * 2.5, mouth[2]) yaw, pitch = camera_angles(camera, target) return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch, "target": target, "clip": None, "camera_xy": camera[:2], "focus": mouth, "mouth": mouth, "outside_camera": False, "camera_location": "passage-air", "focus_radius": 30.0, "eye_height_z": camera[2], "fov_deg": 80.0} # Place each interface view at its true route endpoint. The endpoint camera # is a side cutaway so the solid terrain in front of an underground mouth # cannot hide the floor-to-tunnel connection. arrival_distance = 0.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_location"] = "passage-air" arrival["focus_radius"] = 20.0 arrival["yaw"], arrival["pitch"] = camera_angles( arrival["camera"], arrival["target"]) entry_camera_distance = min(8.0, total * 0.1) entry_center, entry_radius, entry_tangent, entry_side = route_frame( path, entry_camera_distance) # Player-height view from just inside the passage, aimed through its actual # interface mouth so the floor, bore, and opening share one frame. entry_camera = (entry_center[0], entry_center[1], entry_center[2] - entry_radius + 1.6) entry_target_pos, entry_target_radius = interpolate_route( path, min(total, entry_distance + 14.0)) entry_target = (entry_target_pos[0], entry_target_pos[1], entry_camera[2] - 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": False, "camera_location": "passage-air", "focus_radius": 20.0, "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_center, _ = interpolate_route(path, middle_distance) middle_view["camera"] = (middle_camera_center[0] + middle_side[0] * 0.75, middle_camera_center[1] + middle_side[1] * 0.75, middle[2] - middle_radius + 1.6) middle_target_pos, middle_target_radius = interpolate_route( path, min(total, middle_distance + 14.0)) middle_view["target"] = (middle_target_pos[0] + middle_side[0] * 2.35, middle_target_pos[1] + middle_side[1] * 2.35, middle_target_pos[2] - middle_target_radius + 1.8) # Keep the camera inside the bore, close to one wall, and look down the # route so the old beat location is visible at player height. middle_view["clip"] = None 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, inside_mouth_view("inside-mouth-start", True), inside_mouth_view("inside-mouth-end", False)), total, lower_index def export_capture(scene_name, source, recipe, passage, endpoint, view): stem = f"{passage['stable_id'].replace('/', '-')}-{endpoint}" view["name"] = stem 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"] export_camera = view.get("focus", camera) run(f"{stem}-export", [HEADLESS, source, "--workers=12", "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=3", "--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"] surface = None if view.get("outside_camera", False): surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2]) if surface is None: camera = (view["camera_xy"][0], view["camera_xy"][1], view["eye_height_z"]) else: camera = (surface[0], surface[1], surface[2] + 1.6) elif render_clip is not None: camera = view["camera"] yaw, pitch = camera_angles(camera, view["target"]) fov_deg = view.get("fov_deg", 80.0) print(f"[L68CaptureCamera] view={stem} camera_m=({fmt(camera)}) " f"target_m=({fmt(view['target'])}) " f"clip={fmt(render_clip) if render_clip is not None else 'none'} fov_deg={fov_deg:.1f}", flush=True) render = [RENDERER, mesh, png, "--size=1600x900", f"--fov-deg={fov_deg:.1f}", 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"[L68CaptureJPEG] 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")