| |
| """Render one accessibility geometry mesh into a compact turntable review set. |
| |
| Open3D is imported only after the render environment has been configured. This |
| keeps argument/manifest helpers usable on CPU-only review machines and makes a |
| software fallback an explicit, auditable user choice. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import math |
| import os |
| import re |
| from datetime import datetime, timezone |
| from pathlib import Path |
| from typing import Any, Mapping, MutableMapping, Sequence |
|
|
| import imageio.v2 as imageio |
| import numpy as np |
| from PIL import Image, ImageDraw, ImageOps |
|
|
|
|
| REQUESTED_RENDERER = "Open3D/EGL" |
| RENDERER_IMPLEMENTATION = "open3d.visualization.rendering.OffscreenRenderer" |
| PRODUCTION_RENDER_DEVICE = "physical_nvidia_gpu" |
| DOMINANT_COMPONENT_MIN_FACE_FRACTION = 0.90 |
| GL_VENDOR = 0x1F00 |
| GL_RENDERER = 0x1F01 |
| GL_VERSION = 0x1F02 |
| GL_STRING_NAMES = { |
| GL_VENDOR: "GL_VENDOR", |
| GL_RENDERER: "GL_RENDERER", |
| GL_VERSION: "GL_VERSION", |
| } |
| SOFTWARE_OPENGL_MARKERS = ( |
| "llvmpipe", |
| "softpipe", |
| "swrast", |
| "software", |
| "swiftshader", |
| "lavapipe", |
| ) |
| NVIDIA_EGL_VENDOR_JSON = Path( |
| "/usr/share/glvnd/egl_vendor.d/10_nvidia.json" |
| ) |
| OUTPUT_NAMES = { |
| "video": "turntable.mp4", |
| "gif": "turntable.gif", |
| "poster": "turntable_poster.png", |
| "multiview": "multiview.jpg", |
| } |
|
|
|
|
| def parse_color(value: str) -> tuple[float, float, float, float]: |
| """Parse an RGB/RGBA command-line color in the inclusive [0, 1] range.""" |
| values = [ |
| float(item.strip()) |
| for item in value.replace(":", ",").replace(";", ",").split(",") |
| if item.strip() |
| ] |
| if len(values) == 3: |
| values.append(1.0) |
| if len(values) != 4: |
| raise argparse.ArgumentTypeError("expected three or four comma-separated numbers") |
| if any(not math.isfinite(item) or item < 0.0 or item > 1.0 for item in values): |
| raise argparse.ArgumentTypeError("background components must be between 0 and 1") |
| return tuple(values) |
|
|
|
|
| def parse_yaws(value: str) -> tuple[float, ...]: |
| """Parse the yaw angles used in the multiview contact sheet.""" |
| try: |
| values = tuple(float(item.strip()) for item in value.split(",") if item.strip()) |
| except ValueError as error: |
| raise argparse.ArgumentTypeError("multiview yaws must be comma-separated numbers") from error |
| if not values: |
| raise argparse.ArgumentTypeError("at least one multiview yaw is required") |
| if len(values) > 12: |
| raise argparse.ArgumentTypeError("at most 12 multiview yaws are supported") |
| if any(not math.isfinite(item) for item in values): |
| raise argparse.ArgumentTypeError("multiview yaws must be finite") |
| return values |
|
|
|
|
| def build_argument_parser() -> argparse.ArgumentParser: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| "--mesh", |
| "--ply", |
| dest="mesh", |
| required=True, |
| help="Input triangle mesh. --ply is retained as a convenient alias.", |
| ) |
| parser.add_argument("--output-dir", required=True) |
| parser.add_argument("--sample-id", default=None) |
| parser.add_argument("--category", default="unknown") |
| parser.add_argument("--width", type=int, default=960) |
| parser.add_argument("--height", type=int, default=720) |
| parser.add_argument("--frames", type=int, default=96) |
| parser.add_argument("--fps", type=int, default=24) |
| parser.add_argument("--fov", type=float, default=32.0) |
| parser.add_argument("--elevation", type=float, default=18.0) |
| parser.add_argument("--distance-scale", type=float, default=1.25) |
| parser.add_argument("--shader", default="defaultUnlit") |
| parser.add_argument( |
| "--vertex-colors-srgb", |
| action=argparse.BooleanOptionalAction, |
| default=True, |
| help=( |
| "Treat byte RGB vertex colors sampled from photographs as sRGB. " |
| "Enabled by default to avoid linear-color whitening." |
| ), |
| ) |
| parser.add_argument("--background", type=parse_color, default=parse_color("0.82,0.82,0.80,1")) |
| parser.add_argument("--smooth-iterations", type=int, default=0) |
| parser.add_argument( |
| "--orbit-mode", |
| choices=("full_360", "front_arc"), |
| default="front_arc", |
| help=( |
| "full_360 is a conventional object turntable. front_arc keeps an " |
| "open-world scene in camera-facing review views and avoids treating " |
| "its unobserved back side as reconstructed evidence." |
| ), |
| ) |
| parser.add_argument( |
| "--orbit-span", |
| type=float, |
| default=140.0, |
| help="Total yaw span in degrees for --orbit-mode front_arc.", |
| ) |
| parser.add_argument( |
| "--multiview-yaws", |
| type=parse_yaws, |
| default=parse_yaws("-60,-30,0,30,60"), |
| help=( |
| "Comma-separated camera-facing yaw angles. In front_arc mode every " |
| "angle must stay within half of --orbit-span." |
| ), |
| ) |
| parser.add_argument( |
| "--require-vertex-colors", |
| action="store_true", |
| help=( |
| "Reject meshes without a complete finite RGB value for every rendered " |
| "vertex instead of silently producing an untextured white surface." |
| ), |
| ) |
| parser.add_argument( |
| "--cpu-fallback", |
| action="store_true", |
| |
| |
| |
| help=argparse.SUPPRESS, |
| ) |
| return parser |
|
|
|
|
| def validate_arguments(args: argparse.Namespace) -> None: |
| if args.width < 64 or args.height < 64: |
| raise ValueError("--width and --height must each be at least 64") |
| if args.frames < 2: |
| raise ValueError("--frames must be at least 2") |
| if args.fps < 1: |
| raise ValueError("--fps must be at least 1") |
| if not math.isfinite(args.fov) or not (5.0 <= args.fov < 170.0): |
| raise ValueError("--fov must satisfy 5 <= fov < 170") |
| if not math.isfinite(args.elevation): |
| raise ValueError("--elevation must be finite") |
| if not math.isfinite(args.distance_scale) or args.distance_scale <= 0: |
| raise ValueError("--distance-scale must be positive") |
| if args.smooth_iterations < 0: |
| raise ValueError("--smooth-iterations must be non-negative") |
| if not (20.0 <= args.orbit_span <= 240.0): |
| raise ValueError("--orbit-span must satisfy 20 <= span <= 240") |
| if args.orbit_mode == "front_arc": |
| half_span = 0.5 * args.orbit_span |
| if any(abs(yaw) > half_span + 1e-6 for yaw in args.multiview_yaws): |
| raise ValueError( |
| "front_arc multiview yaws must stay within half of --orbit-span" |
| ) |
|
|
|
|
| def _visibility_state(value: str | None) -> str: |
| if value is None: |
| return "unspecified" |
| if not value.strip() or value.strip().lower() in {"-1", "none", "void", "nodevfiles"}: |
| return "hidden_or_disabled" |
| return "configured_visible_set" |
|
|
|
|
| def inspect_render_environment( |
| environment: Mapping[str, str] | None = None, |
| *, |
| cpu_fallback: bool = False, |
| ) -> dict[str, Any]: |
| """Return environment evidence without claiming that Open3D used a GPU. |
| |
| CUDA visibility and EGL configuration are useful diagnostics, but neither |
| proves which physical renderer Filament selected. The returned status is |
| intentionally conservative. |
| """ |
| env = os.environ if environment is None else environment |
| cuda_visible = env.get("CUDA_VISIBLE_DEVICES") |
| egl_platform = env.get("EGL_PLATFORM") |
| software_value = env.get("LIBGL_ALWAYS_SOFTWARE") |
| software_requested = cpu_fallback or software_value == "1" |
| return { |
| "slurm": { |
| "job_id": env.get("SLURM_JOB_ID"), |
| "job_gpus": env.get("SLURM_JOB_GPUS"), |
| "gpu_allocation_present": bool(env.get("SLURM_JOB_ID")), |
| }, |
| "egl": { |
| "platform": egl_platform, |
| "platform_configured": egl_platform is not None, |
| "visibility": "configured" if egl_platform is not None else "unspecified", |
| "device_id": env.get("EGL_DEVICE_ID"), |
| "gpu_use": "not_proven", |
| }, |
| "cuda": { |
| "visible_devices": cuda_visible, |
| "visibility": _visibility_state(cuda_visible), |
| "used_by_renderer": "not_proven", |
| }, |
| "software_rendering": { |
| "requested": software_requested, |
| "libgl_always_software": software_value, |
| "mesa_loader_driver_override": env.get("MESA_LOADER_DRIVER_OVERRIDE"), |
| "gallium_driver": env.get("GALLIUM_DRIVER"), |
| }, |
| "hardware_acceleration": { |
| "status": ( |
| "software_fallback_requested_backend_unverified" |
| if software_requested |
| else "hardware_backend_unverified" |
| ), |
| "reason": ( |
| "Open3D OffscreenRenderer does not expose reliable physical-device " |
| "identity through this render path; environment visibility alone is " |
| "not hardware proof." |
| ), |
| }, |
| } |
|
|
|
|
| def require_slurm_gpu_environment( |
| environment: Mapping[str, str] | None = None, |
| ) -> dict[str, Any]: |
| """Fail closed unless rendering runs inside a visible Slurm GPU allocation.""" |
|
|
| env = os.environ if environment is None else environment |
| job_id = str(env.get("SLURM_JOB_ID") or "").strip() |
| cuda_visible = str(env.get("CUDA_VISIBLE_DEVICES") or "").strip() |
| if not job_id: |
| raise RuntimeError( |
| "GPU rendering is required by default and must run inside a Slurm " |
| "allocation. Use sbatch/srun, or explicitly pass --cpu-fallback " |
| "for a diagnostic software render." |
| ) |
| if not cuda_visible or cuda_visible.lower() in {"-1", "none", "void", "nodevfiles"}: |
| raise RuntimeError( |
| "The Slurm render job has no visible CUDA device; refusing an " |
| "implicit CPU/software fallback." |
| ) |
| return { |
| "policy": "slurm_gpu_required_unless_cpu_fallback_explicit", |
| "slurm_job_id": job_id, |
| "cuda_visible_devices": cuda_visible, |
| "gpu_allocation_contract_satisfied": True, |
| } |
|
|
|
|
| def configure_render_environment( |
| cpu_fallback: bool, |
| environment: MutableMapping[str, str] | None = None, |
| ) -> dict[str, Any]: |
| """Configure EGL before importing Open3D and return auditable evidence.""" |
| env = os.environ if environment is None else environment |
| original = { |
| key: env.get(key) |
| for key in ( |
| "EGL_PLATFORM", |
| "CUDA_VISIBLE_DEVICES", |
| "__EGL_VENDOR_LIBRARY_FILENAMES", |
| "LIBGL_ALWAYS_SOFTWARE", |
| "MESA_LOADER_DRIVER_OVERRIDE", |
| "GALLIUM_DRIVER", |
| ) |
| } |
| env.setdefault("EGL_PLATFORM", "surfaceless") |
| gpu_contract = None |
| if cpu_fallback: |
| env["LIBGL_ALWAYS_SOFTWARE"] = "1" |
| env.setdefault("MESA_LOADER_DRIVER_OVERRIDE", "llvmpipe") |
| env.setdefault("GALLIUM_DRIVER", "llvmpipe") |
| else: |
| gpu_contract = require_slurm_gpu_environment(env) |
| if NVIDIA_EGL_VENDOR_JSON.is_file(): |
| env.setdefault( |
| "__EGL_VENDOR_LIBRARY_FILENAMES", |
| str(NVIDIA_EGL_VENDOR_JSON), |
| ) |
| evidence = inspect_render_environment(env, cpu_fallback=cpu_fallback) |
| evidence["configuration"] = { |
| "original": original, |
| "egl_platform_defaulted_by_tool": original["EGL_PLATFORM"] is None, |
| "nvidia_egl_vendor_defaulted_by_tool": ( |
| not cpu_fallback |
| and original["__EGL_VENDOR_LIBRARY_FILENAMES"] is None |
| and env.get("__EGL_VENDOR_LIBRARY_FILENAMES") |
| == str(NVIDIA_EGL_VENDOR_JSON) |
| ), |
| "cpu_fallback_explicit": cpu_fallback, |
| "gpu_contract": gpu_contract, |
| } |
| return evidence |
|
|
|
|
| def _load_gl_get_string() -> tuple[Any, str]: |
| """Resolve ``glGetString`` without importing an OpenGL Python package. |
| |
| Open3D wheels do not expose Filament's selected renderer through their |
| Python API. Once ``OffscreenRenderer`` has created its EGL context, the GL |
| dispatch function is sufficient to query the active context directly. |
| """ |
|
|
| import ctypes |
| import ctypes.util |
|
|
| attempts: list[str] = [] |
| candidates: list[tuple[str, str | None]] = [("process", None)] |
| for library_name in ("GL", "OpenGL", "GLESv2"): |
| library_path = ctypes.util.find_library(library_name) |
| if library_path: |
| candidates.append((library_name, library_path)) |
|
|
| for label, library_path in candidates: |
| try: |
| library = ctypes.CDLL(library_path) |
| gl_get_string = getattr(library, "glGetString") |
| except (AttributeError, OSError) as error: |
| attempts.append(f"{label}: {error}") |
| continue |
| gl_get_string.argtypes = [ctypes.c_uint] |
| gl_get_string.restype = ctypes.c_char_p |
| return gl_get_string, f"ctypes:{label}" |
|
|
| |
| |
| egl_path = ctypes.util.find_library("EGL") |
| if egl_path: |
| try: |
| egl = ctypes.CDLL(egl_path) |
| egl_get_proc_address = egl.eglGetProcAddress |
| egl_get_proc_address.argtypes = [ctypes.c_char_p] |
| egl_get_proc_address.restype = ctypes.c_void_p |
| address = egl_get_proc_address(b"glGetString") |
| if address: |
| prototype = ctypes.CFUNCTYPE(ctypes.c_char_p, ctypes.c_uint) |
| return prototype(address), "ctypes:EGL.eglGetProcAddress" |
| attempts.append("EGL.eglGetProcAddress: returned NULL") |
| except (AttributeError, OSError) as error: |
| attempts.append(f"EGL: {error}") |
|
|
| detail = "; ".join(attempts) or "no OpenGL libraries were discoverable" |
| raise RuntimeError(f"Could not resolve glGetString ({detail})") |
|
|
|
|
| def _decode_gl_string(value: Any) -> str | None: |
| """Convert a ``glGetString`` result into a non-empty Python string.""" |
|
|
| if value is None: |
| return None |
| if isinstance(value, bytes): |
| decoded = value.decode("utf-8", errors="replace") |
| elif isinstance(value, str): |
| decoded = value |
| else: |
| decoded = str(value) |
| decoded = decoded.strip() |
| return decoded or None |
|
|
|
|
| def query_current_opengl_context( |
| gl_get_string: Any | None = None, |
| *, |
| query_source: str | None = None, |
| ) -> dict[str, Any]: |
| """Query the OpenGL strings for the context current on this thread. |
| |
| The optional callable is an injection seam for CPU-only unit tests. In |
| production this function is called immediately after constructing Open3D's |
| ``OffscreenRenderer``. |
| """ |
|
|
| evidence: dict[str, Any] = { |
| "query_api": "glGetString", |
| "query_source": query_source, |
| "query_succeeded": False, |
| "GL_VENDOR": None, |
| "GL_RENDERER": None, |
| "GL_VERSION": None, |
| "software_renderer_detected": False, |
| "software_markers": [], |
| "verification_status": "active_context_unverified", |
| "error": None, |
| } |
| try: |
| if gl_get_string is None: |
| gl_get_string, resolved_source = _load_gl_get_string() |
| evidence["query_source"] = resolved_source |
| elif evidence["query_source"] is None: |
| evidence["query_source"] = "injected_gl_get_string" |
|
|
| errors: list[str] = [] |
| for enum_value, field_name in GL_STRING_NAMES.items(): |
| try: |
| evidence[field_name] = _decode_gl_string(gl_get_string(enum_value)) |
| except Exception as error: |
| errors.append(f"{field_name}: {type(error).__name__}: {error}") |
| missing = [name for name in GL_STRING_NAMES.values() if not evidence[name]] |
| if missing: |
| errors.append("NULL or empty values: " + ", ".join(missing)) |
| evidence["query_succeeded"] = not errors |
| if errors: |
| evidence["error"] = "; ".join(errors) |
| except Exception as error: |
| evidence["error"] = f"{type(error).__name__}: {error}" |
|
|
| combined = " ".join( |
| str(evidence[name] or "").lower() for name in GL_STRING_NAMES.values() |
| ) |
| markers = [marker for marker in SOFTWARE_OPENGL_MARKERS if marker in combined] |
| evidence["software_markers"] = markers |
| evidence["software_renderer_detected"] = bool(markers) |
| if evidence["query_succeeded"]: |
| evidence["verification_status"] = ( |
| "active_software_context_verified" |
| if markers |
| else "active_non_software_context_verified" |
| ) |
| return evidence |
|
|
|
|
| def inspect_process_graphics_backend( |
| *, |
| maps_text: str | None = None, |
| fd_targets: Sequence[str] | None = None, |
| environment: Mapping[str, str] | None = None, |
| ) -> dict[str, Any]: |
| """Inspect process-wide EGL libraries and open GPU device handles. |
| |
| Filament owns its OpenGL context on an internal render thread, so |
| ``glGetString`` from the Python thread may legitimately return NULL. In |
| that case a process-wide NVIDIA EGL binding plus an open physical |
| ``/dev/nvidiaN`` handle is stronger evidence than CUDA visibility alone. |
| Injection seams keep this policy fully unit-testable without a GPU. |
| """ |
|
|
| env = os.environ if environment is None else environment |
| errors: list[str] = [] |
| if maps_text is None: |
| try: |
| maps_text = Path("/proc/self/maps").read_text( |
| encoding="utf-8", |
| errors="replace", |
| ) |
| except OSError as error: |
| maps_text = "" |
| errors.append(f"maps: {type(error).__name__}: {error}") |
| if fd_targets is None: |
| discovered_targets: list[str] = [] |
| try: |
| for entry in Path("/proc/self/fd").iterdir(): |
| try: |
| discovered_targets.append(os.readlink(entry)) |
| except OSError: |
| continue |
| except OSError as error: |
| errors.append(f"fd: {type(error).__name__}: {error}") |
| fd_targets = discovered_targets |
|
|
| mapped_paths = sorted( |
| { |
| line.rsplit(None, 1)[-1] |
| for line in maps_text.splitlines() |
| if line.strip() and "/" in line.rsplit(None, 1)[-1] |
| } |
| ) |
| nvidia_egl_libraries = [ |
| path |
| for path in mapped_paths |
| if ( |
| "libegl_nvidia" in path.lower() |
| or "libnvidia-eglcore" in path.lower() |
| ) |
| ] |
| software_library_markers = sorted( |
| { |
| marker |
| for path in mapped_paths |
| for marker in ( |
| "libegl_mesa", |
| "llvmpipe", |
| "softpipe", |
| "swrast", |
| "lavapipe", |
| "swiftshader", |
| ) |
| if marker in path.lower() |
| } |
| ) |
| gpu_device_fds = sorted( |
| {target for target in fd_targets if target.startswith("/dev/nvidia")} |
| ) |
| physical_gpu_fds = [ |
| target |
| for target in gpu_device_fds |
| if re.fullmatch(r"/dev/nvidia\d+", target) |
| ] |
| nvidia_backend_verified = bool( |
| nvidia_egl_libraries |
| and physical_gpu_fds |
| and not software_library_markers |
| ) |
| return { |
| "inspection_api": "/proc/self/maps + /proc/self/fd", |
| "nvidia_egl_libraries": nvidia_egl_libraries, |
| "software_library_markers": software_library_markers, |
| "gpu_device_fds": gpu_device_fds, |
| "physical_gpu_fds": physical_gpu_fds, |
| "forced_egl_vendor_json": env.get( |
| "__EGL_VENDOR_LIBRARY_FILENAMES" |
| ), |
| "nvidia_process_backend_verified": nvidia_backend_verified, |
| "verification_status": ( |
| "nvidia_egl_and_physical_device_fd_verified" |
| if nvidia_backend_verified |
| else "process_graphics_backend_unverified" |
| ), |
| "errors": errors, |
| } |
|
|
|
|
| def enforce_opengl_context_policy( |
| opengl_context: Mapping[str, Any], |
| *, |
| cpu_fallback: bool, |
| process_backend: Mapping[str, Any] | None = None, |
| ) -> dict[str, Any]: |
| """Apply the fail-closed GPU policy to actual active-context evidence.""" |
|
|
| query_succeeded = bool(opengl_context.get("query_succeeded")) |
| software_detected = bool(opengl_context.get("software_renderer_detected")) |
| renderer_name = opengl_context.get("GL_RENDERER") |
| nvidia_process_verified = bool( |
| process_backend |
| and process_backend.get("nvidia_process_backend_verified") |
| ) |
|
|
| if cpu_fallback: |
| if not query_succeeded: |
| status = "cpu_fallback_requested_context_unverified" |
| reason = ( |
| "Explicit --cpu-fallback permits diagnostic rendering even though " |
| "the active OpenGL strings could not be verified." |
| ) |
| elif software_detected: |
| status = "software_fallback_active_context_verified" |
| reason = ( |
| "Explicit --cpu-fallback was requested and the active OpenGL " |
| "context identifies a software renderer." |
| ) |
| else: |
| status = "cpu_fallback_requested_non_software_context_verified" |
| reason = ( |
| "Explicit --cpu-fallback was requested, but the active OpenGL " |
| "strings do not identify a known software renderer." |
| ) |
| return { |
| "status": status, |
| "reason": reason, |
| "active_context_verified": query_succeeded, |
| "software_renderer_detected": software_detected, |
| "physical_gpu_identity_proven": False, |
| } |
|
|
| if software_detected: |
| markers = ", ".join( |
| str(value) for value in opengl_context.get("software_markers", []) |
| ) |
| raise RuntimeError( |
| "The active OpenGL context is a software OpenGL renderer " |
| f"(GL_RENDERER={renderer_name!r}; markers={markers}); refusing the " |
| "default GPU render. Use --cpu-fallback to opt in explicitly." |
| ) |
| if nvidia_process_verified: |
| physical_gpu_fds = list( |
| process_backend.get("physical_gpu_fds", []) |
| if process_backend is not None |
| else [] |
| ) |
| return { |
| "status": "active_nvidia_egl_process_backend_verified", |
| "reason": ( |
| ( |
| "The active OpenGL context contains no software-renderer " |
| "marker, and the renderer process loaded NVIDIA EGL without " |
| "a known software library and opened a physical NVIDIA GPU " |
| "device." |
| ) |
| if query_succeeded |
| else ( |
| "Filament's render-thread GL strings were not visible to " |
| "Python, but the renderer process loaded NVIDIA EGL without " |
| "a known software library and opened a physical NVIDIA GPU " |
| "device." |
| ) |
| ), |
| "active_context_verified": query_succeeded, |
| "process_backend_verified": True, |
| "software_renderer_detected": False, |
| "physical_gpu_identity_proven": True, |
| "physical_gpu_fds": physical_gpu_fds, |
| } |
| if not query_succeeded: |
| detail = str(opengl_context.get("error") or "no GL strings were returned") |
| raise RuntimeError( |
| "Could not verify the active OpenGL context after creating " |
| f"OffscreenRenderer; refusing the default GPU render ({detail}). " |
| "Use --cpu-fallback only for an explicit diagnostic software render." |
| ) |
| raise RuntimeError( |
| "The active OpenGL strings contain no known software marker, but the " |
| "renderer process did not prove an NVIDIA EGL backend plus a physical " |
| "/dev/nvidiaN device; refusing the default GPU render. Use " |
| "--cpu-fallback only for an explicit diagnostic software render." |
| ) |
|
|
|
|
| def _merge_runtime_graphics_evidence( |
| environment_evidence: Mapping[str, Any], |
| render_result: Mapping[str, Any], |
| ) -> dict[str, Any]: |
| """Combine pre-import environment facts with post-context GL evidence.""" |
|
|
| merged = dict(environment_evidence) |
| opengl_context = render_result.get("opengl_context") |
| process_backend = render_result.get("process_graphics_backend") |
| hardware_acceleration = render_result.get("hardware_acceleration") |
| if isinstance(opengl_context, Mapping): |
| merged["opengl_context"] = dict(opengl_context) |
| if isinstance(process_backend, Mapping): |
| merged["process_graphics_backend"] = dict(process_backend) |
| if isinstance(hardware_acceleration, Mapping): |
| merged["hardware_acceleration"] = dict(hardware_acceleration) |
| return merged |
|
|
|
|
| def relative_path(path: Path, anchor: Path) -> str: |
| """Return a portable relative path, even for an input outside ``anchor``.""" |
| return Path(os.path.relpath(path.resolve(), anchor.resolve())).as_posix() |
|
|
|
|
| def file_record(path: Path, anchor: Path) -> dict[str, Any]: |
| import hashlib |
|
|
| digest = hashlib.sha256() |
| with path.open("rb") as handle: |
| for chunk in iter(lambda: handle.read(1024 * 1024), b""): |
| digest.update(chunk) |
| return { |
| "path": relative_path(path, anchor), |
| "sha256": digest.hexdigest(), |
| "bytes": path.stat().st_size, |
| } |
|
|
|
|
| def build_render_manifest( |
| *, |
| mesh_path: Path, |
| output_dir: Path, |
| sample_id: str, |
| category: str, |
| render_settings: Mapping[str, Any], |
| render_result: Mapping[str, Any], |
| environment_evidence: Mapping[str, Any], |
| cpu_fallback: bool, |
| ) -> dict[str, Any]: |
| """Build a manifest whose filesystem paths are all relative.""" |
| runtime_evidence = _merge_runtime_graphics_evidence( |
| environment_evidence, |
| render_result, |
| ) |
| opengl_context = dict(runtime_evidence.get("opengl_context", {})) |
| process_backend = dict( |
| runtime_evidence.get("process_graphics_backend", {}) |
| ) |
| outputs = { |
| role: file_record(output_dir / filename, output_dir) |
| for role, filename in OUTPUT_NAMES.items() |
| } |
| return { |
| "schema_version": "accessibilityamodal_turntable_v2", |
| "created_at_utc": datetime.now(timezone.utc).isoformat(), |
| "sample_id": sample_id, |
| "category": category, |
| "requested_renderer": REQUESTED_RENDERER, |
| "renderer": { |
| "requested": REQUESTED_RENDERER, |
| "implementation": RENDERER_IMPLEMENTATION, |
| "production_render_device": PRODUCTION_RENDER_DEVICE, |
| "production_gpu_fail_closed": True, |
| "requested_execution": ( |
| "cpu_fallback_explicit" |
| if cpu_fallback |
| else "slurm_gpu_required_egl" |
| ), |
| "cpu_fallback_explicit": cpu_fallback, |
| "gpu_required_by_default": True, |
| "render_call_succeeded": True, |
| "hardware_acceleration_status": runtime_evidence[ |
| "hardware_acceleration" |
| ]["status"], |
| "opengl_context": opengl_context, |
| "process_graphics_backend": process_backend, |
| }, |
| "environment_evidence": runtime_evidence, |
| "input": { |
| "mesh": file_record(mesh_path, output_dir), |
| }, |
| "outputs": outputs, |
| "render_settings": dict(render_settings), |
| "mesh_summary": { |
| "vertices": int(render_result["vertices"]), |
| "renderable_vertices": int( |
| render_result.get("renderable_vertices", render_result["vertices"]) |
| ), |
| "unreferenced_vertices_excluded_from_camera_fit": int( |
| render_result.get( |
| "unreferenced_vertices_excluded_from_camera_fit", |
| 0, |
| ) |
| ), |
| "faces": int(render_result["faces"]), |
| "camera_framing": dict(render_result.get("camera_framing", {})), |
| "camera_motion": dict(render_result.get("camera", {})), |
| "vertex_colors": dict(render_result.get("vertex_colors", {})), |
| }, |
| "scene_review": dict(render_result.get("orbit", {})), |
| "limitations": [ |
| "GPU mode rejects known software GL strings; when Filament keeps its context on a render thread, NVIDIA EGL mappings plus a physical /dev/nvidiaN handle provide process-backend proof.", |
| "When one connected surface contains at least 90% of faces, camera framing prioritizes it; detached islands remain in the mesh but may fall outside a close-up view.", |
| "front_arc shows the observed scene-facing hemisphere and intentionally does not claim an inferred back side.", |
| "This rotating geometry is review evidence, not metric accessibility or navigation certification.", |
| ], |
| "path_policy": "All filesystem paths in this manifest are relative to manifest.json.", |
| } |
|
|
|
|
| def _camera_pose( |
| center: np.ndarray, |
| camera_distance: float, |
| yaw_degrees: float, |
| elevation_degrees: float, |
| ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: |
| yaw = math.radians(yaw_degrees) |
| elevation = math.radians(elevation_degrees) |
| |
| |
| distance = max(camera_distance, 1e-3) |
| eye = center + np.array( |
| [ |
| distance * math.sin(yaw) * math.cos(elevation), |
| -distance * math.cos(yaw) * math.cos(elevation), |
| distance * math.sin(elevation), |
| ], |
| dtype=np.float64, |
| ) |
| return eye, center, np.array([0.0, 0.0, 1.0], dtype=np.float64) |
|
|
|
|
| def _orbit_yaws( |
| frames: int, |
| *, |
| mode: str, |
| span_degrees: float, |
| ) -> list[float]: |
| """Return deterministic yaw positions for object or open-scene review.""" |
|
|
| if frames < 2: |
| raise ValueError("orbit requires at least two frames") |
| if mode == "full_360": |
| return [360.0 * index / frames for index in range(frames)] |
| if mode != "front_arc": |
| raise ValueError(f"Unsupported orbit mode: {mode}") |
| half_span = 0.5 * float(span_degrees) |
| |
| |
| |
| return [ |
| half_span * math.sin(2.0 * math.pi * index / frames) |
| for index in range(frames) |
| ] |
|
|
|
|
| def _fitted_camera_distance( |
| bbox_extent: np.ndarray, |
| vertical_fov_degrees: float, |
| aspect_ratio: float, |
| ) -> float: |
| radius = max(float(np.linalg.norm(bbox_extent)) * 0.5, 1e-3) |
| vertical_fov = math.radians(vertical_fov_degrees) |
| horizontal_fov = 2.0 * math.atan(math.tan(vertical_fov * 0.5) * aspect_ratio) |
| limiting_fov = min(vertical_fov, horizontal_fov) |
| return radius / max(math.sin(limiting_fov * 0.5), 1e-3) |
|
|
|
|
| def _fitted_camera_distance_for_view( |
| vertices: np.ndarray, |
| center: np.ndarray, |
| *, |
| yaw_degrees: float, |
| elevation_degrees: float, |
| vertical_fov_degrees: float, |
| aspect_ratio: float, |
| distance_scale: float, |
| ) -> float: |
| """Fit an elongated surface for one view without a global-sphere zoom-out.""" |
|
|
| yaw = math.radians(yaw_degrees) |
| elevation = math.radians(elevation_degrees) |
| eye_direction = np.array( |
| [ |
| math.sin(yaw) * math.cos(elevation), |
| -math.cos(yaw) * math.cos(elevation), |
| math.sin(elevation), |
| ], |
| dtype=np.float64, |
| ) |
| forward = -eye_direction |
| world_up = np.array([0.0, 0.0, 1.0], dtype=np.float64) |
| right = np.cross(forward, world_up) |
| right_norm = float(np.linalg.norm(right)) |
| if right_norm < 1e-8: |
| right = np.array([1.0, 0.0, 0.0], dtype=np.float64) |
| else: |
| right /= right_norm |
| view_up = np.cross(right, forward) |
| view_up /= max(float(np.linalg.norm(view_up)), 1e-8) |
|
|
| relative = np.asarray(vertices, dtype=np.float64) - center |
| projected_x = np.abs(relative @ right) |
| projected_y = np.abs(relative @ view_up) |
| toward_camera = relative @ eye_direction |
| vertical_half_fov = math.radians(vertical_fov_degrees) * 0.5 |
| horizontal_half_fov = math.atan( |
| math.tan(vertical_half_fov) * aspect_ratio |
| ) |
| required_x = toward_camera + projected_x / max( |
| math.tan(horizontal_half_fov), |
| 1e-6, |
| ) |
| required_y = toward_camera + projected_y / max( |
| math.tan(vertical_half_fov), |
| 1e-6, |
| ) |
| fitted = max(float(np.max(required_x)), float(np.max(required_y)), 1e-3) |
| return fitted * distance_scale |
|
|
|
|
| def _fixed_camera_distance_for_views( |
| vertices: np.ndarray, |
| center: np.ndarray, |
| *, |
| yaw_degrees: Sequence[float], |
| elevation_degrees: float, |
| vertical_fov_degrees: float, |
| aspect_ratio: float, |
| distance_scale: float, |
| ) -> tuple[float, dict[str, Any]]: |
| """Fit every requested view once and use the largest distance for all frames.""" |
|
|
| unique_yaws = tuple(dict.fromkeys(float(yaw) for yaw in yaw_degrees)) |
| if not unique_yaws: |
| raise ValueError("fixed camera fitting requires at least one yaw") |
| if any(not math.isfinite(yaw) for yaw in unique_yaws): |
| raise ValueError("fixed camera fitting requires finite yaws") |
| required_distances = [ |
| _fitted_camera_distance_for_view( |
| vertices, |
| center, |
| yaw_degrees=yaw, |
| elevation_degrees=elevation_degrees, |
| vertical_fov_degrees=vertical_fov_degrees, |
| aspect_ratio=aspect_ratio, |
| distance_scale=distance_scale, |
| ) |
| for yaw in unique_yaws |
| ] |
| fixed_distance = max(required_distances) |
| return fixed_distance, { |
| "distance_policy": "fixed_max_over_orbit_and_multiview", |
| "fixed_distance": float(fixed_distance), |
| "fit_yaw_count": len(unique_yaws), |
| "fit_yaw_min": float(min(unique_yaws)), |
| "fit_yaw_max": float(max(unique_yaws)), |
| "per_view_required_distance_min": float(min(required_distances)), |
| "per_view_required_distance_max": float(max(required_distances)), |
| } |
|
|
|
|
| def _summarize_vertex_colors( |
| vertex_colors: np.ndarray, |
| vertex_count: int, |
| *, |
| required: bool, |
| ) -> dict[str, Any]: |
| """Return auditable texture evidence without inferring visual quality.""" |
|
|
| colors = np.asarray(vertex_colors, dtype=np.float64) |
| complete = ( |
| vertex_count > 0 |
| and colors.ndim == 2 |
| and colors.shape == (vertex_count, 3) |
| ) |
| finite = bool(complete and np.isfinite(colors).all()) |
| summary: dict[str, Any] = { |
| "required": bool(required), |
| "present": bool(complete), |
| "finite": finite, |
| "colored_vertices": int(colors.shape[0]) if colors.ndim == 2 else 0, |
| "coverage_fraction": 1.0 if complete else 0.0, |
| } |
| if finite: |
| summary.update( |
| { |
| "channel_mean": [float(value) for value in np.mean(colors, axis=0)], |
| "channel_std": [float(value) for value in np.std(colors, axis=0)], |
| "channel_min": [float(value) for value in np.min(colors, axis=0)], |
| "channel_max": [float(value) for value in np.max(colors, axis=0)], |
| } |
| ) |
| return summary |
|
|
|
|
| def _referenced_vertex_indices( |
| triangles: np.ndarray, |
| vertex_count: int, |
| ) -> np.ndarray: |
| """Return the vertices that can contribute pixels to a triangle render. |
| |
| Some reconstruction meshes retain point-cloud vertices that are not |
| referenced by any triangle. Those vertices are invisible, and a single |
| far-away orphan must not expand the camera bounds and shrink the actual |
| surface to a few pixels. |
| """ |
|
|
| faces = np.asarray(triangles, dtype=np.int64) |
| if faces.ndim != 2 or faces.shape[1] != 3 or faces.size == 0: |
| raise ValueError("camera framing requires at least one triangular face") |
| referenced = np.unique(faces.reshape(-1)) |
| if int(referenced[0]) < 0 or int(referenced[-1]) >= vertex_count: |
| raise ValueError("triangle index is outside the mesh vertex array") |
| return referenced |
|
|
|
|
| def _camera_framing_vertex_indices( |
| triangles: np.ndarray, |
| triangle_component_labels: np.ndarray, |
| *, |
| dominant_component_min_face_fraction: float = DOMINANT_COMPONENT_MIN_FACE_FRACTION, |
| ) -> tuple[np.ndarray, dict[str, Any]]: |
| """Choose a review framing surface without letting tiny islands zoom out. |
| |
| A strongly dominant connected triangle surface represents the primary |
| accessibility target in these reconstructions. Small detached islands can |
| still be rendered, but they should not make the target unreadably small. |
| When no component owns at least 90% of faces, all triangle vertices remain |
| in the camera fit so legitimate multi-part geometry is not guessed away. |
| """ |
|
|
| faces = np.asarray(triangles, dtype=np.int64) |
| labels = np.asarray(triangle_component_labels, dtype=np.int64).reshape(-1) |
| referenced = _referenced_vertex_indices(faces, int(faces.max()) + 1) |
| if labels.shape[0] != faces.shape[0] or np.any(labels < 0): |
| raise ValueError("triangle component labels must match triangular faces") |
| if not (0.0 < dominant_component_min_face_fraction <= 1.0): |
| raise ValueError("dominant component face fraction must be in (0, 1]") |
|
|
| counts = np.bincount(labels) |
| dominant_label = int(np.argmax(counts)) |
| dominant_faces = int(counts[dominant_label]) |
| face_count = int(faces.shape[0]) |
| dominant_fraction = dominant_faces / face_count |
| use_dominant = dominant_fraction >= dominant_component_min_face_fraction |
| if use_dominant: |
| framing = np.unique(faces[labels == dominant_label].reshape(-1)) |
| scope = "dominant_connected_surface" |
| else: |
| framing = referenced |
| scope = "all_connected_surfaces" |
| return framing, { |
| "scope": scope, |
| "framing_faces": dominant_faces if use_dominant else face_count, |
| "framing_vertices": int(len(framing)), |
| "dominant_component_faces": dominant_faces, |
| "dominant_component_face_fraction": float(dominant_fraction), |
| "component_count": int(len(counts)), |
| "minimum_dominant_face_fraction": float( |
| dominant_component_min_face_fraction |
| ), |
| } |
|
|
|
|
| def _contact_sheet(images: Sequence[np.ndarray], labels: Sequence[str], path: Path) -> None: |
| columns = 2 if len(images) > 1 else 1 |
| rows = math.ceil(len(images) / columns) |
| panel_width = 480 |
| panel_height = 390 |
| label_height = 34 |
| canvas = Image.new( |
| "RGB", |
| (panel_width * columns, panel_height * rows), |
| (235, 235, 232), |
| ) |
| for index, (array, label) in enumerate(zip(images, labels)): |
| image = Image.fromarray(array).convert("RGB") |
| body = ImageOps.contain(image, (panel_width, panel_height - label_height)) |
| panel = Image.new("RGB", (panel_width, panel_height), "white") |
| draw = ImageDraw.Draw(panel) |
| draw.rectangle((0, 0, panel_width, label_height), fill=(242, 242, 239)) |
| draw.text((12, 11), label, fill=(20, 20, 20)) |
| panel.paste( |
| body, |
| ( |
| (panel_width - body.width) // 2, |
| label_height + (panel_height - label_height - body.height) // 2, |
| ), |
| ) |
| canvas.paste(panel, ((index % columns) * panel_width, (index // columns) * panel_height)) |
| canvas.save(path, quality=94, subsampling=0) |
|
|
|
|
| def render_outputs( |
| mesh_path: Path, |
| output_dir: Path, |
| *, |
| width: int, |
| height: int, |
| frames: int, |
| fps: int, |
| fov: float, |
| elevation: float, |
| distance_scale: float, |
| shader: str, |
| background: Sequence[float], |
| smooth_iterations: int, |
| multiview_yaws: Sequence[float], |
| orbit_mode: str = "front_arc", |
| orbit_span: float = 140.0, |
| require_vertex_colors: bool = False, |
| vertex_colors_srgb: bool = True, |
| cpu_fallback: bool = False, |
| ) -> dict[str, Any]: |
| """Render all outputs through Open3D's OffscreenRenderer path.""" |
| import open3d as o3d |
| from open3d.visualization import rendering |
|
|
| mesh = o3d.io.read_triangle_mesh(str(mesh_path)) |
| if len(mesh.vertices) == 0 or len(mesh.triangles) == 0: |
| raise ValueError(f"Input is not a non-empty triangle mesh: {mesh_path}") |
|
|
| source_vertex_count = len(mesh.vertices) |
| referenced_indices = _referenced_vertex_indices( |
| np.asarray(mesh.triangles, dtype=np.int64), |
| source_vertex_count, |
| ) |
| unreferenced_vertex_count = source_vertex_count - len(referenced_indices) |
| |
| |
| |
| if unreferenced_vertex_count: |
| mesh.remove_unreferenced_vertices() |
|
|
| |
| |
| vertices = np.asarray(mesh.vertices, dtype=np.float64) |
| mesh.vertices = o3d.utility.Vector3dVector( |
| np.stack([vertices[:, 0], vertices[:, 2], -vertices[:, 1]], axis=1) |
| ) |
| if smooth_iterations: |
| mesh = mesh.filter_smooth_laplacian(number_of_iterations=smooth_iterations) |
| mesh.compute_vertex_normals() |
| vertex_color_summary = _summarize_vertex_colors( |
| np.asarray(mesh.vertex_colors, dtype=np.float64), |
| len(mesh.vertices), |
| required=require_vertex_colors, |
| ) |
| if require_vertex_colors and not ( |
| vertex_color_summary["present"] and vertex_color_summary["finite"] |
| ): |
| raise ValueError( |
| "Input mesh does not provide complete finite vertex colors required " |
| f"for texture-preserving rendering: {mesh_path}" |
| ) |
|
|
| transformed_vertices = np.asarray(mesh.vertices, dtype=np.float64) |
| triangle_component_labels, _, _ = mesh.cluster_connected_triangles() |
| framing_indices, framing_summary = _camera_framing_vertex_indices( |
| np.asarray(mesh.triangles, dtype=np.int64), |
| np.asarray(triangle_component_labels, dtype=np.int64), |
| ) |
| framing_vertices = transformed_vertices[framing_indices] |
| framing_min = np.min(framing_vertices, axis=0) |
| framing_max = np.max(framing_vertices, axis=0) |
| center = (framing_min + framing_max) * 0.5 |
| orbit_yaws = _orbit_yaws( |
| frames, |
| mode=orbit_mode, |
| span_degrees=orbit_span, |
| ) |
| fixed_camera_distance, camera_summary = _fixed_camera_distance_for_views( |
| framing_vertices, |
| center, |
| yaw_degrees=tuple(orbit_yaws) + tuple(multiview_yaws), |
| elevation_degrees=elevation, |
| vertical_fov_degrees=fov, |
| aspect_ratio=width / height, |
| distance_scale=distance_scale, |
| ) |
|
|
| output_dir.mkdir(parents=True, exist_ok=True) |
| renderer = rendering.OffscreenRenderer(width, height) |
| try: |
| opengl_context = query_current_opengl_context() |
| process_graphics_backend = inspect_process_graphics_backend() |
| hardware_acceleration = enforce_opengl_context_policy( |
| opengl_context, |
| cpu_fallback=cpu_fallback, |
| process_backend=process_graphics_backend, |
| ) |
| renderer.scene.set_background(list(background)) |
| renderer.scene.scene.set_sun_light( |
| [0.35, -0.5, -0.75], |
| [1.0, 0.97, 0.92], |
| 65000, |
| ) |
| renderer.scene.scene.enable_sun_light(True) |
| renderer.scene.show_axes(False) |
| material = rendering.MaterialRecord() |
| material.shader = shader |
| material.sRGB_color = bool(vertex_colors_srgb) |
| material.base_color = [1.0, 1.0, 1.0, 1.0] |
| material.base_roughness = 0.68 |
| material.base_reflectance = 0.18 |
| renderer.scene.add_geometry("accessibility_geometry", mesh, material) |
|
|
| def render_yaw(yaw: float) -> np.ndarray: |
| eye, look_at, up = _camera_pose( |
| center, |
| fixed_camera_distance, |
| yaw, |
| elevation, |
| ) |
| renderer.setup_camera(fov, look_at, eye, up) |
| array = np.asarray(renderer.render_to_image()) |
| if array.ndim != 3 or array.shape[2] not in (3, 4): |
| raise RuntimeError(f"Unexpected Open3D render shape: {array.shape}") |
| return array[..., :3].astype(np.uint8, copy=False) |
|
|
| video_frames = [render_yaw(yaw) for yaw in orbit_yaws] |
| imageio.mimsave( |
| output_dir / OUTPUT_NAMES["video"], |
| video_frames, |
| fps=fps, |
| quality=8, |
| macro_block_size=1, |
| ) |
| imageio.mimsave( |
| output_dir / OUTPUT_NAMES["gif"], |
| video_frames, |
| duration=1000.0 / fps, |
| loop=0, |
| ) |
| Image.fromarray(video_frames[0]).save(output_dir / OUTPUT_NAMES["poster"]) |
|
|
| views = [render_yaw(yaw) for yaw in multiview_yaws] |
| labels = [f"Geometry view {yaw:g} deg" for yaw in multiview_yaws] |
| _contact_sheet(views, labels, output_dir / OUTPUT_NAMES["multiview"]) |
| finally: |
| release = getattr(renderer, "release_resources", None) |
| if callable(release): |
| release() |
|
|
| return { |
| "vertices": source_vertex_count, |
| "renderable_vertices": len(mesh.vertices), |
| "unreferenced_vertices_excluded_from_camera_fit": unreferenced_vertex_count, |
| "faces": len(mesh.triangles), |
| "camera_framing": framing_summary, |
| "camera": camera_summary, |
| "vertex_colors": vertex_color_summary, |
| "opengl_context": opengl_context, |
| "process_graphics_backend": process_graphics_backend, |
| "hardware_acceleration": hardware_acceleration, |
| "orbit": { |
| "mode": orbit_mode, |
| "span_degrees": float(orbit_span), |
| "yaw_min": float(min(orbit_yaws)), |
| "yaw_max": float(max(orbit_yaws)), |
| "multiview_yaws": [float(yaw) for yaw in multiview_yaws], |
| "open_world_scene_review": orbit_mode == "front_arc", |
| }, |
| } |
|
|
|
|
| def write_json(path: Path, payload: Mapping[str, Any]) -> None: |
| path.write_text( |
| json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", |
| encoding="utf-8", |
| ) |
|
|
|
|
| def main(argv: Sequence[str] | None = None) -> int: |
| parser = build_argument_parser() |
| args = parser.parse_args(argv) |
| validate_arguments(args) |
|
|
| mesh_path = Path(args.mesh).expanduser().resolve() |
| if not mesh_path.is_file(): |
| raise FileNotFoundError(mesh_path) |
| output_dir = Path(args.output_dir).expanduser().resolve() |
| output_dir.mkdir(parents=True, exist_ok=True) |
| sample_id = str(args.sample_id or mesh_path.stem) |
| category = str(args.category).strip().lower() or "unknown" |
|
|
| environment_evidence = configure_render_environment(args.cpu_fallback) |
| render_settings = { |
| "resolution": [args.width, args.height], |
| "frames": args.frames, |
| "fps": args.fps, |
| "fov_degrees": args.fov, |
| "elevation_degrees": args.elevation, |
| "distance_scale": args.distance_scale, |
| "camera_fit": "fixed_max_projected_primary_surface_v4", |
| "camera_distance_policy": "fixed_max_over_orbit_and_multiview", |
| "shader": args.shader, |
| "background": list(args.background), |
| "smooth_iterations": args.smooth_iterations, |
| "require_vertex_colors": args.require_vertex_colors, |
| "vertex_colors_srgb": args.vertex_colors_srgb, |
| "multiview_yaws_degrees": list(args.multiview_yaws), |
| "orbit_mode": args.orbit_mode, |
| "orbit_span_degrees": args.orbit_span, |
| "scene_interpretation": ( |
| "open_world_accessibility_surface_review" |
| if args.orbit_mode == "front_arc" |
| else "full_object_turntable" |
| ), |
| } |
| result = render_outputs( |
| mesh_path, |
| output_dir, |
| width=args.width, |
| height=args.height, |
| frames=args.frames, |
| fps=args.fps, |
| fov=args.fov, |
| elevation=args.elevation, |
| distance_scale=args.distance_scale, |
| shader=args.shader, |
| background=args.background, |
| smooth_iterations=args.smooth_iterations, |
| multiview_yaws=args.multiview_yaws, |
| orbit_mode=args.orbit_mode, |
| orbit_span=args.orbit_span, |
| require_vertex_colors=args.require_vertex_colors, |
| vertex_colors_srgb=args.vertex_colors_srgb, |
| cpu_fallback=args.cpu_fallback, |
| ) |
| manifest = build_render_manifest( |
| mesh_path=mesh_path, |
| output_dir=output_dir, |
| sample_id=sample_id, |
| category=category, |
| render_settings=render_settings, |
| render_result=result, |
| environment_evidence=environment_evidence, |
| cpu_fallback=args.cpu_fallback, |
| ) |
| write_json(output_dir / "manifest.json", manifest) |
| print( |
| json.dumps( |
| { |
| "status": "rendered", |
| "sample_id": sample_id, |
| "output_dir": str(output_dir), |
| "cpu_fallback_explicit": args.cpu_fallback, |
| "hardware_acceleration_status": manifest["renderer"][ |
| "hardware_acceleration_status" |
| ], |
| }, |
| ensure_ascii=False, |
| sort_keys=True, |
| ) |
| ) |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|