#!/usr/bin/env python3 # 20260228 ChatGPT # $Header$ # $Id$ # # Usage: # blender -b -P render_stl_variants.py -- /path/to/part.stl /path/to/out_prefix # # Output: # _solid.png # _solid_edges.png # _wire.png # _matcap.png # _freestyle.png # # Notes: # - Uses orthographic camera + fit-to-bounds framing (consistent prints). # - Workbench engine for fast technical-looking renders. # - Freestyle enabled only for the freestyle variant. import bpy import sys import math from mathutils import Vector def die(msg): print(msg, file=sys.stderr) raise SystemExit(1) def argv_after_double_dash(): if "--" not in sys.argv: return [] return sys.argv[sys.argv.index("--") + 1:] def reset_scene(): bpy.ops.wm.read_factory_settings(use_empty=True) scene = bpy.context.scene scene.unit_settings.system = 'NONE' scene.render.resolution_x = 1200 scene.render.resolution_y = 1200 scene.render.film_transparent = False scene.render.image_settings.file_format = 'PNG' return scene def import_stl(path): bpy.ops.import_mesh.stl(filepath=path) objs = [o for o in bpy.context.selected_objects if o.type == 'MESH'] if not objs: die(f"Failed to import STL: {path}") # join into one object for consistent bounds bpy.context.view_layer.objects.active = objs[0] for o in objs: o.select_set(True) bpy.ops.object.join() obj = bpy.context.view_layer.objects.active obj.name = "PART" return obj def center_origin_and_location(obj): bpy.context.view_layer.objects.active = obj obj.select_set(True) bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') obj.location = (0, 0, 0) def bounds_world(obj): # bounding box corners in world coordinates mw = obj.matrix_world corners = [mw @ Vector(c) for c in obj.bound_box] xs = [c.x for c in corners]; ys = [c.y for c in corners]; zs = [c.z for c in corners] min_v = Vector((min(xs), min(ys), min(zs))) max_v = Vector((max(xs), max(ys), max(zs))) size = max_v - min_v center = (min_v + max_v) / 2.0 return center, size def ensure_camera(scene): cam_data = bpy.data.cameras.new("OrthoCam") cam_data.type = 'ORTHO' cam = bpy.data.objects.new("OrthoCam", cam_data) scene.collection.objects.link(cam) scene.camera = cam return cam def ensure_light_rig(scene): # For workbench it barely matters, but keep a light for non-workbench fallback ld = bpy.data.lights.new("Key", type='AREA') lo = bpy.data.objects.new("Key", ld) lo.location = (3, -3, 3) scene.collection.objects.link(lo) def set_isometric_camera(cam, target=(0,0,0), ortho_scale=2.0): cam.data.ortho_scale = ortho_scale cam.location = (3.0, -3.0, 3.0) cam.rotation_euler = (math.radians(55), 0, math.radians(45)) def fit_camera_to_object(cam, obj, margin=1.15): center, size = bounds_world(obj) max_dim = max(size.x, size.y, size.z) # ortho_scale roughly maps to view width; margin gives padding cam.data.ortho_scale = max_dim * margin def set_world_background(scene, gray=1.0): # Simple white/gray background via World color if scene.world is None: scene.world = bpy.data.worlds.new("World") scene.world.use_nodes = False scene.world.color = (gray, gray, gray) def workbench_common(scene): scene.render.engine = 'BLENDER_WORKBENCH' scene.display.shading.light = 'STUDIO' scene.display.shading.color_type = 'SINGLE' scene.display.shading.single_color = (0.82, 0.82, 0.82) # light gray “clay” scene.display.shading.show_backface_culling = False scene.display.shading.show_xray = False def variant_solid(scene): workbench_common(scene) scene.display.shading.show_wireframes = False def variant_wire(scene): workbench_common(scene) scene.display.shading.show_wireframes = True scene.display.shading.wireframe_color_type = 'SINGLE' scene.display.shading.wireframe_color = (0.0, 0.0, 0.0) scene.display.shading.wireframe_opacity = 1.0 scene.display.shading.wireframe_thickness = 1 def variant_solid_edges(scene): # Workbench wire overlay on top of solid workbench_common(scene) scene.display.shading.show_wireframes = True scene.display.shading.wireframe_color_type = 'SINGLE' scene.display.shading.wireframe_color = (0.0, 0.0, 0.0) scene.display.shading.wireframe_opacity = 1.0 scene.display.shading.wireframe_thickness = 1 def variant_matcap(scene): # Workbench “Matcap-like” studio + cavity (helps valleys/holes read on paper) workbench_common(scene) scene.display.shading.show_wireframes = False scene.display.shading.show_cavity = True scene.display.shading.cavity_type = 'WORLD' scene.display.shading.cavity_ridge_factor = 1.0 scene.display.shading.cavity_valley_factor = 1.0 def variant_freestyle(scene): # Freestyle line drawing + light solid base # Works with Workbench in many cases, but Freestyle is historically tied to Eevee/Cycles. # Here we switch to Eevee for reliability. scene.render.engine = 'BLENDER_EEVEE' scene.eevee.use_gtao = True scene.render.film_transparent = False # Setup simple material mat = bpy.data.materials.new("Clay") mat.use_nodes = True bsdf = mat.node_tree.nodes.get("Principled BSDF") bsdf.inputs["Base Color"].default_value = (0.85, 0.85, 0.85, 1.0) bsdf.inputs["Roughness"].default_value = 1.0 obj = bpy.data.objects.get("PART") if obj and obj.data.materials: obj.data.materials[0] = mat elif obj: obj.data.materials.append(mat) # Enable Freestyle scene.render.use_freestyle = True # Default freestyle line set is OK; keep lines black and modest thickness if scene.view_layers: vl = scene.view_layers[0] vl.freestyle_settings.linesets.new("LineSet") if not vl.freestyle_settings.linesets else None # Render Properties -> Freestyle settings are somewhat verbose to tune; default is acceptable for v1. def render(scene, outpath): scene.render.filepath = outpath bpy.ops.render.render(write_still=True) def main(): args = argv_after_double_dash() if len(args) != 2: die("Expected: ") in_stl, out_prefix = args scene = reset_scene() obj = import_stl(in_stl) center_origin_and_location(obj) ensure_light_rig(scene) cam = ensure_camera(scene) set_isometric_camera(cam) fit_camera_to_object(cam, obj, margin=1.20) set_world_background(scene, gray=1.0) # SOLID variant_solid(scene) scene.render.use_freestyle = False render(scene, f"{out_prefix}_solid.png") # SOLID + EDGES variant_solid_edges(scene) scene.render.use_freestyle = False render(scene, f"{out_prefix}_solid_edges.png") # WIRE variant_wire(scene) scene.render.use_freestyle = False render(scene, f"{out_prefix}_wire.png") # MATCAP-ish (cavity) variant_matcap(scene) scene.render.use_freestyle = False render(scene, f"{out_prefix}_matcap.png") # FREESTYLE variant_freestyle(scene) render(scene, f"{out_prefix}_freestyle.png") print("Done.") if __name__ == "__main__": main()