Spaces:
Running on Zero
Running on Zero
Download app.py from RenderFormer/renderformer-v2-demo: direct link, hf CLI and curl.
- Browser
- Download file 27.1 kB
-
https://huggingface.co/spaces/RenderFormer/renderformer-v2-demo/resolve/main/app.py
- Command line
-
hf download hf://spaces/RenderFormer/renderformer-v2-demo/app.py
-
curl -L -o app.py https://huggingface.co/spaces/RenderFormer/renderformer-v2-demo/resolve/main/app.py
27.1 kB
| """RenderFormer-V2 — neural rendering of triangle-mesh scenes, on ZeroGPU. | |
| RenderFormer replaces a path tracer with a transformer: it takes a triangle | |
| soup plus per-triangle material/emission tokens and a camera, and directly | |
| predicts the HDR image — global illumination, soft shadows, reflections and | |
| refraction included, with no ray tracing at inference time. | |
| """ | |
| import os | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") | |
| # No sm_120 flash-attn wheel matches the runtime torch ABI; SDPA is the | |
| # repository's own supported fallback and is selected explicitly so the import | |
| # never has to fail first. | |
| os.environ.setdefault("ATTN_IMPL", "sdpa") | |
| import spaces # noqa: E402 (must precede any CUDA-touching import) | |
| import json # noqa: E402 | |
| import shutil # noqa: E402 | |
| import tempfile # noqa: E402 | |
| import time # noqa: E402 | |
| import traceback # noqa: E402 | |
| from pathlib import Path # noqa: E402 | |
| import gradio as gr # noqa: E402 | |
| import numpy as np # noqa: E402 | |
| import torch # noqa: E402 | |
| from renderformer.data import SceneData # noqa: E402 | |
| import simple_exr # noqa: E402 (the repository's own EXR writer) | |
| from renderformer.pipelines import RenderFormerV2Pipeline # noqa: E402 | |
| import scene_prep # noqa: E402 | |
| MODEL_ID = "RenderFormer/renderformer-v2" | |
| RESOLUTION = 512 | |
| HERE = Path(__file__).parent | |
| WORK = Path(tempfile.gettempdir()) / "rf2-work" | |
| WORK.mkdir(parents=True, exist_ok=True) | |
| # --------------------------------------------------------------------------- # | |
| # model | |
| # --------------------------------------------------------------------------- # | |
| PIPE = RenderFormerV2Pipeline.from_pretrained( | |
| MODEL_ID, | |
| resolution=RESOLUTION, | |
| device="cpu", | |
| apply_fused_kernels=True, | |
| # Both roles are the same Qwen-Image VAE; matching dtypes makes the | |
| # component registry hand back one shared instance instead of two copies. | |
| environment_encoder_dtype=torch.float16, | |
| texture_encoder_dtype=torch.float16, | |
| ) | |
| PIPE._ensure_texture_vae() | |
| PIPE.to("cuda") | |
| print("[rf2] pipeline ready:", json.dumps(PIPE.component_info, default=str), flush=True) | |
| # --------------------------------------------------------------------------- # | |
| # bundled paper scenes | |
| # --------------------------------------------------------------------------- # | |
| SCENES = { | |
| "Transparent torus": { | |
| "dir": HERE / "scenes" / "transparent-torus", | |
| "hero": "Torus", | |
| "tone": "none", | |
| }, | |
| "Cube pile": { | |
| "dir": HERE / "scenes" / "cube-pile", | |
| "hero": "object1", | |
| "tone": "none", | |
| }, | |
| "Dragon": { | |
| "dir": HERE / "scenes" / "dragon", | |
| "hero": "dragon", | |
| "tone": "agx", | |
| }, | |
| } | |
| PREPARED = {} | |
| for _name, _spec in SCENES.items(): | |
| _prep, _cfg = scene_prep.load_prepared_frame( | |
| _spec["dir"], WORK / _spec["dir"].name | |
| ) | |
| PREPARED[_name] = (_prep, _cfg) | |
| print( | |
| f"[rf2] staged {_name}: " | |
| f"{sum(int(o.triangles.shape[0]) for o in _prep.objects)} triangles", | |
| flush=True, | |
| ) | |
| STUDIO_TEMPLATE = SCENES["Transparent torus"]["dir"] | |
| # --------------------------------------------------------------------------- # | |
| # material / tone-mapping helpers | |
| # --------------------------------------------------------------------------- # | |
| MATERIAL_MODES = ["Scene default", "Opaque (metallic/roughness)", "Glass (transmissive)"] | |
| TONE_MAPPERS = ["Scene default", "none", "agx", "filmic", "pbr_neutral"] | |
| def _srgb_hex_to_linear(value: str) -> list: | |
| text = (value or "").strip() | |
| if text.startswith("rgba") or text.startswith("rgb"): | |
| parts = text[text.find("(") + 1 : text.find(")")].split(",") | |
| srgb = [float(p) / 255.0 for p in parts[:3]] | |
| else: | |
| text = text.lstrip("#") | |
| if len(text) == 3: | |
| text = "".join(c * 2 for c in text) | |
| if len(text) != 6: | |
| raise gr.Error(f"could not parse colour {value!r}") | |
| srgb = [int(text[i : i + 2], 16) / 255.0 for i in (0, 2, 4)] | |
| return [ | |
| c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 for c in srgb | |
| ] | |
| def _material_override(mode, colour, metallic, roughness, ior): | |
| """Build a MaterialConfig attribute patch for the hero object.""" | |
| if mode == MATERIAL_MODES[0]: | |
| return None | |
| base_color = _srgb_hex_to_linear(colour) | |
| patch = { | |
| "texture_map_path": None, | |
| "measured_brdf_npy_path": None, | |
| "diffuse_specular_params": None, | |
| "metallic_roughness_params": None, | |
| "metallic_roughness_transmission_params": None, | |
| } | |
| if mode == MATERIAL_MODES[1]: | |
| patch["material_type"] = "homo_metallic_roughness" | |
| patch["metallic_roughness_params"] = { | |
| "base_color": base_color, | |
| "metallic": float(metallic), | |
| "roughness": float(roughness), | |
| } | |
| else: | |
| patch["material_type"] = "homo_metallic_roughness_transmission" | |
| patch["metallic_roughness_transmission_params"] = { | |
| "base_color": base_color, | |
| "metallic": float(metallic), | |
| "roughness": float(roughness), | |
| "transmission_weight": 1.0, | |
| "ior": float(ior), | |
| } | |
| return patch | |
| def _tone_map(hdr: np.ndarray, name: str) -> np.ndarray: | |
| if name == "none": | |
| return np.clip(hdr, 0.0, 1.0) | |
| from simple_ocio import ToneMapper | |
| ocio_name = "Khronos PBR Neutral" if name == "pbr_neutral" else name | |
| return ToneMapper(ocio_name).hdr_to_ldr(hdr) | |
| # --------------------------------------------------------------------------- # | |
| # GPU worker | |
| # --------------------------------------------------------------------------- # | |
| def _prime_dynamo_handlers(): | |
| """Make ``torch.compile`` usable inside a ZeroGPU worker. | |
| RenderFormer-V2's triangle transformer is built on ``flex_attention`` with a | |
| sliding-window + summary/register-sink block mask, and the repository wraps | |
| both ``flex_attention`` and ``create_block_mask`` in ``torch.compile`` — the | |
| eager fallback would materialise a dense NxN score matrix, which is not an | |
| option at 41k triangles. | |
| ZeroGPU's torch shim aliases ``torch.cuda._maybe_exchange_device`` onto | |
| ``torch.cuda._exchange_device``. Dynamo builds a ``function -> handler`` | |
| table keyed by those objects and refuses a duplicate key, so the very first | |
| compile dies with ``AssertionError: Handler already registered for | |
| <built-in function _cuda_exchangeDevice>``. The table is ``functools.cache``d, | |
| so building it once with the alias broken fixes every later compile. | |
| """ | |
| from torch._dynamo.variables.torch import TorchInGraphFunctionVariable | |
| build = TorchInGraphFunctionVariable._get_handlers | |
| info = getattr(build, "cache_info", None) | |
| if info is not None and info().currsize: | |
| return | |
| saved = torch.cuda._maybe_exchange_device | |
| if saved is torch.cuda._exchange_device: | |
| def _distinct_maybe_exchange_device(device): | |
| return saved(device) | |
| torch.cuda._maybe_exchange_device = _distinct_maybe_exchange_device | |
| try: | |
| build() | |
| except AssertionError as exc: # pragma: no cover - diagnostic only | |
| print(f"[rf2] dynamo handler priming failed: {exc}", flush=True) | |
| finally: | |
| torch.cuda._maybe_exchange_device = saved | |
| def _tame_inductor(): | |
| """Stop Inductor from autotuning kernels by timing them with CUDA events. | |
| Inside a ZeroGPU worker the autotuner's timing loop raises | |
| ``RuntimeError: Both events must be completed before calculating elapsed | |
| time``, which aborts the compile. Picking the heuristic config instead of a | |
| benchmarked one costs a little kernel throughput and saves the compile, so | |
| turn the benchmarking off and keep a defensive fallback for any autotuner | |
| that still slips through. | |
| """ | |
| from torch._inductor import config as inductor_config | |
| inductor_config.max_autotune = False | |
| inductor_config.max_autotune_pointwise = False | |
| inductor_config.max_autotune_gemm = False | |
| inductor_config.coordinate_descent_tuning = False | |
| inductor_config.benchmark_kernel = False | |
| inductor_config.triton.autotune_pointwise = False | |
| inductor_config.triton.autotune_cublasLt = False | |
| from torch._inductor.runtime import benchmarking | |
| cls = type(benchmarking.benchmarker) | |
| original = cls.benchmark_gpu | |
| if getattr(original, "_rf2_patched", False): | |
| return | |
| def benchmark_gpu(self, _callable, *args, **kwargs): | |
| try: | |
| return original(self, _callable, *args, **kwargs) | |
| except RuntimeError as exc: | |
| print(f"[rf2] inductor autotune timing unavailable: {exc}", flush=True) | |
| _callable() | |
| torch.cuda.synchronize() | |
| return 0.0 | |
| benchmark_gpu._rf2_patched = True | |
| cls.benchmark_gpu = benchmark_gpu | |
| def _install_compiled_flex(): | |
| """Force flex attention through Inductor instead of its dense fallback. | |
| ``torch.nn.attention.flex_attention`` only emits the fused sparse kernel when | |
| it is *inlined into* a compiled region — ``torch.compiler.is_dynamo_compiling()`` | |
| has to be true inside its own frame. The repository rebinds | |
| ``flex_attention = torch.compile(flex_attention)``, and in the ZeroGPU worker | |
| that decoration graph-breaks before the check, so every call lands in | |
| ``sdpa_dense`` and materialises a dense ``(B, H, N, N)`` score matrix. That is | |
| survivable at 4k triangles and about 54 GB at 41k, which is what killed the | |
| dragon scene with an allocator assert. | |
| Compiling a thin wrapper *around* the call instead keeps ``flex_attention`` | |
| inside the traced graph, so the block-sparse Triton kernel is actually used. | |
| """ | |
| from renderformer.models import attention as rf_attention | |
| if getattr(rf_attention, "_rf2_flex_shim", False): | |
| return | |
| import torch._dynamo as dynamo | |
| import torch.nn.attention.flex_attention as flex_module | |
| from torch.nn.attention.flex_attention import flex_attention as eager_flex | |
| dynamo.config.recompile_limit = max(getattr(dynamo.config, "recompile_limit", 8), 32) | |
| # ``flex_attention`` opens with a block of pure input validation, and Dynamo | |
| # cannot trace ``query.device != key.device`` there ("SourcelessBuilder.create | |
| # does not know how to wrap <class 'torch.device'>"). That graph break is what | |
| # drops the call out of the compiled region in the first place. The checks | |
| # guard against mismatched devices/dtypes/shapes that the pipeline has already | |
| # ruled out, so retire them for the duration of the render. | |
| # The same trace stops again on ``query.device != block_mask.kv_num_blocks.device``, | |
| # which is inline in ``flex_attention`` and cannot be patched out. Teach the | |
| # sourceless builder to wrap ``torch.device`` instead (newer PyTorch does this | |
| # itself). Best effort: if it does not take, the shim below still falls back. | |
| try: | |
| from torch._dynamo.variables import constant as dynamo_constant | |
| from torch._dynamo.variables.builder import SourcelessBuilder | |
| handlers = SourcelessBuilder._type_handlers | |
| if torch.device not in handlers: | |
| dynamo_constant.common_constant_types.add(torch.device) | |
| handlers[torch.device] = ( | |
| lambda tx, value: dynamo_constant.ConstantVariable.create(value) | |
| ) | |
| except Exception as exc: # pragma: no cover - diagnostic only | |
| print(f"[rf2] could not teach dynamo about torch.device: {exc}", flush=True) | |
| def _skip_validation(*_args, **_kwargs): | |
| return None | |
| for name in ( | |
| "_validate_sdpa_input", | |
| "_validate_device", | |
| "_validate_embed_dim", | |
| "_validate_no_nested_tensors", | |
| "_validate_block_mask_shape", | |
| ): | |
| if hasattr(flex_module, name): | |
| setattr(flex_module, name, _skip_validation) | |
| def compiled_flex(query, key, value, block_mask): | |
| return eager_flex(query, key, value, block_mask=block_mask) | |
| def flex(query, key, value, block_mask=None, **kwargs): | |
| if block_mask is not None and not kwargs: | |
| try: | |
| return compiled_flex(query, key, value, block_mask) | |
| except Exception as exc: # pragma: no cover - diagnostic only | |
| print(f"[rf2] compiled flex attention unavailable: {exc}", flush=True) | |
| return eager_flex(query, key, value, block_mask=block_mask, **kwargs) | |
| rf_attention.flex_attention = flex | |
| rf_attention._rf2_flex_shim = True | |
| def _gpu_duration(*_args, **_kwargs): | |
| """Measured on this Space: 1.6-6 s warm, ~86 s on the first render after a restart. | |
| Inductor caches the flex-attention kernels to disk, so only the first visitor | |
| after a reboot pays the compile; the reservation has to cover it anyway. | |
| """ | |
| return 120 | |
| def _gpu_render(inputs: "scene_prep.SceneInputs", c2w: np.ndarray, fov: np.ndarray): | |
| """Run the RenderFormer-V2 transformer on one prepared scene + one camera.""" | |
| _prime_dynamo_handlers() | |
| _tame_inductor() | |
| _install_compiled_flex() | |
| started = time.perf_counter() | |
| raw = torch.from_numpy(inputs.raw_texture_unique) | |
| encoded = PIPE.encode_texture(raw, dtype=torch.float16) # (O, 80, 4, 4) | |
| counts = torch.as_tensor( | |
| inputs.object_triangle_counts, device=encoded.device, dtype=torch.long | |
| ) | |
| texture = encoded.repeat_interleave(counts, dim=0) # (N, 80, 4, 4) | |
| scene = SceneData( | |
| path=Path(inputs.scene_name), | |
| triangles=torch.from_numpy(inputs.triangles), | |
| texture=texture, | |
| vn=torch.from_numpy(inputs.vn), | |
| c2w=torch.from_numpy(c2w), | |
| fov=torch.from_numpy(fov), | |
| mvp=torch.from_numpy(np.repeat(np.eye(4, dtype=np.float32)[None], 1, axis=0)), | |
| light_strength=torch.from_numpy(inputs.light_strength), | |
| input_metadata={ | |
| "texture": "preencoded", | |
| "light_strength": "h5", | |
| "environment": "missing", | |
| }, | |
| ) | |
| out = PIPE.render_scene(scene, resolution=RESOLUTION, precision="fp16") | |
| hdr = out.hdr[0, 0].float().cpu().numpy() | |
| torch.cuda.synchronize() | |
| return np.ascontiguousarray(hdr), time.perf_counter() - started | |
| # --------------------------------------------------------------------------- # | |
| # shared render path (CPU prep + GPU call + tone mapping) | |
| # --------------------------------------------------------------------------- # | |
| def _render(prepared, camera, overrides, yaw, pitch, distance, tone_name, label): | |
| inputs = scene_prep.build_scene_inputs(prepared, material_overrides=overrides) | |
| position, target, up = scene_prep.orbit_camera( | |
| camera, float(yaw), float(pitch), float(distance) | |
| ) | |
| c2w, fov, _ = scene_prep.camera_matrices(position, target, up, camera.fov) | |
| hdr, seconds = _gpu_render(inputs, c2w, fov) | |
| hdr = np.nan_to_num(np.asarray(hdr, dtype=np.float32), nan=0.0, posinf=0.0, neginf=0.0) | |
| hdr = np.clip(hdr, 0.0, 1e4) | |
| ldr = (np.clip(_tone_map(hdr, tone_name), 0.0, 1.0) * 255.0).astype(np.uint8) | |
| exr_dir = Path(tempfile.mkdtemp(prefix="rf2-out-")) | |
| exr_path = exr_dir / f"{label}.exr" | |
| # The OpenCV wheels ship without the OpenEXR codec, so use the writer the | |
| # RenderFormer data pipeline itself uses. | |
| simple_exr.write_exr(str(exr_path), hdr) | |
| info = ( | |
| f"**{inputs.num_triangles:,} triangles** · {RESOLUTION}x{RESOLUTION} · " | |
| f"tone mapper `{tone_name}` · **{seconds:.2f} s** on the GPU" | |
| ) | |
| return ldr, info, str(exr_path) | |
| # --------------------------------------------------------------------------- # | |
| # handlers | |
| # --------------------------------------------------------------------------- # | |
| def render_paper_scene( | |
| scene_name: str, | |
| yaw: float = 0.0, | |
| pitch: float = 0.0, | |
| distance: float = 1.0, | |
| tone_mapper: str = "Scene default", | |
| material_mode: str = "Scene default", | |
| base_colour: str = "#c75a3c", | |
| metallic: float = 0.0, | |
| roughness: float = 0.3, | |
| ior: float = 1.33, | |
| ): | |
| """Render one of the bundled RenderFormer-V2 paper scenes. | |
| Args: | |
| scene_name: Which bundled scene to render. | |
| yaw: Orbit the camera left/right around the scene, in degrees. | |
| pitch: Orbit the camera up/down, in degrees. | |
| distance: Multiplier on the camera's distance to its look-at point. | |
| tone_mapper: HDR-to-LDR transform; "Scene default" uses the authored one. | |
| material_mode: Optionally replace the hero object's material. | |
| base_colour: Base colour of the replacement material (sRGB hex). | |
| metallic: Metallic parameter of the replacement material. | |
| roughness: Roughness parameter of the replacement material. | |
| ior: Index of refraction, used by the glass material only. | |
| Returns: | |
| The tone-mapped render, a short info line, and the linear HDR .exr file. | |
| """ | |
| if scene_name not in SCENES: | |
| raise gr.Error(f"unknown scene {scene_name!r}") | |
| spec = SCENES[scene_name] | |
| prepared, config = PREPARED[scene_name] | |
| tone = spec["tone"] if tone_mapper == "Scene default" else tone_mapper | |
| patch = _material_override(material_mode, base_colour, metallic, roughness, ior) | |
| overrides = {spec["hero"]: patch} if patch else {} | |
| try: | |
| return _render( | |
| prepared, | |
| config.cameras[0], | |
| overrides, | |
| yaw, | |
| pitch, | |
| distance, | |
| tone, | |
| spec["dir"].name, | |
| ) | |
| except gr.Error: | |
| raise | |
| except Exception as exc: # surface a readable message in the UI | |
| traceback.print_exc() | |
| raise gr.Error(f"render failed: {type(exc).__name__}: {exc}") from exc | |
| def render_uploaded_mesh( | |
| mesh_file, | |
| yaw: float = 0.0, | |
| pitch: float = 0.0, | |
| distance: float = 1.0, | |
| tone_mapper: str = "none", | |
| material_mode: str = "Opaque (metallic/roughness)", | |
| base_colour: str = "#c75a3c", | |
| metallic: float = 0.0, | |
| roughness: float = 0.3, | |
| ior: float = 1.33, | |
| ): | |
| """Drop a triangle mesh into a bundled studio set and render it. | |
| The studio (floor, two coloured walls, one area light) is lifted verbatim | |
| from the authors' "transparent torus" scene, so the lighting matches a | |
| configuration the model was demonstrated on. Meshes above ~14k triangles | |
| are quadric-decimated, then centred, scaled and seated on the floor. | |
| Args: | |
| mesh_file: Path to an .obj / .ply / .glb / .stl triangle mesh. | |
| yaw: Orbit the camera left/right around the studio, in degrees. | |
| pitch: Orbit the camera up/down, in degrees. | |
| distance: Multiplier on the camera's distance to its look-at point. | |
| tone_mapper: HDR-to-LDR transform. | |
| material_mode: Material assigned to the uploaded mesh. | |
| base_colour: Base colour of that material (sRGB hex). | |
| metallic: Metallic parameter of that material. | |
| roughness: Roughness parameter of that material. | |
| ior: Index of refraction, used by the glass material only. | |
| Returns: | |
| The tone-mapped render, a short info line, and the linear HDR .exr file. | |
| """ | |
| if not mesh_file: | |
| raise gr.Error("upload a mesh first") | |
| path = mesh_file if isinstance(mesh_file, str) else getattr(mesh_file, "name", None) | |
| if not path or not Path(path).is_file(): | |
| raise gr.Error("could not read the uploaded mesh") | |
| mode = material_mode if material_mode != "Scene default" else MATERIAL_MODES[1] | |
| patch = _material_override(mode, base_colour, metallic, roughness, ior) | |
| tone = "none" if tone_mapper == "Scene default" else tone_mapper | |
| out_dir = Path(tempfile.mkdtemp(prefix="rf2-studio-", dir=WORK)) | |
| try: | |
| scene_prep.build_studio_frame(path, STUDIO_TEMPLATE, out_dir) | |
| prepared, config = scene_prep.load_prepared_frame(out_dir, out_dir) | |
| return _render( | |
| prepared, | |
| config.cameras[0], | |
| {"hero": patch} if patch else {}, | |
| yaw, | |
| pitch, | |
| distance, | |
| tone, | |
| "studio", | |
| ) | |
| except gr.Error: | |
| raise | |
| except Exception as exc: | |
| traceback.print_exc() | |
| raise gr.Error(f"render failed: {type(exc).__name__}: {exc}") from exc | |
| finally: | |
| shutil.rmtree(out_dir, ignore_errors=True) | |
| # --------------------------------------------------------------------------- # | |
| # UI | |
| # --------------------------------------------------------------------------- # | |
| CSS = """ | |
| #col-container { max-width: 1180px; margin: 0 auto; } | |
| .dark .gradio-container { color: var(--body-text-color); } | |
| """ | |
| HEADER = """ | |
| # RenderFormer V2 — neural rendering with heterogeneous scene primitives | |
| A transformer that **replaces the path tracer**: give it a triangle soup with | |
| per-triangle material and emission tokens plus a camera, and it directly | |
| predicts the HDR image — global illumination, soft shadows, glossy reflections | |
| and refraction included, with no ray tracing and no per-scene training. | |
| [paper](https://huggingface.co/papers/2609.05738) · | |
| [model](https://huggingface.co/RenderFormer/renderformer-v2) · | |
| [code](https://github.com/iamNCJ/RenderFormer-Studio) | |
| """ | |
| def _camera_controls(): | |
| yaw = gr.Slider(-180, 180, value=0, step=1, label="Orbit (yaw°)") | |
| pitch = gr.Slider(-60, 60, value=0, step=1, label="Elevation (pitch°)") | |
| distance = gr.Slider(0.5, 2.0, value=1.0, step=0.05, label="Distance ×") | |
| return yaw, pitch, distance | |
| def _material_controls(default_mode: str): | |
| mode = gr.Radio(MATERIAL_MODES, value=default_mode, label="Material") | |
| colour = gr.ColorPicker(value="#c75a3c", label="Base colour") | |
| metallic = gr.Slider(0.0, 1.0, value=0.0, step=0.01, label="Metallic") | |
| roughness = gr.Slider(0.02, 1.0, value=0.3, step=0.01, label="Roughness") | |
| ior = gr.Slider(1.0, 2.5, value=1.33, step=0.01, label="Index of refraction (glass)") | |
| return mode, colour, metallic, roughness, ior | |
| with gr.Blocks(title="RenderFormer V2") as demo: | |
| with gr.Column(elem_id="col-container"): | |
| gr.Markdown(HEADER) | |
| with gr.Tabs(): | |
| # ---------------------------------------------------------- # | |
| with gr.Tab("Paper scenes"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| p_scene = gr.Dropdown( | |
| list(SCENES), value="Transparent torus", label="Scene" | |
| ) | |
| p_yaw, p_pitch, p_distance = _camera_controls() | |
| p_run = gr.Button("Render", variant="primary") | |
| with gr.Accordion("Material & tone mapping", open=False): | |
| p_tone = gr.Dropdown( | |
| TONE_MAPPERS, value="Scene default", label="Tone mapper" | |
| ) | |
| ( | |
| p_mode, | |
| p_colour, | |
| p_metallic, | |
| p_roughness, | |
| p_ior, | |
| ) = _material_controls(MATERIAL_MODES[0]) | |
| with gr.Column(scale=1): | |
| p_image = gr.Image(label="Render", type="numpy", height=512) | |
| p_info = gr.Markdown() | |
| p_exr = gr.File(label="Linear HDR (.exr)") | |
| p_inputs = [ | |
| p_scene, | |
| p_yaw, | |
| p_pitch, | |
| p_distance, | |
| p_tone, | |
| p_mode, | |
| p_colour, | |
| p_metallic, | |
| p_roughness, | |
| p_ior, | |
| ] | |
| p_outputs = [p_image, p_info, p_exr] | |
| p_run.click(render_paper_scene, p_inputs, p_outputs, api_name="render_scene") | |
| gr.Examples( | |
| examples=[ | |
| ["Transparent torus", 0, 0], | |
| ["Cube pile", 25, 10], | |
| ["Dragon", 0, 0], | |
| ], | |
| inputs=[p_scene, p_yaw, p_pitch], | |
| outputs=p_outputs, | |
| fn=render_paper_scene, | |
| cache_examples=True, | |
| cache_mode="lazy", | |
| ) | |
| # ---------------------------------------------------------- # | |
| with gr.Tab("Your own mesh"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| u_mesh = gr.Model3D(label="Triangle mesh (.obj / .glb / .ply)") | |
| u_yaw, u_pitch, u_distance = _camera_controls() | |
| u_run = gr.Button("Render", variant="primary") | |
| with gr.Accordion("Material & tone mapping", open=False): | |
| u_tone = gr.Dropdown( | |
| TONE_MAPPERS, value="none", label="Tone mapper" | |
| ) | |
| ( | |
| u_mode, | |
| u_colour, | |
| u_metallic, | |
| u_roughness, | |
| u_ior, | |
| ) = _material_controls(MATERIAL_MODES[1]) | |
| with gr.Column(scale=1): | |
| u_image = gr.Image(label="Render", type="numpy", height=512) | |
| u_info = gr.Markdown() | |
| u_exr = gr.File(label="Linear HDR (.exr)") | |
| u_inputs = [ | |
| u_mesh, | |
| u_yaw, | |
| u_pitch, | |
| u_distance, | |
| u_tone, | |
| u_mode, | |
| u_colour, | |
| u_metallic, | |
| u_roughness, | |
| u_ior, | |
| ] | |
| u_outputs = [u_image, u_info, u_exr] | |
| u_run.click(render_uploaded_mesh, u_inputs, u_outputs, api_name="render_mesh") | |
| gr.Examples( | |
| examples=[[str(HERE / "assets" / "constant-width.obj")]], | |
| inputs=[u_mesh], | |
| outputs=u_outputs, | |
| fn=render_uploaded_mesh, | |
| cache_examples=True, | |
| cache_mode="lazy", | |
| ) | |
| gr.Markdown( | |
| "Scenes are built entirely on CPU (trimesh + the checkpoint's BRDF " | |
| "mappers), so no Blender install is needed — the transformer is the " | |
| "only thing that touches the GPU.\n\n" | |
| "**Assets** — *Transparent torus* and *Cube pile* are the authors' own " | |
| "scenes (MIT). *Dragon* is “Dragon” by Delatronic, CC BY 3.0, " | |
| "via Benedikt Bitterli's Rendering Resources. The constant-width " | |
| "surface example is CC0 1.0 by Keenan Crane." | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True, show_error=True) | |