365 lines
9.6 KiB
Python
365 lines
9.6 KiB
Python
import bpy
|
|
import mathutils
|
|
|
|
# ============================================================
|
|
# CONFIG
|
|
# ============================================================
|
|
|
|
robot = {
|
|
"vision_config":{
|
|
"MarkerType":"DICT_4X4_250",
|
|
"MarkerSize":0.025
|
|
},
|
|
"renderingInfo":{
|
|
"cameraPosition":[-400, -700, 300],
|
|
"cameraTarget":["x", 0, 0],
|
|
"cameraUpVector":[0, 0, 1],
|
|
"lightPosition":[-500, -500, 500],
|
|
"lightTarget":[0, 0, 0],
|
|
"lightUpVector":[0, 0, 1],
|
|
"metric": "mm",
|
|
"materials":{
|
|
"wood":{
|
|
"baseColor":[0.72,0.52,0.33],
|
|
"roughness":0.8,
|
|
"metallic":0.0
|
|
},
|
|
|
|
"plaWhite":{
|
|
"baseColor":[0.95,0.95,0.95],
|
|
"roughness":0.45,
|
|
"metallic":0.0
|
|
},
|
|
|
|
"steel":{
|
|
"baseColor":[0.7,0.7,0.72],
|
|
"roughness":0.25,
|
|
"metallic":1.0
|
|
},
|
|
|
|
"powderCoatBlue":{
|
|
"baseColor":[0.15,0.25,0.7],
|
|
"roughness":0.55,
|
|
"metallic":0.0
|
|
},
|
|
|
|
"marbleStone":{
|
|
"baseColor":[0.85,0.85,0.87],
|
|
"roughness":0.9,
|
|
"metallic":0.0
|
|
}
|
|
}
|
|
},
|
|
"recognized":{"x":None, "y":None, "z": None, "a":None, "b":None, "c":None, "e": None},
|
|
"movements":{"x":None, "y":None, "z": None, "a":None, "b":None, "c":None, "e": None},
|
|
"elements":{
|
|
"Board":{"type":"static", "parent":None, "size":[1000, 200, 25],
|
|
"model":[{
|
|
"stlFile":"Board.stl",
|
|
"originOfModel":[0,0,0],
|
|
"rotationOfModelDegree":[0,0,90],
|
|
"material":"wood"},
|
|
{
|
|
"stlFile":"BoardRail.stl",
|
|
"originOfModel":[0,0,0],
|
|
"rotationOfModelDegree":[0,0,90],
|
|
"material":"steel"}
|
|
]},
|
|
"Base":{"type":"rigid", "parent":"Board", "size":[150, 200, 150], "rotationInParentCoordinates":[0, 0, 0],
|
|
"model":[{ "stlFile":"Base.stl"
|
|
}],
|
|
"jointToParent":{"name":"Slider", "type":"linear", "axis":[1,0,0], "origin":[0, 0, 0],
|
|
"rotation":[0, 0, 0],
|
|
"variable":"x"}},
|
|
"Arm1":{"type":"rigid", "parent":"Base", "size":[70, 250, 70],
|
|
"model":[{ "stlFile":"Holm.stl",
|
|
"originOfModel":[0,0,0],
|
|
"rotationOfModelDegree":[0,0,0],
|
|
"material":"powderCoatBlue"
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
LOCAL_PATH = r"C:\Users\kech\SynologyDrive\2026-AppServer-AppRobot\appRobotRendering\\"
|
|
MODEL_FILE = LOCAL_PATH + r"surfaces\BoardRail.stl"
|
|
|
|
OUTPUT_FILE = r"C:\Users\kech\SynologyDrive\2026-AppServer-AppRobot\appRobotRendering\render.png"
|
|
|
|
RENDER_WIDTH = 2000
|
|
RENDER_HEIGHT = 1000
|
|
|
|
# ============================================================
|
|
# CLEAN SCENE
|
|
# ============================================================
|
|
|
|
bpy.ops.object.select_all(action='SELECT')
|
|
bpy.ops.object.delete(use_global=False)
|
|
|
|
# ============================================================
|
|
# UNITS
|
|
# ============================================================
|
|
|
|
scene = bpy.context.scene
|
|
|
|
scene.unit_settings.system = 'METRIC'
|
|
scene.unit_settings.length_unit = 'MILLIMETERS'
|
|
|
|
metric = robot["renderingInfo"]["metric"]
|
|
|
|
# IMPORTANT:
|
|
# Blender internally uses meters.
|
|
# Your STL is already in millimeters.
|
|
# We therefore scale mm -> meters.
|
|
scale_factor = 0.001 if metric == "mm" else 1.0
|
|
|
|
# ============================================================
|
|
# IMPORT STL
|
|
# ============================================================
|
|
|
|
def import_stl(filepath):
|
|
try:
|
|
bpy.ops.wm.stl_import(filepath=filepath)
|
|
except:
|
|
bpy.ops.import_mesh.stl(filepath=filepath)
|
|
|
|
import_stl(MODEL_FILE)
|
|
|
|
imported_objects = bpy.context.selected_objects
|
|
|
|
# Apply scale
|
|
for obj in imported_objects:
|
|
obj.scale = (scale_factor, scale_factor, scale_factor)
|
|
|
|
# ============================================================
|
|
# CENTER OBJECT
|
|
# ============================================================
|
|
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
|
|
for obj in imported_objects:
|
|
obj.select_set(True)
|
|
|
|
bpy.context.view_layer.objects.active = imported_objects[0]
|
|
|
|
# Move origin to geometry center
|
|
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
|
|
|
|
# Move object to world center
|
|
for obj in imported_objects:
|
|
obj.location = (0, 0, 0)
|
|
|
|
# ============================================================
|
|
# WHITE PLASTIC MATERIAL
|
|
# ============================================================
|
|
|
|
mat = bpy.data.materials.new(name="WhitePlastic")
|
|
mat.use_nodes = True
|
|
|
|
bsdf = mat.node_tree.nodes["Principled BSDF"]
|
|
|
|
bsdf.inputs["Base Color"].default_value = (
|
|
0.95, 0.95, 0.95, 1.0
|
|
)
|
|
|
|
bsdf.inputs["Roughness"].default_value = 0.4
|
|
bsdf.inputs["Metallic"].default_value = 0.0
|
|
|
|
for obj in imported_objects:
|
|
if obj.type == 'MESH':
|
|
|
|
if len(obj.data.materials) == 0:
|
|
obj.data.materials.append(mat)
|
|
else:
|
|
obj.data.materials[0] = mat
|
|
|
|
# ============================================================
|
|
# CAMERA
|
|
# ============================================================
|
|
|
|
cam_data = bpy.data.cameras.new("Camera")
|
|
cam_obj = bpy.data.objects.new("Camera", cam_data)
|
|
|
|
bpy.context.collection.objects.link(cam_obj)
|
|
|
|
cam_pos = robot["renderingInfo"]["cameraPosition"]
|
|
cam_target = robot["renderingInfo"]["cameraTarget"]
|
|
|
|
# Convert mm -> meters
|
|
cam_obj.location = (
|
|
cam_pos[0] * scale_factor,
|
|
cam_pos[1] * scale_factor,
|
|
cam_pos[2] * scale_factor
|
|
)
|
|
|
|
target = mathutils.Vector((
|
|
cam_target[0] * scale_factor,
|
|
cam_target[1] * scale_factor,
|
|
cam_target[2] * scale_factor
|
|
))
|
|
|
|
direction = target - cam_obj.location
|
|
|
|
cam_obj.rotation_euler = (
|
|
direction.to_track_quat('-Z', 'Y').to_euler()
|
|
)
|
|
|
|
cam_data.lens = 50
|
|
|
|
scene.camera = cam_obj
|
|
|
|
# ============================================================
|
|
# SUN LIGHT
|
|
# ============================================================
|
|
|
|
sun_data = bpy.data.lights.new(name="Sun", type='SUN')
|
|
sun_obj = bpy.data.objects.new(name="Sun", object_data=sun_data)
|
|
|
|
bpy.context.collection.objects.link(sun_obj)
|
|
|
|
light_pos = robot["renderingInfo"]["lightPosition"]
|
|
light_target = robot["renderingInfo"]["lightTarget"]
|
|
|
|
sun_obj.location = (
|
|
light_pos[0] * scale_factor,
|
|
light_pos[1] * scale_factor,
|
|
light_pos[2] * scale_factor
|
|
)
|
|
|
|
light_target_vec = mathutils.Vector((
|
|
light_target[0] * scale_factor,
|
|
light_target[1] * scale_factor,
|
|
light_target[2] * scale_factor
|
|
))
|
|
|
|
light_direction = light_target_vec - sun_obj.location
|
|
|
|
sun_obj.rotation_euler = (
|
|
light_direction.to_track_quat('-Z', 'Y').to_euler()
|
|
)
|
|
|
|
sun_data.energy = 3.0
|
|
|
|
# ============================================================
|
|
# ADDITIONAL AREA LIGHT
|
|
# ============================================================
|
|
|
|
area_data = bpy.data.lights.new(name="AreaLight", type='AREA')
|
|
area_obj = bpy.data.objects.new(
|
|
name="AreaLight",
|
|
object_data=area_data
|
|
)
|
|
|
|
bpy.context.collection.objects.link(area_obj)
|
|
|
|
area_obj.location = (0, -1.2, 15)
|
|
|
|
area_data.energy = 5000
|
|
area_data.size = 2.0
|
|
|
|
# ============================================================
|
|
# CHECKERBOARD FLOOR
|
|
# ============================================================
|
|
|
|
# 2m x 2m floor
|
|
bpy.ops.mesh.primitive_plane_add(size=2, location=(0, 0, -27))
|
|
|
|
floor = bpy.context.active_object
|
|
|
|
# Create checker material
|
|
checker_mat = bpy.data.materials.new(name="Checkerboard")
|
|
checker_mat.use_nodes = True
|
|
|
|
nodes = checker_mat.node_tree.nodes
|
|
links = checker_mat.node_tree.links
|
|
|
|
nodes.clear()
|
|
|
|
output_node = nodes.new(type='ShaderNodeOutputMaterial')
|
|
bsdf_node = nodes.new(type='ShaderNodeBsdfPrincipled')
|
|
checker_node = nodes.new(type='ShaderNodeTexChecker')
|
|
mapping_node = nodes.new(type='ShaderNodeMapping')
|
|
texcoord_node = nodes.new(type='ShaderNodeTexCoord')
|
|
|
|
# Checker colors
|
|
checker_node.inputs["Color1"].default_value = (
|
|
0.8, 0.8, 0.8, 1.0
|
|
)
|
|
|
|
checker_node.inputs["Color2"].default_value = (
|
|
0.2, 0.2, 0.2, 1.0
|
|
)
|
|
|
|
# 100mm tiles
|
|
# floor is 2m -> 20 tiles
|
|
mapping_node.inputs["Scale"].default_value = (
|
|
20.0, 20.0, 20.0
|
|
)
|
|
|
|
links.new(
|
|
texcoord_node.outputs["UV"],
|
|
mapping_node.inputs["Vector"]
|
|
)
|
|
|
|
links.new(
|
|
mapping_node.outputs["Vector"],
|
|
checker_node.inputs["Vector"]
|
|
)
|
|
|
|
links.new(
|
|
checker_node.outputs["Color"],
|
|
bsdf_node.inputs["Base Color"]
|
|
)
|
|
|
|
links.new(
|
|
bsdf_node.outputs["BSDF"],
|
|
output_node.inputs["Surface"]
|
|
)
|
|
|
|
floor.data.materials.append(checker_mat)
|
|
|
|
# ============================================================
|
|
# SKY BACKGROUND
|
|
# ============================================================
|
|
|
|
world = scene.world
|
|
world.use_nodes = True
|
|
|
|
bg = world.node_tree.nodes["Background"]
|
|
|
|
# Light blue sky
|
|
bg.inputs[0].default_value = (
|
|
0.70, 0.85, 1.0, 1.0
|
|
)
|
|
|
|
bg.inputs[1].default_value = 0.8
|
|
|
|
# ============================================================
|
|
# RENDER SETTINGS
|
|
# ============================================================
|
|
|
|
scene.render.engine = 'CYCLES'
|
|
|
|
scene.cycles.samples = 128
|
|
|
|
scene.render.resolution_x = RENDER_WIDTH
|
|
scene.render.resolution_y = RENDER_HEIGHT
|
|
scene.render.resolution_percentage = 100
|
|
|
|
scene.render.image_settings.file_format = 'PNG'
|
|
|
|
scene.render.filepath = OUTPUT_FILE
|
|
|
|
# Slightly nicer shadows
|
|
scene.cycles.preview_samples = 32
|
|
|
|
# ============================================================
|
|
# RENDER
|
|
# ============================================================
|
|
|
|
bpy.ops.render.render(write_still=True)
|
|
|
|
print("Finished rendering:")
|
|
print(OUTPUT_FILE) |