Files
Aero-Ex-ComfyUI-SegviGen/data_toolkit/bpy_render.py
T
2026-03-18 23:15:53 +05:30

261 lines
11 KiB
Python

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)