Files

255 lines
9.1 KiB
Python

import bpy
import sys
import argparse
import math
import os
def reset_scene():
bpy.ops.wm.read_factory_settings(use_empty=True)
def configure_gpu(rendering):
if rendering == 'GPU':
bpy.context.scene.cycles.device = 'GPU'
prefs = bpy.context.preferences
cprefs = prefs.addons['cycles'].preferences
for compute_device_type in ('OPTIX', 'CUDA', 'HIP', 'METAL'):
try:
cprefs.compute_device_type = compute_device_type
cprefs.get_devices()
devices = cprefs.devices
if devices:
print(f"Blender: Found {compute_device_type} devices:")
for device in devices:
device.use = True
print(f" - Activated: {device.name}")
return
except Exception as e:
continue
print("Blender: No GPU found, falling back to CPU.")
bpy.context.scene.cycles.device = 'CPU'
else:
bpy.context.scene.cycles.device = 'CPU'
def set_camera(elev, azim, distance, scale, clip_start=0.1, clip_end=100):
# Location calculation
elev_rad = math.radians(elev)
azim_rad = math.radians(azim - 90)
x = distance * math.cos(elev_rad) * math.cos(azim_rad)
y = distance * math.cos(elev_rad) * math.sin(azim_rad)
z = distance * math.sin(elev_rad)
camera_data = bpy.data.cameras.new(name='Camera')
# Orthographic settings
camera_data.type = 'ORTHO'
camera_data.ortho_scale = scale
camera_data.clip_start = clip_start
camera_data.clip_end = clip_end
camera_object = bpy.data.objects.new('Camera', camera_data)
bpy.context.scene.collection.objects.link(camera_object)
bpy.context.scene.camera = camera_object
camera_object.location = (x, y, z)
# Point camera at origin using a Track To constraint
bpy.ops.object.empty_add(location=(0, 0, 0))
target = bpy.context.active_object
constraint = camera_object.constraints.new(type='TRACK_TO')
constraint.target = target
constraint.track_axis = 'TRACK_NEGATIVE_Z'
constraint.up_axis = 'UP_Y'
def setup_lighting():
# Ensure a World data block exists (fix for factory settings)
if bpy.context.scene.world is None:
new_world = bpy.data.worlds.new("New_Render_World")
bpy.context.scene.world = new_world
world = bpy.context.scene.world
world.use_nodes = True
# Get or create the Background node
if 'Background' not in world.node_tree.nodes:
world.node_tree.nodes.new('ShaderNodeBackground')
output_node = world.node_tree.nodes['World Output']
bg_node = world.node_tree.nodes['Background']
world.node_tree.links.new(bg_node.outputs['Background'], output_node.inputs['Surface'])
bg_node = world.node_tree.nodes['Background']
bg_node.inputs['Color'].default_value = (1.0, 1.0, 1.0, 1.0)
bg_node.inputs['Strength'].default_value = 1.0
bpy.context.scene.view_settings.view_transform = 'Standard'
bpy.context.scene.cycles.max_bounces = 8
bpy.context.scene.cycles.diffuse_bounces = 4 # How many times light reflects off diffuse surfaces
def auto_center_and_scale(obj, norm_size):
"""
Centers the mesh object's geometry at the origin and scales it such that
(Max Radius * 2.0) equals norm_size.
"""
if obj.type != 'MESH':
print(f"Object {obj.name} is not a mesh. Skipping transformation.")
return
# Ensure we are in Object Mode before modifying geometry
if bpy.context.object and bpy.context.object.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
# Get the bounding box coordinates (min/max for X, Y, Z)
# The bounding box is an 8-tuple of (x,y,z) coordinates.
# The Bounding Box is in local coordinates when the object's scale is (1,1,1)
bbox = obj.bound_box
# Find min and max coordinates
min_x = min(v[0] for v in bbox)
max_x = max(v[0] for v in bbox)
min_y = min(v[1] for v in bbox)
max_y = max(v[1] for v in bbox)
min_z = min(v[2] for v in bbox)
max_z = max(v[2] for v in bbox)
# Calculate Center (equivalent to your (max_bb + min_bb) / 2)
center_x = (min_x + max_x) / 2
center_y = (min_y + max_y) / 2
center_z = (min_z + max_z) / 2
center = (center_x, center_y, center_z)
# 1. Translate Geometry (Equivalent to vtx_pos = (vtx_pos - center))
# This moves the geometry relative to the object's local origin.
# The context must be set correctly for bpy.ops.transform.translate to work.
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
# Enter Edit Mode
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
# Translate vertices by -center vector
bpy.ops.transform.translate(value=(-center_x, -center_y, -center_z))
# Return to Object Mode
bpy.ops.object.mode_set(mode='OBJECT')
# The object's geometry is now centered on the local origin (0,0,0).
# We ensure the object's world location is also (0,0,0)
obj.location = (0, 0, 0)
# 2. Calculate Maximum Radius (for scaling)
current_max_radius_sq = 0.0
mesh = obj.data
# Iterate over vertices to find the farthest one from the origin
# The vertex coordinates are now relative to the center
for vertex in mesh.vertices:
if vertex.co.length_squared > current_max_radius_sq:
current_max_radius_sq = vertex.co.length_squared
current_max_radius = math.sqrt(current_max_radius_sq)
if current_max_radius < 1e-6:
print("Skipping scaling: mesh has negligible size.")
return
# 3. Calculate and Apply Scale
# User's scaling basis: scale_user = current_max_radius * 2.0
scale_user = current_max_radius * 2.0
# Target scale factor: (scale_factor / scale_user)
scale_factor_needed = norm_size / scale_user
# Apply scale to the object
obj.scale = (scale_factor_needed, scale_factor_needed, scale_factor_needed)
# 4. Apply transformation to bake scale into geometry (optional but good practice)
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
print(f"Mesh '{obj.name}' centered. Scaled by {scale_factor_needed:.4f}.")
print(f"Final normalized size (Max Radius * 2.0) is approx {norm_size:.4f}.")
def import_mesh(mesh_path):
if not os.path.exists(mesh_path):
print(f"Error: Mesh file not found at {mesh_path}")
sys.exit(1)
if mesh_path.endswith('.obj'):
if hasattr(bpy.ops.wm, 'obj_import'):
bpy.ops.wm.obj_import(filepath=mesh_path)
else:
bpy.ops.import_scene.obj(filepath=mesh_path)
elif mesh_path.endswith('.glb') or mesh_path.endswith('.gltf'):
bpy.ops.import_scene.gltf(filepath=mesh_path)
else:
print(f"Unsupported mesh format: {mesh_path}")
sys.exit(1)
# Return the first imported mesh object
return bpy.context.view_layer.objects.active
def render_scene(output_path, resolution, rendering):
abs_output_path = os.path.abspath(output_path)
output_dir = os.path.dirname(abs_output_path)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir, exist_ok=True)
bpy.context.scene.render.engine = 'CYCLES'
configure_gpu(rendering)
bpy.context.scene.render.resolution_x = resolution
bpy.context.scene.render.resolution_y = resolution
bpy.context.scene.render.image_settings.file_format = 'PNG'
bpy.context.scene.render.filepath = abs_output_path
bpy.context.scene.render.use_file_extension = False
bpy.context.scene.cycles.samples = 512
bpy.context.scene.cycles.preview_samples = 32
bpy.context.scene.cycles.use_denoising = True
print(f"Rendering to: {abs_output_path}")
bpy.ops.render.render(write_still=True)
def main():
argv = sys.argv
if "--" not in argv:
argv = []
else:
argv = argv[argv.index("--") + 1:]
parser = argparse.ArgumentParser()
parser.add_argument('--mesh', required=True)
parser.add_argument('--output', required=True)
parser.add_argument('--elev', type=float, required=True)
parser.add_argument('--azim', type=float, required=True)
parser.add_argument('--distance', type=float, default=1.45)
parser.add_argument('--scale', type=float, default=2.0, help="Orthographic scale (field of view size)")
# New argument for normalization size
parser.add_argument('--norm_size', type=float, default=1.15, help="Target size for Max Radius * 2.0")
parser.add_argument('--resolution', type=int, default=1024)
parser.add_argument('--rendering', default="GPU")
args = parser.parse_args(argv)
reset_scene()
# Import and get the mesh object
mesh_obj = import_mesh(args.mesh)
# Auto-center and scale the mesh
if mesh_obj:
auto_center_and_scale(mesh_obj, args.norm_size)
# Set up the camera and lighting
set_camera(args.elev, args.azim, args.distance, args.scale)
setup_lighting()
render_scene(args.output, args.resolution, args.rendering)
if __name__ == "__main__":
main()