"""Render seven validation views of a generated 3MF with Blender. Usage: blender --background --python script/render_previews.py -- model.3mf output_dir """ import math import os import sys import zipfile import xml.etree.ElementTree as ET import bpy from mathutils import Vector def args(): marker = sys.argv.index("--") return os.path.abspath(sys.argv[marker + 1]), os.path.abspath(sys.argv[marker + 2]) source, output = args() os.makedirs(output, exist_ok=True) bpy.ops.object.select_all(action="SELECT") bpy.ops.object.delete(use_global=False) def import_3mf(path): with zipfile.ZipFile(path) as package: root = ET.fromstring(package.read("3D/3dmodel.model")) namespace = {"m": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02"} materials = [] bases = root.find("m:resources/m:basematerials", namespace) if bases is not None: for base in bases: color = base.attrib.get("displaycolor", "#B0B0B0").lstrip("#")[:6] rgba = tuple(int(color[i:i+2], 16) / 255 for i in (0, 2, 4)) + (1.0,) material = bpy.data.materials.new(base.attrib.get("name", "material")) material.diffuse_color = rgba materials.append(material) for element in root.findall("m:resources/m:object", namespace): mesh_node = element.find("m:mesh", namespace) if mesh_node is None: continue vertices = [(float(v.attrib["x"]), float(v.attrib["y"]), float(v.attrib["z"])) for v in mesh_node.findall("m:vertices/m:vertex", namespace)] faces = [(int(t.attrib["v1"]), int(t.attrib["v2"]), int(t.attrib["v3"])) for t in mesh_node.findall("m:triangles/m:triangle", namespace)] data = bpy.data.meshes.new(element.attrib.get("name", "mesh")) data.from_pydata(vertices, [], faces) data.update() obj = bpy.data.objects.new(element.attrib.get("name", "object"), data) bpy.context.collection.objects.link(obj) if materials and "pindex" in element.attrib: obj.data.materials.append(materials[int(element.attrib["pindex"])]) import_3mf(source) objects = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"] if not objects: raise RuntimeError(f"No mesh objects imported from {source}") corners = [] for obj in objects: corners.extend(obj.matrix_world @ Vector(corner) for corner in obj.bound_box) minimum = Vector((min(p.x for p in corners), min(p.y for p in corners), min(p.z for p in corners))) maximum = Vector((max(p.x for p in corners), max(p.y for p in corners), max(p.z for p in corners))) center = (minimum + maximum) / 2 extent = max(maximum - minimum) world = bpy.context.scene.world world.color = (0.035, 0.035, 0.035) bpy.context.scene.render.engine = "BLENDER_EEVEE" bpy.context.scene.render.resolution_x = 700 bpy.context.scene.render.resolution_y = 700 bpy.context.scene.render.resolution_percentage = 100 bpy.context.scene.render.image_settings.file_format = "PNG" bpy.context.scene.render.film_transparent = False camera_data = bpy.data.cameras.new("Validation camera") camera = bpy.data.objects.new("Validation camera", camera_data) bpy.context.collection.objects.link(camera) bpy.context.scene.camera = camera camera.data.type = "ORTHO" camera.data.ortho_scale = extent * 1.25 for energy, direction in [(1800, Vector((1, -1, 2))), (900, Vector((-2, 1, 0.5)) )]: light_data = bpy.data.lights.new(name="Area", type="AREA") light_data.energy = energy light_data.shape = "DISK" light_data.size = extent light = bpy.data.objects.new(name="Area", object_data=light_data) light.location = center + direction.normalized() * extent * 2 bpy.context.collection.objects.link(light) views = { "front": Vector((0, -1, 0)), "back": Vector((0, 1, 0)), "left": Vector((-1, 0, 0)), "right": Vector((1, 0, 0)), "top": Vector((0, 0, 1)), "bottom": Vector((0, 0, -1)), "iso": Vector((1, -1, 0.8)).normalized(), } for name, direction in views.items(): camera.location = center + direction * extent * 3 camera.rotation_euler = (center - camera.location).to_track_quat("-Z", "Y").to_euler() bpy.context.scene.render.filepath = os.path.join(output, f"{name}.png") bpy.ops.render.render(write_still=True) print(f"Rendered {len(views)} views to {output}")