#!/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")