Last active
April 1, 2023 08:55
-
-
Save pndurette/42f292717e961a24458b5f37fcb39c06 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import math | |
import os | |
import sys | |
# Blender imports | |
import bpy | |
from mathutils import Vector | |
""" | |
STL to rotated images sequence | |
This scripts loads an STL file and renders it from 36 different angles. The frames are saved as | |
PNG images in the specified output folder. | |
Requirements: | |
- Blender 2.8 or higher | |
- Python 3.6 or higher | |
Usage: | |
blender --background --python render_script.py -- <input_filepath> <output_folder> [--rotate-x-90] | |
To create a gif from the rendered frames, use the following command (requires ImageMagick): | |
convert -delay 10 -loop 0 <output_folder>/*.png <output_folder>/output.gif | |
""" | |
# Set the background color to a light blue | |
BACKGROUND_COLOR = (0.5, 0.7, 1) | |
# Argument parser | |
# "argparse" conflicts with Blender's argument parser | |
# Find the index of the double-dash argument separator | |
arg_separator_idx = sys.argv.index("--") | |
# Get the script's command-line arguments | |
script_args = sys.argv[arg_separator_idx + 1 :] | |
# Extract the input_filepath, output_folder, and rotate_x_90 flag | |
input_filepath = script_args[0] | |
output_folder = script_args[1] | |
rotate_x_90 = "--rotate-x-90" in script_args | |
def delete_all_mesh_objects(): | |
"""Delete all mesh objects in the scene""" | |
bpy.ops.object.select_all(action="DESELECT") | |
bpy.ops.object.select_by_type(type="MESH") | |
bpy.ops.object.delete() | |
def delete_all_cameras(): | |
"""Delete all cameras in the scene""" | |
bpy.ops.object.select_all(action="DESELECT") | |
bpy.ops.object.select_by_type(type="CAMERA") | |
bpy.ops.object.delete() | |
def delete_all_lights(): | |
"""Delete all lights in the scene""" | |
bpy.ops.object.select_all(action="DESELECT") | |
bpy.ops.object.select_by_type(type="LIGHT") | |
bpy.ops.object.delete() | |
def import_stl(filepath, rotate_x_90=False): | |
"""Import an STL file and return the object | |
Args: | |
filepath: The path to the STL file | |
rotate_x_90: Whether to rotate the object by 90 degrees around the X-axis | |
Returns: | |
The imported object | |
""" | |
bpy.ops.import_mesh.stl(filepath=filepath) | |
obj = bpy.context.selected_objects[0] | |
if rotate_x_90: | |
obj.rotation_euler.x = math.radians(90) | |
return obj | |
def calculate_bounding_box_diagonal(obj): | |
"""Calculate the diagonal of the bounding box of the object""" | |
bbox = [obj.matrix_world @ Vector(v) for v in obj.bound_box] | |
min_corner = [min([coord[i] for coord in bbox]) for i in range(3)] | |
max_corner = [max([coord[i] for coord in bbox]) for i in range(3)] | |
diagonal = [(max_corner[i] - min_corner[i]) for i in range(3)] | |
return (diagonal[0] ** 2 + diagonal[1] ** 2 + diagonal[2] ** 2) ** 0.5 | |
def setup_camera(obj): | |
"""Set up the camera to view the object""" | |
bbox_diagonal = calculate_bounding_box_diagonal(obj) | |
camera_distance = bbox_diagonal * 1.5 | |
camera_elevation = camera_distance * 0.5 | |
# Place the camera at the specified distance and elevation | |
camera_location = Vector((0, -camera_distance, camera_elevation)) | |
bpy.ops.object.camera_add(location=camera_location) | |
camera = bpy.context.active_object | |
# Point the camera at the object's center | |
direction = -camera_location.normalized() | |
camera.rotation_euler = direction.to_track_quat("-Z", "Y").to_euler() | |
# Set the camera as the active camera | |
bpy.context.scene.camera = camera | |
return camera | |
def setup_light(obj): | |
"""Set up the light to illuminate the object""" | |
bbox_diagonal = calculate_bounding_box_diagonal(obj) | |
light_distance = bbox_diagonal * 2 | |
light_elevation = light_distance * 0.75 | |
# Place the light at the specified distance and elevation | |
bpy.ops.object.light_add(type="SUN", location=(0, -light_distance, light_elevation)) | |
light = bpy.context.active_object | |
light.rotation_euler = (math.pi / 2, 0, 0) | |
# Reduce the light strength | |
light.data.energy = 1 | |
def setup_background(camera, obj, color=(0.5, 0.7, 1)): | |
"""Set up the background plane to be a solid color | |
Args: | |
camera: The camera object | |
obj: The object to be rendered | |
color: The background color as an RGB tuple | |
""" | |
# Calculate the diagonal of the object's bounding box | |
bb_diag = calculate_bounding_box_diagonal(obj) | |
# Calculate the distance for the background plane | |
distance = bb_diag * 2 | |
# Create a large plane | |
bpy.ops.mesh.primitive_plane_add(size=1000) | |
bg_plane = bpy.context.active_object | |
# Position the plane behind the object based on the camera's location and rotation | |
bg_plane.location = camera.location + camera.matrix_world.to_quaternion() @ Vector( | |
(0, 0, -distance) | |
) | |
bg_plane.rotation_euler = camera.rotation_euler | |
# Create a new material for the plane and set the color | |
bg_mat = bpy.data.materials.new("BackgroundMaterial") | |
bg_mat.use_nodes = True | |
bg_node_tree = bg_mat.node_tree | |
principled_bsdf_node = bg_node_tree.nodes["Principled BSDF"] | |
principled_bsdf_node.inputs["Base Color"].default_value = color + (1,) | |
# Disable shadows and reflections for the background plane | |
principled_bsdf_node.inputs["Specular"].default_value = 0 | |
principled_bsdf_node.inputs["Roughness"].default_value = 1 | |
bg_mat.shadow_method = "NONE" | |
# Assign the material to the plane | |
bg_plane.data.materials.append(bg_mat) | |
# Clear the scene | |
delete_all_mesh_objects() | |
delete_all_cameras() | |
delete_all_lights() | |
# Import the STL file | |
obj = import_stl(filepath=input_filepath, rotate_x_90=rotate_x_90) | |
# Center the object | |
bpy.ops.object.origin_set(type="ORIGIN_CENTER_OF_MASS", center="BOUNDS") | |
obj.location = (0, 0, 0) | |
# Set up the camera | |
camera = setup_camera(obj) | |
# Set up the background | |
setup_background(camera, obj, color=BACKGROUND_COLOR) | |
# Set up the light | |
setup_light(obj) | |
# Render settings | |
bpy.context.scene.render.image_settings.file_format = "PNG" | |
bpy.context.scene.render.resolution_x = 500 | |
bpy.context.scene.render.resolution_y = 500 | |
bpy.context.scene.render.resolution_percentage = 100 | |
# Render the frames and save to output directory | |
for frame in range(0, 36): | |
# Rotate the object by 10 degrees around the Z-axis | |
angle = 2 * math.pi * frame / 36 | |
obj.rotation_euler.z = angle | |
# Render the frame | |
bpy.context.scene.render.filepath = os.path.join( | |
output_folder, f"frame_{frame:03d}.png" | |
) | |
bpy.ops.render.render(write_still=True) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment