Spaces:
Paused
Paused
| """ | |
| Blender script to render images of 3D models. | |
| This script is adopted from the Trellis rendering script: | |
| https://github.com/microsoft/TRELLIS/blob/main/dataset_toolkits/render.py | |
| """ | |
| import argparse | |
| import math | |
| import os | |
| import platform | |
| import random | |
| import sys | |
| from pathlib import Path | |
| from typing import Any, Callable, Dict, Generator, Literal, Optional, Tuple | |
| import bpy | |
| import numpy as np | |
| from mathutils import Vector | |
| pathdir = Path(__file__).parent | |
| sys.path.append(pathdir.as_posix()) | |
| print(dir(bpy), bpy.__path__) | |
| IMPORT_FUNCTIONS: Dict[str, Callable] = { | |
| ".obj": bpy.ops.wm.obj_import, | |
| ".glb": bpy.ops.import_scene.gltf, | |
| ".gltf": bpy.ops.import_scene.gltf, | |
| } | |
| def center_and_scale_mesh(scale_value: float = 1.0) -> None: | |
| """Centers and scales the scene to fit in a unit cube. | |
| For example, | |
| scale_value = 1.0 ==> [-0.5, 0.5] | |
| scale_value = 2.0 ==> [-1.0, 1.0] | |
| """ | |
| # Get all mesh objects | |
| mesh_objects = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"] | |
| if not mesh_objects: | |
| return | |
| # Calculate bounds | |
| min_coords = Vector((float("inf"),) * 3) | |
| max_coords = Vector((float("-inf"),) * 3) | |
| for obj in mesh_objects: | |
| # Get all vertices in world space | |
| for vertex in obj.data.vertices: | |
| world_coord = obj.matrix_world @ vertex.co | |
| min_coords.x = min(min_coords.x, world_coord.x) | |
| min_coords.y = min(min_coords.y, world_coord.y) | |
| min_coords.z = min(min_coords.z, world_coord.z) | |
| max_coords.x = max(max_coords.x, world_coord.x) | |
| max_coords.y = max(max_coords.y, world_coord.y) | |
| max_coords.z = max(max_coords.z, world_coord.z) | |
| # Calculate center and dimensions | |
| center = (min_coords + max_coords) / 2 | |
| dimensions = max_coords - min_coords | |
| scale = scale_value / max( | |
| dimensions.x, dimensions.y, dimensions.z | |
| ) # Scale to fit in [-scale_value/2, scale_value/2] cube | |
| # Create an empty to serve as the parent | |
| empty = bpy.data.objects.new("Parent_Empty", None) | |
| bpy.context.scene.collection.objects.link(empty) | |
| # Parent all mesh objects to the empty | |
| for obj in mesh_objects: | |
| obj.parent = empty | |
| # Move empty to center everything | |
| empty.location = -center | |
| # Apply scale to empty | |
| empty.scale = (scale, scale, scale) | |
| bpy.context.view_layer.update() | |
| bpy.ops.object.select_all(action="DESELECT") | |
| empty.select_set(True) | |
| bpy.context.view_layer.objects.active = empty | |
| bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) | |
| print(f"Empty location: {empty.location}") | |
| print(f"Empty scale: {empty.scale}") | |
| return scale | |
| def normalize_scene() -> None: | |
| """Normalizes the scene by scaling and translating it to fit in a unit cube centered | |
| at the origin. | |
| Mostly taken from the Point-E / Shap-E rendering script | |
| (https://github.com/openai/point-e/blob/main/point_e/evals/scripts/blender_script.py#L97-L112), | |
| but fix for multiple root objects: (see bug report here: | |
| https://github.com/openai/shap-e/pull/60). | |
| Returns: | |
| The new parent object that all objects descend from. | |
| """ | |
| if len(list(get_scene_root_objects())) > 1: | |
| # create an empty object to be used as a parent for all root objects | |
| parent_empty = bpy.data.objects.new("ParentEmpty", None) | |
| bpy.context.scene.collection.objects.link(parent_empty) | |
| # parent all root objects to the empty object | |
| for obj in get_scene_root_objects(): | |
| if obj != parent_empty: | |
| obj.parent = parent_empty | |
| bbox_min, bbox_max = scene_bbox() | |
| scale = 1 / max(bbox_max - bbox_min) | |
| for obj in get_scene_root_objects(): | |
| obj.scale = obj.scale * scale | |
| # Apply scale to matrix_world. | |
| bpy.context.view_layer.update() | |
| bbox_min, bbox_max = scene_bbox() | |
| offset = -(bbox_min + bbox_max) / 2 | |
| for obj in get_scene_root_objects(): | |
| obj.matrix_world.translation += offset | |
| bpy.ops.object.select_all(action="DESELECT") | |
| bbox_min, bbox_max = scene_bbox() | |
| print(f"After normalize_scene: bbox_min: {bbox_min}, bbox_max: {bbox_max}") | |
| # unparent the camera | |
| bpy.data.objects["Camera"].parent = None | |
| return parent_empty | |
| def reset_cameras() -> None: | |
| """Resets the cameras in the scene to a single default camera.""" | |
| # Delete all existing cameras | |
| bpy.ops.object.select_all(action="DESELECT") | |
| bpy.ops.object.select_by_type(type="CAMERA") | |
| bpy.ops.object.delete() | |
| # Create a new camera with default properties | |
| bpy.ops.object.camera_add() | |
| # Rename the new camera to 'NewDefaultCamera' | |
| new_camera = bpy.context.active_object | |
| new_camera.name = "Camera" | |
| # Set the new camera as the active camera for the scene | |
| scene.camera = new_camera | |
| def get_camera_with_position(x, y, z, fov_degrees=40): | |
| camera = bpy.data.objects["Camera"] | |
| camera.data.angle = math.radians(fov_degrees) | |
| camera.location = np.array([x, y, z]) | |
| direction = -camera.location | |
| rot_quat = direction.to_track_quat("-Z", "Y") | |
| camera.rotation_euler = rot_quat.to_euler() | |
| return camera | |
| def reset_scene() -> None: | |
| """Resets the scene to a clean state. | |
| Returns: | |
| None | |
| """ | |
| # delete everything that isn't part of a camera or a light | |
| for obj in bpy.data.objects: | |
| if obj.type not in {"CAMERA", "LIGHT"}: | |
| bpy.data.objects.remove(obj, do_unlink=True) | |
| # delete all the materials | |
| for material in bpy.data.materials: | |
| bpy.data.materials.remove(material, do_unlink=True) | |
| # delete all the textures | |
| for texture in bpy.data.textures: | |
| bpy.data.textures.remove(texture, do_unlink=True) | |
| # delete all the images | |
| for image in bpy.data.images: | |
| bpy.data.images.remove(image, do_unlink=True) | |
| def load_object(object_path: str) -> None: | |
| """Loads a model with a supported file extension into the scene. | |
| Args: | |
| object_path (str): Path to the model file. | |
| Raises: | |
| ValueError: If the file extension is not supported. | |
| Returns: | |
| None | |
| """ | |
| file_extension = Path(object_path).suffix | |
| if file_extension is None or file_extension == "": | |
| raise ValueError(f"Unsupported file type: {object_path}") | |
| # load from existing import functions | |
| import_function = IMPORT_FUNCTIONS[file_extension] | |
| if file_extension in {".glb", ".gltf"}: | |
| import_function(filepath=object_path, merge_vertices=True) | |
| else: | |
| import_function(filepath=object_path) | |
| def clear_lights(): | |
| bpy.ops.object.select_all(action="DESELECT") | |
| for obj in bpy.context.scene.objects.values(): | |
| if isinstance(obj.data, bpy.types.Light): | |
| obj.select_set(True) | |
| bpy.ops.object.delete() | |
| def create_light( | |
| location, | |
| energy=1.0, | |
| angle=0.5 * math.pi / 180, | |
| light_type: Literal["POINT", "SUN", "SPOT", "AREA"] = "SUN", | |
| ): | |
| # https://blender.stackexchange.com/questions/215624/how-to-create-a-light-with-the-python-api-in-blender-2-92 | |
| light_data = bpy.data.lights.new(name="Light", type=light_type) | |
| light_data.energy = energy | |
| if light_type != "AREA" and light_type != "POINT": | |
| light_data.angle = angle | |
| light_object = bpy.data.objects.new(name="Light", object_data=light_data) | |
| direction = -location | |
| rot_quat = direction.to_track_quat("-Z", "Y") | |
| light_object.rotation_euler = rot_quat.to_euler() | |
| bpy.context.view_layer.update() | |
| bpy.context.collection.objects.link(light_object) | |
| light_object.location = location | |
| def create_uniform_lights( | |
| distance=2.0, | |
| energy=3.0, | |
| light_type: Literal["POINT", "SUN", "SPOT", "AREA"] = "SUN", | |
| ): | |
| clear_lights() | |
| create_light(Vector([1, 0, 0]) * distance, energy=energy, light_type=light_type) | |
| create_light(-Vector([1, 0, 0]) * distance, energy=energy, light_type=light_type) | |
| create_light(Vector([0, 1, 0]) * distance, energy=energy, light_type=light_type) | |
| create_light(-Vector([0, 1, 0]) * distance, energy=energy, light_type=light_type) | |
| create_light(Vector([0, 0, 1]) * distance, energy=energy, light_type=light_type) | |
| create_light(-Vector([0, 0, 1]) * distance, energy=energy, light_type=light_type) | |
| def create_light_at_camera_position( | |
| camera_position: Vector, | |
| energy=1.5, | |
| use_shadow=False, | |
| light_type: Literal["POINT", "SUN", "SPOT", "AREA"] = "SUN", | |
| ): | |
| clear_lights() | |
| create_light(camera_position, energy=energy, light_type=light_type) | |
| # disable shadows | |
| if not use_shadow: | |
| for light in bpy.data.lights: | |
| light.use_shadow = False | |
| def set_world_background_color( | |
| color: Tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0), | |
| ) -> None: | |
| bpy.context.scene.world.use_nodes = True | |
| bpy.context.scene.world.node_tree.nodes["Background"].inputs[ | |
| 0 | |
| ].default_value = color | |
| bpy.context.scene.view_settings.view_transform = "Standard" | |
| def scene_bbox( | |
| single_obj: Optional[bpy.types.Object] = None, ignore_matrix: bool = False | |
| ) -> Tuple[Vector, Vector]: | |
| """Returns the bounding box of the scene. | |
| Taken from Shap-E rendering script | |
| (https://github.com/openai/shap-e/blob/main/shap_e/rendering/blender/blender_script.py#L68-L82) | |
| Args: | |
| single_obj (Optional[bpy.types.Object], optional): If not None, only computes | |
| the bounding box for the given object. Defaults to None. | |
| ignore_matrix (bool, optional): Whether to ignore the object's matrix. Defaults | |
| to False. | |
| Raises: | |
| RuntimeError: If there are no objects in the scene. | |
| Returns: | |
| Tuple[Vector, Vector]: The minimum and maximum coordinates of the bounding box. | |
| """ | |
| bbox_min = (math.inf,) * 3 | |
| bbox_max = (-math.inf,) * 3 | |
| found = False | |
| for obj in get_scene_meshes() if single_obj is None else [single_obj]: | |
| found = True | |
| for coord in obj.bound_box: | |
| coord = Vector(coord) | |
| if not ignore_matrix: | |
| coord = obj.matrix_world @ coord | |
| bbox_min = tuple(min(x, y) for x, y in zip(bbox_min, coord)) | |
| bbox_max = tuple(max(x, y) for x, y in zip(bbox_max, coord)) | |
| if not found: | |
| raise RuntimeError("no objects in scene to compute bounding box for") | |
| return Vector(bbox_min), Vector(bbox_max) | |
| def get_scene_root_objects() -> Generator[bpy.types.Object, None, None]: | |
| """Returns all root objects in the scene. | |
| Yields: | |
| Generator[bpy.types.Object, None, None]: Generator of all root objects in the | |
| scene. | |
| """ | |
| for obj in bpy.context.scene.objects.values(): | |
| if not obj.parent and not isinstance(obj.data, bpy.types.Light): | |
| yield obj | |
| def get_scene_meshes() -> Generator[bpy.types.Object, None, None]: | |
| """Returns all meshes in the scene. | |
| Yields: | |
| Generator[bpy.types.Object, None, None]: Generator of all meshes in the scene. | |
| """ | |
| for obj in bpy.context.scene.objects.values(): | |
| if isinstance(obj.data, (bpy.types.Mesh)): | |
| yield obj | |
| def delete_missing_textures() -> Dict[str, Any]: | |
| """Deletes all missing textures in the scene. | |
| Returns: | |
| Dict[str, Any]: Dictionary with keys "count", "files", and "file_path_to_color". | |
| "count" is the number of missing textures, "files" is a list of the missing | |
| texture file paths, and "file_path_to_color" is a dictionary mapping the | |
| missing texture file paths to a random color. | |
| """ | |
| missing_file_count = 0 | |
| out_files = [] | |
| file_path_to_color = {} | |
| # Check all materials in the scene | |
| for material in bpy.data.materials: | |
| if material.use_nodes: | |
| for node in material.node_tree.nodes: | |
| if node.type == "TEX_IMAGE": | |
| image = node.image | |
| if image is not None: | |
| file_path = bpy.path.abspath(image.filepath) | |
| if file_path == "": | |
| # means it's embedded | |
| continue | |
| if not os.path.exists(file_path): | |
| # Find the connected Principled BSDF node | |
| connected_node = node.outputs[0].links[0].to_node | |
| if connected_node.type == "BSDF_PRINCIPLED": | |
| if file_path not in file_path_to_color: | |
| # Set a random color for the unique missing file path | |
| random_color = [random.random() for _ in range(3)] | |
| file_path_to_color[file_path] = random_color + [1] | |
| connected_node.inputs[ | |
| "Base Color" | |
| ].default_value = file_path_to_color[file_path] | |
| # Delete the TEX_IMAGE node | |
| material.node_tree.nodes.remove(node) | |
| missing_file_count += 1 | |
| out_files.append(image.filepath) | |
| return { | |
| "count": missing_file_count, | |
| "files": out_files, | |
| "file_path_to_color": file_path_to_color, | |
| } | |
| def setup_environment_lighting(envmap_path): | |
| world = bpy.context.scene.world | |
| world.use_nodes = True | |
| nodes = world.node_tree.nodes | |
| links = world.node_tree.links | |
| # Clear existing nodes | |
| for node in nodes: | |
| nodes.remove(node) | |
| # Create Background node | |
| bg_node = nodes.new(type="ShaderNodeBackground") | |
| bg_node.location = (0, 0) | |
| # Create Environment Texture node | |
| env_tex_node = nodes.new(type="ShaderNodeTexEnvironment") | |
| env_tex_node.location = (-300, 0) | |
| # Set the environment texture path (replace this with your file path) | |
| env_tex_node.image = bpy.data.images.load(envmap_path) | |
| # Create World Output node | |
| world_output_node = nodes.new(type="ShaderNodeOutputWorld") | |
| world_output_node.location = (300, 0) | |
| # Link nodes | |
| links.new(env_tex_node.outputs["Color"], bg_node.inputs["Color"]) | |
| links.new(bg_node.outputs["Background"], world_output_node.inputs["Surface"]) | |
| def create_solid_color_material(name, color): | |
| mat = bpy.data.materials.new(name) | |
| mat.use_nodes = True | |
| node_tree = mat.node_tree | |
| color_node = node_tree.nodes.new("ShaderNodeBsdfDiffuse") | |
| color_node.inputs["Color"].default_value = color | |
| mat_output = node_tree.nodes["Material Output"] | |
| node_tree.links.new(color_node.outputs["BSDF"], mat_output.inputs["Surface"]) | |
| return mat | |
| def create_phong_material(name, color): | |
| mat = bpy.data.materials.new(name) | |
| mat.use_nodes = True | |
| node_tree = mat.node_tree | |
| spec_node = node_tree.nodes.new("ShaderNodeBsdfPrincipled") | |
| print(spec_node.inputs.keys()) | |
| spec_node.inputs["Base Color"].default_value = color | |
| spec_node.inputs["Roughness"].default_value = 0.5 | |
| spec_node.inputs["Metallic"].default_value = 1.0 | |
| mat_output = node_tree.nodes["Material Output"] | |
| node_tree.links.new(spec_node.outputs["BSDF"], mat_output.inputs["Surface"]) | |
| return mat | |
| def render_object( | |
| object_file: str, | |
| num_renders: int, | |
| output_dir: str, | |
| transparent_background: bool = False, | |
| environment_map: str = None, | |
| ) -> None: | |
| """Saves rendered images for given asset to specified output directory. | |
| Args: | |
| object_file (str): Path to the object file. | |
| num_renders (int): Number of renders to save of the object. | |
| output_dir (str): Path to the directory where the rendered images and metadata | |
| will be saved. The rendered images will be saved in the subdirectory | |
| `output_dir/stemname`. | |
| transparent_background (bool): Whether to use transparent background, | |
| otherwise the background is white. | |
| Returns: | |
| None | |
| """ | |
| os.makedirs(output_dir, exist_ok=True) | |
| # load the object | |
| reset_scene() | |
| load_object(object_file) | |
| if transparent_background: | |
| scene.render.film_transparent = True | |
| else: | |
| scene.render.film_transparent = False | |
| set_world_background_color([0.2, 0.2, 0.2, 1.0]) | |
| # normalize the scene | |
| _ = normalize_scene() | |
| # Set up cameras | |
| cam = scene.objects["Camera"] | |
| fov_degrees = 40.0 | |
| cam.data.angle = np.radians(fov_degrees) | |
| # Set up camera constraints | |
| cam_constraint = cam.constraints.new(type="TRACK_TO") | |
| cam_constraint.track_axis = "TRACK_NEGATIVE_Z" | |
| cam_constraint.up_axis = "UP_Y" | |
| empty = bpy.data.objects.new("Empty", None) | |
| empty.location = (0, 0, 0) | |
| scene.collection.objects.link(empty) | |
| cam_constraint.target = empty | |
| cam.parent = empty | |
| # delete all objects that are not meshes | |
| delete_missing_textures() | |
| if environment_map: | |
| setup_environment_lighting(environment_map) | |
| else: | |
| create_uniform_lights(energy=1.0, light_type="SUN") | |
| camera_position = [0, -2, 0] | |
| # determine how much to orbit camera by. | |
| stepsize = 360.0 / num_renders | |
| def render_views(name): | |
| for i in range(num_renders): | |
| # set camera | |
| _ = get_camera_with_position( | |
| camera_position[0], | |
| camera_position[1], | |
| camera_position[2], | |
| fov_degrees=fov_degrees, | |
| ) | |
| # Set output paths with absolute paths | |
| render_path = os.path.abspath( | |
| os.path.join(output_dir, f"{i:03d}_{name}.png") | |
| ) | |
| # Set file output paths | |
| scene.render.filepath = render_path | |
| # Make sure the output directory exists | |
| os.makedirs(output_dir, exist_ok=True) | |
| # Render | |
| bpy.ops.render.render(write_still=True) | |
| context.view_layer.objects.active = empty | |
| empty.rotation_euler[2] += math.radians(stepsize) | |
| # ensure that all objects have materials, if not then add a default | |
| # one. | |
| textured_mat = create_solid_color_material("default texture", [0.6, 0.6, 0.6, 1]) | |
| for obj in get_scene_meshes(): | |
| if obj.active_material is None: | |
| obj.active_material = textured_mat | |
| render_views("textured") | |
| def enable_gpus(device_type, use_cpus=False): | |
| preferences = bpy.context.preferences | |
| cycles_preferences = preferences.addons["cycles"].preferences | |
| cycles_preferences.refresh_devices() | |
| try: | |
| devices = cycles_preferences.devices | |
| except: | |
| print("No devices detected") | |
| if device_type == "CPU": | |
| return [] | |
| else: | |
| raise RuntimeError(f"No devices detected, set use_cpus to True") | |
| assert device_type in [ | |
| "CUDA", | |
| "METAL", | |
| "OPENCL", | |
| "CPU", | |
| "NONE", | |
| ], f"Unsupported device type: {device_type}" | |
| try: | |
| # print(devices) | |
| iter(devices) | |
| except TypeError: | |
| # print("Single GPU Detected") | |
| devices = [devices] | |
| activated_gpus = [] | |
| for device in devices: | |
| if device.type == "CPU": | |
| device.use = use_cpus | |
| else: | |
| device.use = True | |
| activated_gpus.append(device.name) | |
| if device_type == "CUDA": | |
| cycles_preferences.compute_device_type = "CUDA" | |
| bpy.context.scene.cycles.device = "GPU" | |
| elif device_type == "METAL": | |
| cycles_preferences.compute_device_type = "METAL" | |
| bpy.context.scene.cycles.device = "GPU" | |
| elif device_type == "OPENCL": | |
| cycles_preferences.compute_device_type = "OPENCL" | |
| bpy.context.scene.cycles.device = "GPU" | |
| else: | |
| raise RuntimeError(f"Unsupported device type: {device_type}") | |
| return activated_gpus | |
| def set_render_settings(engine, resolution): | |
| # Set render settings | |
| render.engine = engine # | |
| render.image_settings.file_format = "PNG" | |
| render.image_settings.color_mode = "RGBA" | |
| render.resolution_x = resolution | |
| render.resolution_y = resolution | |
| render.resolution_percentage = 100 | |
| # Set cycles settings | |
| scene.cycles.device = "GPU" | |
| scene.cycles.use_adaptive_sampling = True | |
| scene.cycles.adaptive_threshold = 0.1 | |
| scene.cycles.samples = 64 | |
| scene.cycles.adaptive_min_samples = 1 | |
| scene.cycles.filter_width = 2 | |
| scene.cycles.use_fast_gi = True | |
| scene.cycles.fast_gi_method = "REPLACE" | |
| world.light_settings.ao_factor = 1.0 | |
| world.light_settings.distance = 10 | |
| scene.cycles.use_denoising = True # ML denoising | |
| scene.cycles.denoising_use_gpu = True | |
| # bake existing frames for faster future renders | |
| scene.render.use_persistent_data = True | |
| # Set eevee settings | |
| scene.eevee.use_shadows = True | |
| scene.eevee.use_raytracing = True | |
| scene.eevee.ray_tracing_options.use_denoise = True | |
| scene.eevee.use_fast_gi = True | |
| scene.eevee.fast_gi_method = "GLOBAL_ILLUMINATION" | |
| scene.eevee.ray_tracing_options.trace_max_roughness = 0.5 | |
| scene.eevee.fast_gi_resolution = "2" | |
| scene.eevee.fast_gi_ray_count = 2 | |
| scene.eevee.fast_gi_step_count = 8 | |
| def print_devices(): | |
| print("Devices:") | |
| preferences = bpy.context.preferences | |
| cycles_preferences = preferences.addons["cycles"].preferences | |
| cycles_preferences.refresh_devices() | |
| devices = cycles_preferences.devices | |
| for device in devices: | |
| print(f' [{device.id}]<{device.type}> "{device.name}" Using: {device.use}') | |
| print(f"Compute device type: {cycles_preferences.compute_device_type}") | |
| print(f"Cycles device: {bpy.context.scene.cycles.device}") | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument( | |
| "--object_path", | |
| type=str, | |
| required=False, | |
| help="Path to the object file", | |
| ) | |
| parser.add_argument( | |
| "--output_dir", | |
| type=str, | |
| required=True, | |
| help="Path to the directory where the rendered images and metadata will be saved.", | |
| ) | |
| parser.add_argument( | |
| "--engine", | |
| type=str, | |
| default="BLENDER_EEVEE_NEXT", # BLENDER_BLENDER_EEVEE_NEXT rasterization, better than nvdifrast, CYCLES | |
| choices=["CYCLES", "BLENDER_EEVEE_NEXT"], | |
| ) | |
| parser.add_argument( | |
| "--num_renders", | |
| type=int, | |
| default=12, | |
| help="Number of renders to save of the object.", | |
| ) | |
| parser.add_argument( | |
| "--render_resolution", | |
| type=int, | |
| default=512, | |
| help="Resolution of the rendered images.", | |
| ) | |
| parser.add_argument( | |
| "--transparent_background", | |
| action="store_true", | |
| help="Whether to use transparent background", | |
| ) | |
| parser.add_argument( | |
| "--environment_map", | |
| default=None, | |
| type=str, | |
| help="Use the given environment map for lighting", | |
| ) | |
| argv = sys.argv[sys.argv.index("--") + 1 :] | |
| args = parser.parse_args(argv) | |
| context = bpy.context | |
| scene = context.scene | |
| render = scene.render | |
| world = bpy.data.worlds["World"] | |
| set_render_settings(args.engine, args.render_resolution) | |
| # detect platform and activate GPUs | |
| platform = platform.system() | |
| if platform == "Darwin": | |
| activated_gpus = enable_gpus("METAL", use_cpus=True) | |
| elif platform == "Linux": | |
| activated_gpus = enable_gpus("CUDA", use_cpus=False) | |
| else: | |
| raise RuntimeError("Unsupported platform") | |
| print(f"Activated GPUs: {activated_gpus}") | |
| print_devices() | |
| render_object( | |
| object_file=args.object_path, | |
| num_renders=args.num_renders, | |
| output_dir=args.output_dir, | |
| transparent_background=args.transparent_background, | |
| environment_map=args.environment_map, | |
| ) | |