|
import bpy |
|
import math |
|
|
|
# ========================= |
|
# USER SETTINGS |
|
# ========================= |
|
OBJ_PATH = "/absolute/path/to/toolpath.obj" |
|
|
|
NOZZLE_DIAMETER_MM = 0.4 |
|
MM_TO_M = 0.001 |
|
|
|
FPS = 30 |
|
DURATION_SECONDS = 8 |
|
FRAMES = FPS * DURATION_SECONDS |
|
|
|
OUTPUT_PATH = "//print_simulation.webm" |
|
|
|
# ========================= |
|
# CLEAN SCENE |
|
# ========================= |
|
bpy.ops.object.select_all(action='SELECT') |
|
bpy.ops.object.delete() |
|
|
|
scene = bpy.context.scene |
|
scene.unit_settings.system = 'METRIC' |
|
scene.unit_settings.scale_length = 1.0 |
|
|
|
# ========================= |
|
# IMPORT OBJ |
|
# ========================= |
|
bpy.ops.import_scene.obj(filepath=OBJ_PATH) |
|
imported = bpy.context.selected_objects |
|
|
|
for obj in imported: |
|
obj.scale = (MM_TO_M, MM_TO_M, MM_TO_M) |
|
|
|
bpy.ops.object.transform_apply(scale=True) |
|
|
|
# ========================= |
|
# CONVERT TO CURVES |
|
# ========================= |
|
for obj in imported: |
|
bpy.context.view_layer.objects.active = obj |
|
bpy.ops.object.convert(target='CURVE') |
|
|
|
# ========================= |
|
# NOZZLE PROFILE |
|
# ========================= |
|
bpy.ops.curve.primitive_bezier_circle_add( |
|
radius=(NOZZLE_DIAMETER_MM * MM_TO_M) / 2 |
|
) |
|
nozzle = bpy.context.active_object |
|
nozzle.name = "NozzleProfile" |
|
|
|
# ========================= |
|
# BEVEL CURVES → FILAMENT |
|
# ========================= |
|
for obj in bpy.context.scene.objects: |
|
if obj.type == 'CURVE' and obj != nozzle: |
|
obj.data.bevel_mode = 'OBJECT' |
|
obj.data.bevel_object = nozzle |
|
obj.data.fill_mode = 'FULL' |
|
|
|
# ========================= |
|
# CONVERT TO SINGLE MESH |
|
# ========================= |
|
filament_objs = [o for o in bpy.context.scene.objects if o.type == 'CURVE' and o != nozzle] |
|
|
|
for obj in filament_objs: |
|
bpy.context.view_layer.objects.active = obj |
|
bpy.ops.object.convert(target='MESH') |
|
|
|
bpy.ops.object.select_all(action='DESELECT') |
|
for obj in bpy.context.scene.objects: |
|
if obj.type == 'MESH': |
|
obj.select_set(True) |
|
|
|
bpy.context.view_layer.objects.active = bpy.context.selected_objects[0] |
|
bpy.ops.object.join() |
|
|
|
print_obj = bpy.context.active_object |
|
print_obj.name = "Print" |
|
|
|
# ========================= |
|
# FILAMENT MATERIAL |
|
# ========================= |
|
mat = bpy.data.materials.new("Filament") |
|
mat.use_nodes = True |
|
bsdf = mat.node_tree.nodes["Principled BSDF"] |
|
bsdf.inputs["Base Color"].default_value = (0.9, 0.3, 0.1, 1) |
|
bsdf.inputs["Roughness"].default_value = 0.55 |
|
bsdf.inputs["Specular"].default_value = 0.3 |
|
print_obj.data.materials.append(mat) |
|
|
|
# ========================= |
|
# CREATE CUTTER (NEGATIVE PRISM) |
|
# ========================= |
|
bpy.ops.mesh.primitive_cube_add(size=1) |
|
cutter = bpy.context.active_object |
|
cutter.name = "Cutter" |
|
|
|
# Fit cutter to print bounds |
|
bbox = [print_obj.matrix_world @ v.co for v in print_obj.data.vertices] |
|
xs = [v.x for v in bbox] |
|
ys = [v.y for v in bbox] |
|
zs = [v.z for v in bbox] |
|
|
|
cutter.scale.x = (max(xs) - min(xs)) * 0.6 |
|
cutter.scale.y = (max(ys) - min(ys)) * 0.6 |
|
cutter.scale.z = (max(zs) - min(zs)) * 0.6 |
|
|
|
min_z = min(zs) - cutter.scale.z |
|
max_z = max(zs) + 0.001 |
|
|
|
cutter.location = (0, 0, min_z) |
|
|
|
# ========================= |
|
# BOOLEAN MODIFIER |
|
# ========================= |
|
mod = print_obj.modifiers.new(name="Reveal", type='BOOLEAN') |
|
mod.operation = 'DIFFERENCE' |
|
mod.object = cutter |
|
mod.solver = 'FAST' |
|
|
|
# ========================= |
|
# ANIMATE CUTTER (LINEAR!) |
|
# ========================= |
|
scene.frame_start = 1 |
|
scene.frame_end = FRAMES |
|
|
|
cutter.location.z = min_z |
|
cutter.keyframe_insert("location", frame=1) |
|
|
|
cutter.location.z = max_z |
|
cutter.keyframe_insert("location", frame=FRAMES) |
|
|
|
action = cutter.animation_data.action |
|
for fcurve in action.fcurves: |
|
for kp in fcurve.keyframe_points: |
|
kp.interpolation = 'LINEAR' |
|
|
|
# ========================= |
|
# CAMERA |
|
# ========================= |
|
bpy.ops.object.camera_add(location=(0.4, -0.4, 0.35)) |
|
cam = bpy.context.active_object |
|
cam.rotation_euler = (1.1, 0, 0.8) |
|
cam.data.lens = 50 |
|
scene.camera = cam |
|
|
|
# ========================= |
|
# LIGHTING |
|
# ========================= |
|
bpy.ops.object.light_add(type='AREA', location=(0.3, -0.3, 0.4)) |
|
key = bpy.context.active_object |
|
key.data.energy = 1200 |
|
key.data.size = 0.25 |
|
|
|
bpy.ops.object.light_add(type='AREA', location=(-0.3, -0.2, 0.3)) |
|
fill = bpy.context.active_object |
|
fill.data.energy = 500 |
|
fill.data.size = 0.35 |
|
fill.data.use_shadow = False |
|
|
|
bpy.ops.object.light_add(type='AREA', location=(0.0, 0.4, 0.35)) |
|
rim = bpy.context.active_object |
|
rim.data.energy = 800 |
|
rim.data.size = 0.15 |
|
|
|
# ========================= |
|
# BACKGROUND |
|
# ========================= |
|
world = scene.world |
|
world.use_nodes = True |
|
bg = world.node_tree.nodes["Background"] |
|
bg.inputs[0].default_value = (0.15, 0.15, 0.15, 1) |
|
bg.inputs[1].default_value = 1.0 |
|
|
|
# ========================= |
|
# RENDER SETTINGS (WEBM) |
|
# ========================= |
|
scene.render.engine = 'BLENDER_EEVEE' |
|
scene.render.fps = FPS |
|
scene.render.use_motion_blur = False |
|
|
|
scene.render.image_settings.file_format = 'FFMPEG' |
|
scene.render.ffmpeg.format = 'WEBM' |
|
scene.render.ffmpeg.codec = 'VP9' |
|
scene.render.ffmpeg.constant_rate_factor = 'MEDIUM' |
|
scene.render.filepath = OUTPUT_PATH |
|
|
|
# ========================= |
|
# RENDER |
|
# ========================= |
|
bpy.ops.render.render(animation=True) |