import os import platform import subprocess import tempfile import zipfile import blobfile as bf import numpy as np from PIL import Image from shap_e.rendering.mesh import TriMesh from .constants import BASIC_AMBIENT_COLOR, BASIC_DIFFUSE_COLOR, UNIFORM_LIGHT_DIRECTION SCRIPT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "blender_script.py") def render_model( model_path: str, output_path: str, num_images: int, backend: str = "BLENDER_EEVEE", light_mode: str = "random", camera_pose: str = "random", camera_dist_min: float = 2.0, camera_dist_max: float = 2.0, fast_mode: bool = False, extract_material: bool = False, delete_material: bool = False, verbose: bool = False, timeout: float = 15 * 60, ): with tempfile.TemporaryDirectory() as tmp_dir: tmp_in = model_path tmp_out = os.path.join(tmp_dir, "out") zip_out = tmp_out + ".zip" os.mkdir(tmp_out) args = [] if platform.system() == "Linux": # Needed to enable Eevee backend on headless linux. args = ["xvfb-run", "-a"] args.extend( [ _blender_binary_path(), "-b", "-P", SCRIPT_PATH, "--", "--input_path", tmp_in, "--output_path", tmp_out, "--num_images", str(num_images), "--backend", backend, "--light_mode", light_mode, "--camera_pose", camera_pose, "--camera_dist_min", str(camera_dist_min), "--camera_dist_max", str(camera_dist_max), "--uniform_light_direction", *[str(x) for x in UNIFORM_LIGHT_DIRECTION], "--basic_ambient", str(BASIC_AMBIENT_COLOR), "--basic_diffuse", str(BASIC_DIFFUSE_COLOR), ] ) if fast_mode: args.append("--fast_mode") if extract_material: args.append("--extract_material") if delete_material: args.append("--delete_material") if verbose: subprocess.check_call(args) else: try: output = subprocess.check_output(args, stderr=subprocess.STDOUT, timeout=timeout) except subprocess.CalledProcessError as exc: raise RuntimeError(f"{exc}: {exc.output}") from exc if not os.path.exists(os.path.join(tmp_out, "info.json")): if verbose: # There is no output available, since it was # logged directly to stdout/stderr. raise RuntimeError(f"render failed: output file missing") else: raise RuntimeError(f"render failed: output file missing. Output: {output}") _combine_rgba(tmp_out) with zipfile.ZipFile(zip_out, mode="w") as zf: for name in os.listdir(tmp_out): zf.write(os.path.join(tmp_out, name), name) bf.copy(zip_out, output_path, overwrite=True) def render_mesh( mesh: TriMesh, output_path: str, num_images: int, backend: str = "BLENDER_EEVEE", **kwargs, ): if mesh.has_vertex_colors() and backend not in ["BLENDER_EEVEE", "CYCLES"]: raise ValueError(f"backend does not support vertex colors: {backend}") with tempfile.TemporaryDirectory() as tmp_dir: ply_path = os.path.join(tmp_dir, "out.ply") with open(ply_path, "wb") as f: mesh.write_ply(f) render_model( ply_path, output_path=output_path, num_images=num_images, backend=backend, **kwargs ) def _combine_rgba(out_dir: str): i = 0 while True: paths = [os.path.join(out_dir, f"{i:05}_{ch}.png") for ch in "rgba"] if not os.path.exists(paths[0]): break joined = np.stack( [(np.array(Image.open(path)) >> 8).astype(np.uint8) for path in paths], axis=-1 ) Image.fromarray(joined).save(os.path.join(out_dir, f"{i:05}.png")) for path in paths: os.remove(path) i += 1 def _blender_binary_path() -> str: path = os.getenv("BLENDER_PATH", None) if path is not None: return path if os.path.exists("/Applications/Blender.app/Contents/MacOS/Blender"): return "/Applications/Blender.app/Contents/MacOS/Blender" raise EnvironmentError( "To render 3D models, install Blender version 3.3.1 or higher and " "set the environment variable `BLENDER_PATH` to the path of the Blender executable." )