- Unreal: ready queue depth 8 -> 16 (cap 4 worker-sets / 64), startup mesh-apply cap 12 -> 24 within the unchanged 14 ms budget. Owner SW arrival, same 2,850 keys: initial set ready 4.78 -> 3.74 s, queue p95 123 -> 35 ms, request-to-apply p95 139 -> 45 ms; walkable gate 1.07 -> 1.11 s; per-frame apply p95 0.73 ms. - Core composer: mouth-support segments filtered by tile bounds, mouth relief result reused (route checks -80/-87% in passage tiles). Hashes identical W1/W8/W12, Clang and GCC. - Opt-in per-tile composer counters. Verify control meshes stored as .xz (78.5 -> 18.4 MB). - Fast tier 269 PASS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
452 lines
22 KiB
Python
452 lines
22 KiB
Python
#!/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 sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT",
|
|
os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")))
|
|
BASELINES = OUT / "baselines"
|
|
BIN = OUT / "bin"
|
|
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
|
OUT / "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"
|
|
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
|
ROOT / "Standalone/data/owner_world.json"))
|
|
RECIPE_INPUTS = (
|
|
("owner-fi", OWNER,
|
|
"floating-islands", "FloatingIslands", "upper"),
|
|
("owner-sw", OWNER,
|
|
"surface-world", "SurfaceWorld", "lower"),
|
|
("style-override-upper-sw-fixture-style0",
|
|
OUT / "fixtures/style-override-upper-sw-fixture-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)
|
|
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
|
from runner import current_stamp, first_stamp, bounded_run
|
|
|
|
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()
|
|
binary = Path(command[0]) if command else None
|
|
is_stamped_executable = binary in (HEADLESS, RENDERER)
|
|
expected = current_stamp() if is_stamped_executable else None
|
|
if is_stamped_executable:
|
|
probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
|
if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected:
|
|
raise SystemExit(f"{name} refused an unstamped or stale executable: {binary}")
|
|
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 is_stamped_executable:
|
|
output = log.read_text(encoding="utf-8", errors="replace")
|
|
if first_stamp(output) != expected or current_stamp() != expected:
|
|
raise SystemExit(f"{name} source stamp mismatch; see {log}")
|
|
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 = BASELINES / "capture/l34-standard-set-cameras.baseline"
|
|
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"])
|
|
layout = json.loads(path.read_text(encoding="utf-8"))
|
|
passage_styles = sorted({int(passage["style"]) for passage in layout.get("passages", [])})
|
|
resolved_styles = {str(index): int(style) for index, style in
|
|
layout.get("resolved_strate_passage_styles", {}).items()}
|
|
if name.startswith("owner-"):
|
|
expected_styles = {"0": 2, "1": 1}
|
|
if resolved_styles != expected_styles or passage_styles != [2]:
|
|
raise SystemExit(f"{name} resolved styles={resolved_styles} passage_styles={passage_styles}; "
|
|
f"expected strata={expected_styles} passage_styles=[2]")
|
|
expected_style = 2 if name == "owner-fi" else 1
|
|
print(f"[L37CaptureResolvedOwner] scene={name} recipe={recipe} "
|
|
f"generator_style={expected_style} strata_styles={resolved_styles} "
|
|
f"passage_style={passage_styles} layout_id={layout.get('stable_identity')} input={source}",
|
|
flush=True)
|
|
return layout
|
|
expected_style = 0
|
|
matching_styles = sorted({resolved_styles[str(stratum["strate_index"])]
|
|
for stratum in layout.get("strates", [])
|
|
if stratum.get("generator_kind") == "SurfaceWorld"
|
|
and str(stratum["strate_index"]) in resolved_styles})
|
|
if expected_style not in passage_styles or matching_styles != [expected_style]:
|
|
raise SystemExit(f"{name} resolved style={matching_styles} passage styles={passage_styles}; "
|
|
f"expected [{expected_style}]")
|
|
print(f"[L37CaptureResolvedOwner] scene={name} recipe={recipe} style={matching_styles} "
|
|
f"layout_id={layout.get('stable_identity')} input={source}", flush=True)
|
|
return layout
|
|
|
|
|
|
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 scene_name.startswith("style-override-") else None)
|
|
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])
|
|
run(f"{stem}-quality", [sys.executable,
|
|
ROOT / "Standalone/Tools/verify/capture_quality.py", 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")
|