Skip to content

Instantly share code, notes, and snippets.

@pndurette
Last active April 1, 2023 08:55
Show Gist options
  • Save pndurette/42f292717e961a24458b5f37fcb39c06 to your computer and use it in GitHub Desktop.
Save pndurette/42f292717e961a24458b5f37fcb39c06 to your computer and use it in GitHub Desktop.
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