Spaces:
Running on L40S
Running on L40S
| import bpy | |
| import json | |
| import math | |
| import mathutils | |
| import numpy as np | |
| class BpyRenderer: | |
| def __init__(self, resolution=512, engine="BLENDER_EEVEE", geo_mode=False, split_normal=False): | |
| """ | |
| engine: | |
| - "CYCLES" | |
| - "BLENDER_EEVEE" (Blender 3.x common) | |
| - "BLENDER_EEVEE_NEXT" (Blender 4.x common) | |
| - "EEVEE" / "EEVEE_NEXT" (aliases, optional) | |
| """ | |
| self.resolution = resolution | |
| self.engine = engine | |
| self.geo_mode = geo_mode | |
| self.split_normal = split_normal | |
| self.import_functions = self._setup_import_functions() | |
| def _setup_import_functions(self): | |
| import_functions = { | |
| "obj": bpy.ops.wm.obj_import, | |
| "glb": bpy.ops.import_scene.gltf, | |
| "gltf": bpy.ops.import_scene.gltf, | |
| "usd": bpy.ops.import_scene.usd, | |
| "fbx": bpy.ops.import_scene.fbx, | |
| "stl": bpy.ops.import_mesh.stl, | |
| "usda": bpy.ops.import_scene.usda, | |
| "dae": bpy.ops.wm.collada_import, | |
| "ply": bpy.ops.wm.ply_import, | |
| "abc": bpy.ops.wm.alembic_import, | |
| "blend": bpy.ops.wm.append, | |
| } | |
| return import_functions | |
| # ------------------------- | |
| # Engine helpers | |
| # ------------------------- | |
| def _resolve_render_engine(self, requested: str) -> str: | |
| """ | |
| Robustly set render engine across Blender versions. | |
| Blender 4.x may not accept "BLENDER_EEVEE" and instead uses "BLENDER_EEVEE_NEXT". | |
| """ | |
| req = (requested or "").upper() | |
| if req in {"EEVEE", "BLENDER_EEVEE"}: | |
| candidates = ["BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"] | |
| elif req in {"EEVEE_NEXT", "BLENDER_EEVEE_NEXT"}: | |
| candidates = ["BLENDER_EEVEE_NEXT", "BLENDER_EEVEE"] | |
| elif req in {"CYCLES"}: | |
| candidates = ["CYCLES"] | |
| elif req in {"WORKBENCH", "BLENDER_WORKBENCH"}: | |
| candidates = ["BLENDER_WORKBENCH"] | |
| else: | |
| candidates = [requested] | |
| last_err = None | |
| for eng in candidates: | |
| try: | |
| bpy.context.scene.render.engine = eng | |
| return eng | |
| except Exception as e: | |
| last_err = e | |
| continue | |
| raise ValueError(f"Failed to set render engine from {candidates}. Last error: {last_err}") | |
| def _init_eevee_settings(self, render_samples: int = 64): | |
| """ | |
| EEVEE / EEVEE Next settings (close to huanngzh/bpy-renderer defaults). | |
| """ | |
| scene = bpy.context.scene | |
| # Render basics | |
| scene.render.image_settings.file_format = "PNG" | |
| scene.render.image_settings.color_mode = "RGBA" | |
| scene.render.film_transparent = True | |
| # EEVEE quality knobs | |
| # In Blender, eevee settings live under scene.eevee. | |
| # These fields are used by many scripts including bpy-renderer. :contentReference[oaicite:2]{index=2} | |
| if hasattr(scene, "eevee"): | |
| try: | |
| scene.eevee.taa_render_samples = int(render_samples) | |
| except Exception: | |
| pass | |
| # These flags may not exist in every minor version; guard them. | |
| for name, val in [ | |
| ("use_gtao", True), | |
| ("use_ssr", True), | |
| ("use_bloom", True), | |
| ]: | |
| if hasattr(scene.eevee, name): | |
| try: | |
| setattr(scene.eevee, name, val) | |
| except Exception: | |
| pass | |
| # Normals quality (also in bpy-renderer init) :contentReference[oaicite:3]{index=3} | |
| if hasattr(scene.render, "use_high_quality_normals"): | |
| try: | |
| scene.render.use_high_quality_normals = True | |
| except Exception: | |
| pass | |
| def _init_cycles_settings(self, render_samples: int = 128): | |
| scene = bpy.context.scene | |
| scene.render.image_settings.file_format = "PNG" | |
| scene.render.image_settings.color_mode = "RGBA" | |
| scene.render.film_transparent = True | |
| scene.cycles.samples = int(render_samples) | |
| scene.cycles.filter_type = "BOX" | |
| scene.cycles.filter_width = 1 | |
| scene.cycles.diffuse_bounces = 1 | |
| scene.cycles.glossy_bounces = 1 | |
| scene.cycles.transparent_max_bounces = (3 if not self.geo_mode else 0) | |
| scene.cycles.transmission_bounces = (3 if not self.geo_mode else 1) | |
| scene.cycles.use_denoising = True | |
| # GPU (best-effort) | |
| try: | |
| scene.cycles.device = "GPU" | |
| bpy.context.preferences.addons["cycles"].preferences.get_devices() | |
| bpy.context.preferences.addons["cycles"].preferences.compute_device_type = "CUDA" | |
| except Exception: | |
| pass | |
| # ------------------------- | |
| # Public init | |
| # ------------------------- | |
| def init_render_settings(self): | |
| # Resolution | |
| bpy.context.scene.render.resolution_x = self.resolution | |
| bpy.context.scene.render.resolution_y = self.resolution | |
| bpy.context.scene.render.resolution_percentage = 100 | |
| # Pick engine robustly (EEVEE vs EEVEE_NEXT etc.) | |
| actual_engine = self._resolve_render_engine(self.engine) | |
| # Samples: | |
| # - For geo_mode: keep minimal samples for speed | |
| # - For RGB: moderate samples | |
| if actual_engine == "CYCLES": | |
| samples = 128 if not self.geo_mode else 1 | |
| self._init_cycles_settings(render_samples=samples) | |
| else: | |
| # EEVEE family | |
| samples = 64 if not self.geo_mode else 1 | |
| self._init_eevee_settings(render_samples=samples) | |
| def init_scene(self): | |
| for obj in bpy.data.objects: | |
| bpy.data.objects.remove(obj, do_unlink=True) | |
| for material in bpy.data.materials: | |
| bpy.data.materials.remove(material, do_unlink=True) | |
| for texture in bpy.data.textures: | |
| bpy.data.textures.remove(texture, do_unlink=True) | |
| for image in bpy.data.images: | |
| bpy.data.images.remove(image, do_unlink=True) | |
| def init_camera(self): | |
| cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera")) | |
| bpy.context.collection.objects.link(cam) | |
| bpy.context.scene.camera = cam | |
| cam.data.sensor_height = cam.data.sensor_width = 32 | |
| cam_constraint = cam.constraints.new(type="TRACK_TO") | |
| cam_constraint.track_axis = "TRACK_NEGATIVE_Z" | |
| cam_constraint.up_axis = "UP_Y" | |
| cam_empty = bpy.data.objects.new("Empty", None) | |
| cam_empty.location = (0, 0, 0) | |
| bpy.context.scene.collection.objects.link(cam_empty) | |
| cam_constraint.target = cam_empty | |
| return cam | |
| def init_lighting(self): | |
| bpy.ops.object.select_all(action="DESELECT") | |
| bpy.ops.object.select_by_type(type="LIGHT") | |
| bpy.ops.object.delete() | |
| default_light = bpy.data.objects.new("Default_Light", bpy.data.lights.new("Default_Light", type="POINT")) | |
| bpy.context.collection.objects.link(default_light) | |
| default_light.data.energy = 1000 | |
| default_light.location = (4, 1, 6) | |
| default_light.rotation_euler = (0, 0, 0) | |
| top_light = bpy.data.objects.new("Top_Light", bpy.data.lights.new("Top_Light", type="AREA")) | |
| bpy.context.collection.objects.link(top_light) | |
| top_light.data.energy = 10000 | |
| top_light.location = (0, 0, 10) | |
| top_light.scale = (100, 100, 100) | |
| bottom_light = bpy.data.objects.new("Bottom_Light", bpy.data.lights.new("Bottom_Light", type="AREA")) | |
| bpy.context.collection.objects.link(bottom_light) | |
| bottom_light.data.energy = 1000 | |
| bottom_light.location = (0, 0, -10) | |
| bottom_light.rotation_euler = (0, 0, 0) | |
| return {"default_light": default_light, "top_light": top_light, "bottom_light": bottom_light} | |
| def load_object(self, object_path): | |
| file_extension = object_path.split(".")[-1].lower() | |
| if file_extension not in self.import_functions: | |
| raise ValueError(f"Unsupported file type: {file_extension}") | |
| import_function = self.import_functions[file_extension] | |
| print(f"Loading object from {object_path}") | |
| if file_extension == "blend": | |
| import_function(directory=object_path, link=False) | |
| elif file_extension in {"glb", "gltf"}: | |
| import_function(filepath=object_path, merge_vertices=True, import_shading="NORMALS") | |
| else: | |
| import_function(filepath=object_path) | |
| def delete_invisible_objects(self): | |
| bpy.ops.object.select_all(action="DESELECT") | |
| for obj in bpy.context.scene.objects: | |
| if obj.hide_viewport or obj.hide_render: | |
| obj.hide_viewport = False | |
| obj.hide_render = False | |
| obj.hide_select = False | |
| obj.select_set(True) | |
| bpy.ops.object.delete() | |
| invisible_collections = [col for col in bpy.data.collections if col.hide_viewport] | |
| for col in invisible_collections: | |
| bpy.data.collections.remove(col) | |
| def split_mesh_normal(self): | |
| bpy.ops.object.select_all(action="DESELECT") | |
| objs = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"] | |
| bpy.context.view_layer.objects.active = objs[0] | |
| for obj in objs: | |
| obj.select_set(True) | |
| bpy.ops.object.mode_set(mode="EDIT") | |
| bpy.ops.mesh.select_all(action="SELECT") | |
| bpy.ops.mesh.split_normals() | |
| bpy.ops.object.mode_set(mode="OBJECT") | |
| bpy.ops.object.select_all(action="DESELECT") | |
| def override_material(self): | |
| new_mat = bpy.data.materials.new(name="Override0123456789") | |
| new_mat.use_nodes = True | |
| new_mat.node_tree.nodes.clear() | |
| bsdf = new_mat.node_tree.nodes.new("ShaderNodeBsdfDiffuse") | |
| bsdf.inputs[0].default_value = (0.5, 0.5, 0.5, 1) | |
| bsdf.inputs[1].default_value = 1 | |
| output = new_mat.node_tree.nodes.new("ShaderNodeOutputMaterial") | |
| new_mat.node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"]) | |
| bpy.context.scene.view_layers["View Layer"].material_override = new_mat | |
| def scene_bbox(self): | |
| bbox_min = (math.inf,) * 3 | |
| bbox_max = (-math.inf,) * 3 | |
| found = False | |
| scene_meshes = [obj for obj in bpy.context.scene.objects.values() if isinstance(obj.data, bpy.types.Mesh)] | |
| for obj in scene_meshes: | |
| found = True | |
| for coord in obj.bound_box: | |
| coord = mathutils.Vector(coord) | |
| 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 mathutils.Vector(bbox_min), mathutils.Vector(bbox_max) | |
| def normalize_scene(self): | |
| scene_root_objects = [obj for obj in bpy.context.scene.objects.values() if not obj.parent] | |
| if len(scene_root_objects) > 1: | |
| scene = bpy.data.objects.new("ParentEmpty", None) | |
| bpy.context.scene.collection.objects.link(scene) | |
| for obj in scene_root_objects: | |
| obj.parent = scene | |
| else: | |
| scene = scene_root_objects[0] | |
| bbox_min, bbox_max = self.scene_bbox() | |
| print(f"[INFO] Bounding box: {bbox_min}, {bbox_max}") | |
| scale = 1 / max(bbox_max - bbox_min) | |
| scene.scale = scene.scale * scale | |
| bpy.context.view_layer.update() | |
| bbox_min, bbox_max = self.scene_bbox() | |
| offset = -(bbox_min + bbox_max) / 2 | |
| scene.matrix_world.translation += offset | |
| bpy.ops.object.select_all(action="DESELECT") | |
| return scale, offset | |
| def set_camera_from_matrix(self, cam, transform_matrix): | |
| matrix = mathutils.Matrix(transform_matrix) | |
| cam.matrix_world = matrix | |
| bpy.context.view_layer.update() | |
| def render_from_transforms(self, file_path, transforms_json_path, output_path): | |
| with open(transforms_json_path, "r") as f: | |
| transforms_data = json.load(f) | |
| self.init_render_settings() | |
| # Load scene | |
| if file_path.endswith(".blend"): | |
| self.delete_invisible_objects() | |
| else: | |
| self.init_scene() | |
| self.load_object(file_path) | |
| if self.split_normal: | |
| self.split_mesh_normal() | |
| print("[INFO] Scene initialized.") | |
| scale, offset = self.normalize_scene() | |
| print(f"[INFO] Scene normalized with auto scale: {scale}, offset: {offset}") | |
| cam = self.init_camera() | |
| self.init_lighting() | |
| print("[INFO] Camera and lighting initialized.") | |
| if self.geo_mode: | |
| self.override_material() | |
| # NOTE: your transforms_json format seems like a list-of-dicts. | |
| transform_matrix = transforms_data[0]["transform_matrix"] | |
| camera_angle_x = transforms_data[0].get("camera_angle_x", None) | |
| self.set_camera_from_matrix(cam, transform_matrix) | |
| if camera_angle_x is not None: | |
| cam.data.lens = 16 / np.tan(camera_angle_x / 2) | |
| bpy.context.scene.render.filepath = output_path | |
| bpy.ops.render.render(write_still=True) | |
| bpy.context.view_layer.update() | |
| def render_from_transforms( | |
| file_path, | |
| transforms_json_path, | |
| output_path, | |
| resolution=512, | |
| engine="BLENDER_EEVEE", | |
| geo_mode=False, | |
| split_normal=False, | |
| ): | |
| renderer = BpyRenderer(resolution=resolution, engine=engine, geo_mode=geo_mode, split_normal=split_normal) | |
| return renderer.render_from_transforms(file_path, transforms_json_path, output_path) | |
| if __name__ == "__main__": | |
| file_path = "./assets/example.glb" | |
| transforms_json_path = "transforms.json" | |
| output_path = "./assets/img.png" | |
| # Recommended: | |
| # - engine="BLENDER_EEVEE" for Blender 3.x | |
| # - engine="BLENDER_EEVEE_NEXT" for Blender 4.x | |
| # This script auto-fallbacks between them. | |
| render_from_transforms( | |
| file_path=file_path, | |
| transforms_json_path=transforms_json_path, | |
| output_path=output_path, | |
| resolution=512, | |
| engine="BLENDER_EEVEE", | |
| ) | |
| # import bpy | |
| # import json | |
| # import math | |
| # import mathutils | |
| # import numpy as np | |
| # class BpyRenderer: | |
| # def __init__(self, resolution=512, engine="CYCLES", geo_mode=False, split_normal=False): | |
| # self.resolution = resolution | |
| # self.engine = engine | |
| # self.geo_mode = geo_mode | |
| # self.split_normal = split_normal | |
| # self.import_functions = self._setup_import_functions() | |
| # def _setup_import_functions(self): | |
| # import_functions = { | |
| # "obj": bpy.ops.wm.obj_import, | |
| # "glb": bpy.ops.import_scene.gltf, | |
| # "gltf": bpy.ops.import_scene.gltf, | |
| # "usd": bpy.ops.import_scene.usd, | |
| # "fbx": bpy.ops.import_scene.fbx, | |
| # "stl": bpy.ops.import_mesh.stl, | |
| # "usda": bpy.ops.import_scene.usda, | |
| # "dae": bpy.ops.wm.collada_import, | |
| # "ply": bpy.ops.wm.ply_import, | |
| # "abc": bpy.ops.wm.alembic_import, | |
| # "blend": bpy.ops.wm.append, | |
| # } | |
| # return import_functions | |
| # def init_render_settings(self): | |
| # bpy.context.scene.render.engine = self.engine | |
| # bpy.context.scene.render.resolution_x = self.resolution | |
| # bpy.context.scene.render.resolution_y = self.resolution | |
| # bpy.context.scene.render.resolution_percentage = 100 | |
| # bpy.context.scene.render.image_settings.file_format = "PNG" | |
| # bpy.context.scene.render.image_settings.color_mode = "RGBA" | |
| # bpy.context.scene.render.film_transparent = True | |
| # if self.engine == "CYCLES": | |
| # bpy.context.scene.render.engine = "CYCLES" | |
| # bpy.context.scene.cycles.samples = 128 if not self.geo_mode else 1 | |
| # bpy.context.scene.cycles.filter_type = "BOX" | |
| # bpy.context.scene.cycles.filter_width = 1 | |
| # bpy.context.scene.cycles.diffuse_bounces = 1 | |
| # bpy.context.scene.cycles.glossy_bounces = 1 | |
| # bpy.context.scene.cycles.transparent_max_bounces = (3 if not self.geo_mode else 0) | |
| # bpy.context.scene.cycles.transmission_bounces = (3 if not self.geo_mode else 1) | |
| # bpy.context.scene.cycles.use_denoising = True | |
| # try: | |
| # bpy.context.scene.cycles.device = "GPU" | |
| # bpy.context.preferences.addons["cycles"].preferences.get_devices() | |
| # bpy.context.preferences.addons["cycles"].preferences.compute_device_type = "CUDA" | |
| # except: | |
| # pass | |
| # def init_scene(self): | |
| # for obj in bpy.data.objects: | |
| # bpy.data.objects.remove(obj, do_unlink=True) | |
| # for material in bpy.data.materials: | |
| # bpy.data.materials.remove(material, do_unlink=True) | |
| # for texture in bpy.data.textures: | |
| # bpy.data.textures.remove(texture, do_unlink=True) | |
| # for image in bpy.data.images: | |
| # bpy.data.images.remove(image, do_unlink=True) | |
| # def init_camera(self): | |
| # cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera")) | |
| # bpy.context.collection.objects.link(cam) | |
| # bpy.context.scene.camera = cam | |
| # cam.data.sensor_height = cam.data.sensor_width = 32 | |
| # cam_constraint = cam.constraints.new(type="TRACK_TO") | |
| # cam_constraint.track_axis = "TRACK_NEGATIVE_Z" | |
| # cam_constraint.up_axis = "UP_Y" | |
| # cam_empty = bpy.data.objects.new("Empty", None) | |
| # cam_empty.location = (0, 0, 0) | |
| # bpy.context.scene.collection.objects.link(cam_empty) | |
| # cam_constraint.target = cam_empty | |
| # return cam | |
| # def init_lighting(self): | |
| # bpy.ops.object.select_all(action="DESELECT") | |
| # bpy.ops.object.select_by_type(type="LIGHT") | |
| # bpy.ops.object.delete() | |
| # default_light = bpy.data.objects.new("Default_Light", bpy.data.lights.new("Default_Light", type="POINT")) | |
| # bpy.context.collection.objects.link(default_light) | |
| # default_light.data.energy = 1000 | |
| # default_light.location = (4, 1, 6) | |
| # default_light.rotation_euler = (0, 0, 0) | |
| # top_light = bpy.data.objects.new("Top_Light", bpy.data.lights.new("Top_Light", type="AREA")) | |
| # bpy.context.collection.objects.link(top_light) | |
| # top_light.data.energy = 10000 | |
| # top_light.location = (0, 0, 10) | |
| # top_light.scale = (100, 100, 100) | |
| # bottom_light = bpy.data.objects.new("Bottom_Light", bpy.data.lights.new("Bottom_Light", type="AREA")) | |
| # bpy.context.collection.objects.link(bottom_light) | |
| # bottom_light.data.energy = 1000 | |
| # bottom_light.location = (0, 0, -10) | |
| # bottom_light.rotation_euler = (0, 0, 0) | |
| # return {"default_light": default_light, "top_light": top_light, "bottom_light": bottom_light} | |
| # def load_object(self, object_path): | |
| # file_extension = object_path.split(".")[-1].lower() | |
| # if file_extension not in self.import_functions: | |
| # raise ValueError(f"Unsupported file type: {file_extension}") | |
| # import_function = self.import_functions[file_extension] | |
| # print(f"Loading object from {object_path}") | |
| # if file_extension == "blend": | |
| # import_function(directory=object_path, link=False) | |
| # elif file_extension in {"glb", "gltf"}: | |
| # import_function(filepath=object_path, merge_vertices=True, import_shading="NORMALS") | |
| # else: | |
| # import_function(filepath=object_path) | |
| # def delete_invisible_objects(self): | |
| # bpy.ops.object.select_all(action="DESELECT") | |
| # for obj in bpy.context.scene.objects: | |
| # if obj.hide_viewport or obj.hide_render: | |
| # obj.hide_viewport = False | |
| # obj.hide_render = False | |
| # obj.hide_select = False | |
| # obj.select_set(True) | |
| # bpy.ops.object.delete() | |
| # invisible_collections = [col for col in bpy.data.collections if col.hide_viewport] | |
| # for col in invisible_collections: | |
| # bpy.data.collections.remove(col) | |
| # def unhide_all_objects(self): | |
| # for obj in bpy.context.scene.objects: | |
| # obj.hide_set(False) | |
| # def convert_to_meshes(self): | |
| # bpy.ops.object.select_all(action="DESELECT") | |
| # bpy.context.view_layer.objects.active = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"][0] | |
| # for obj in bpy.context.scene.objects: | |
| # obj.select_set(True) | |
| # bpy.ops.object.convert(target="MESH") | |
| # def triangulate_meshes(self): | |
| # bpy.ops.object.select_all(action="DESELECT") | |
| # objs = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"] | |
| # bpy.context.view_layer.objects.active = objs[0] | |
| # for obj in objs: | |
| # obj.select_set(True) | |
| # bpy.ops.object.mode_set(mode="EDIT") | |
| # bpy.ops.mesh.reveal() | |
| # bpy.ops.mesh.select_all(action="SELECT") | |
| # bpy.ops.mesh.quads_convert_to_tris(quad_method="BEAUTY", ngon_method="BEAUTY") | |
| # bpy.ops.object.mode_set(mode="OBJECT") | |
| # bpy.ops.object.select_all(action="DESELECT") | |
| # def split_mesh_normal(self): | |
| # bpy.ops.object.select_all(action="DESELECT") | |
| # objs = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"] | |
| # bpy.context.view_layer.objects.active = objs[0] | |
| # for obj in objs: | |
| # obj.select_set(True) | |
| # bpy.ops.object.mode_set(mode="EDIT") | |
| # bpy.ops.mesh.select_all(action="SELECT") | |
| # bpy.ops.mesh.split_normals() | |
| # bpy.ops.object.mode_set(mode="OBJECT") | |
| # bpy.ops.object.select_all(action="DESELECT") | |
| # def override_material(self): | |
| # new_mat = bpy.data.materials.new(name="Override0123456789") | |
| # new_mat.use_nodes = True | |
| # new_mat.node_tree.nodes.clear() | |
| # bsdf = new_mat.node_tree.nodes.new("ShaderNodeBsdfDiffuse") | |
| # bsdf.inputs[0].default_value = (0.5, 0.5, 0.5, 1) | |
| # bsdf.inputs[1].default_value = 1 | |
| # output = new_mat.node_tree.nodes.new("ShaderNodeOutputMaterial") | |
| # new_mat.node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"]) | |
| # bpy.context.scene.view_layers["View Layer"].material_override = new_mat | |
| # def scene_bbox(self): | |
| # bbox_min = (math.inf,) * 3 | |
| # bbox_max = (-math.inf,) * 3 | |
| # found = False | |
| # scene_meshes = [obj for obj in bpy.context.scene.objects.values() if isinstance(obj.data, bpy.types.Mesh)] | |
| # for obj in scene_meshes: | |
| # found = True | |
| # for coord in obj.bound_box: | |
| # coord = mathutils.Vector(coord) | |
| # 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 mathutils.Vector(bbox_min), mathutils.Vector(bbox_max) | |
| # def normalize_scene(self): | |
| # scene_root_objects = [obj for obj in bpy.context.scene.objects.values() if not obj.parent] | |
| # if len(scene_root_objects) > 1: | |
| # scene = bpy.data.objects.new("ParentEmpty", None) | |
| # bpy.context.scene.collection.objects.link(scene) | |
| # for obj in scene_root_objects: | |
| # obj.parent = scene | |
| # else: | |
| # scene = scene_root_objects[0] | |
| # bbox_min, bbox_max = self.scene_bbox() | |
| # print(f"[INFO] Bounding box: {bbox_min}, {bbox_max}") | |
| # scale = 1 / max(bbox_max - bbox_min) | |
| # scene.scale = scene.scale * scale | |
| # bpy.context.view_layer.update() | |
| # bbox_min, bbox_max = self.scene_bbox() | |
| # offset = -(bbox_min + bbox_max) / 2 | |
| # scene.matrix_world.translation += offset | |
| # bpy.ops.object.select_all(action="DESELECT") | |
| # return scale, offset | |
| # def set_camera_from_matrix(self, cam, transform_matrix): | |
| # matrix = mathutils.Matrix(transform_matrix) | |
| # cam.matrix_world = matrix | |
| # bpy.context.view_layer.update() | |
| # def render_from_transforms(self, file_path, transforms_json_path, output_path): | |
| # with open(transforms_json_path, 'r') as f: | |
| # transforms_data = json.load(f) | |
| # self.init_render_settings() | |
| # if file_path.endswith(".blend"): | |
| # self.delete_invisible_objects() | |
| # else: | |
| # self.init_scene() | |
| # self.load_object(file_path) | |
| # if self.split_normal: | |
| # self.split_mesh_normal() | |
| # print("[INFO] Scene initialized.") | |
| # scale, offset = self.normalize_scene() | |
| # print(f"[INFO] Scene normalized with auto scale: {scale}, offset: {offset}") | |
| # cam = self.init_camera() | |
| # self.init_lighting() | |
| # print("[INFO] Camera and lighting initialized.") | |
| # if self.geo_mode: | |
| # self.override_material() | |
| # transform_matrix = transforms_data[0]["transform_matrix"] | |
| # camera_angle_x = transforms_data[0]["camera_angle_x"] | |
| # self.set_camera_from_matrix(cam, transform_matrix) | |
| # if camera_angle_x is not None: | |
| # cam.data.lens = 16 / np.tan(camera_angle_x / 2) | |
| # bpy.context.scene.render.filepath = output_path | |
| # bpy.ops.render.render(write_still=True) | |
| # bpy.context.view_layer.update() | |
| # def render_from_transforms(file_path, transforms_json_path, output_path, resolution=512, engine="CYCLES", geo_mode=False, split_normal=False): | |
| # renderer = BpyRenderer(resolution=resolution, engine=engine, geo_mode=geo_mode, split_normal=split_normal) | |
| # return renderer.render_from_transforms(file_path, transforms_json_path, output_path) | |
| # if __name__ == "__main__": | |
| # file_path = "./assets/example.glb" | |
| # transforms_json_path = "transforms.json" | |
| # output_path = "./assets/img.png" | |
| # render_from_transforms(file_path=file_path, transforms_json_path=transforms_json_path, output_path=output_path) |