"""Render six inspection views of an STL using Blender. Usage: blender --background --python tools/render_stl_previews.py -- \ input.stl output-directory basename [comma-separated-views] """ import os import sys import bpy from mathutils import Vector def arguments(): args = sys.argv[sys.argv.index("--") + 1 :] if len(args) not in (3, 4): raise SystemExit( "expected: input.stl output-directory basename [comma-separated-views]" ) requested_views = args[3].split(",") if len(args) == 4 else None return args[0], args[1], args[2], requested_views def look_at(camera, target): direction = Vector(target) - camera.location camera.rotation_euler = direction.to_track_quat("-Z", "Y").to_euler() def render_view(camera, center, span, name, direction, output_dir, basename): distance = span * 2.2 camera.location = Vector(center) + Vector(direction).normalized() * distance look_at(camera, center) camera.data.type = "ORTHO" camera.data.ortho_scale = span * 1.35 bpy.context.scene.render.filepath = os.path.join( output_dir, f"{basename}_{name}.png" ) bpy.ops.render.render(write_still=True) def main(): input_path, output_dir, basename, requested_views = arguments() os.makedirs(output_dir, exist_ok=True) bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.wm.stl_import(filepath=os.path.abspath(input_path)) model = bpy.context.selected_objects[0] model.name = "Feather case" material = bpy.data.materials.new("Case material") material.diffuse_color = (0.16, 0.42, 0.72, 1.0) material.metallic = 0.0 material.roughness = 0.45 model.data.materials.append(material) corners = [model.matrix_world @ Vector(corner) for corner in model.bound_box] mins = Vector([min(v[i] for v in corners) for i in range(3)]) maxs = Vector([max(v[i] for v in corners) for i in range(3)]) center = (mins + maxs) / 2 span = max(maxs - mins) world = bpy.data.worlds.new("Inspection world") world.use_nodes = True world.node_tree.nodes["Background"].inputs["Color"].default_value = ( 0.035, 0.04, 0.05, 1, ) world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.5 bpy.context.scene.world = world camera_data = bpy.data.cameras.new("Camera") camera = bpy.data.objects.new("Camera", camera_data) bpy.context.collection.objects.link(camera) bpy.context.scene.camera = camera key_data = bpy.data.lights.new("Key", type="AREA") key_data.energy = 900 key_data.shape = "DISK" key_data.size = span key = bpy.data.objects.new("Key", key_data) key.location = center + Vector((span, -span, span * 1.5)) bpy.context.collection.objects.link(key) fill_data = bpy.data.lights.new("Fill", type="AREA") fill_data.energy = 500 fill_data.size = span fill = bpy.data.objects.new("Fill", fill_data) fill.location = center + Vector((-span, span, span)) bpy.context.collection.objects.link(fill) scene = bpy.context.scene scene.render.engine = "BLENDER_WORKBENCH" scene.render.resolution_x = 800 scene.render.resolution_y = 600 scene.render.resolution_percentage = 100 scene.render.image_settings.file_format = "PNG" scene.render.film_transparent = False scene.display.shading.light = "STUDIO" scene.display.shading.color_type = "MATERIAL" scene.display.shading.show_shadows = True scene.display.shading.show_cavity = True scene.display.shading.cavity_type = "BOTH" scene.display.shading.curvature_ridge_factor = 1.5 scene.display.shading.curvature_valley_factor = 1.2 views = { "iso": (1, -1, 0.8), "front": (-1, 0, 0), "back": (1, 0, 0), "left": (0, -1, 0), "right": (0, 1, 0), "top": (0, 0, 1), } selected = requested_views or list(views) unknown = sorted(set(selected) - set(views)) if unknown: raise SystemExit(f"unknown views: {', '.join(unknown)}") for name in selected: direction = views[name] render_view(camera, center, span, name, direction, output_dir, basename) if __name__ == "__main__": main()