diff --git a/render.png b/render.png index e1341c9..1f80d8e 100644 Binary files a/render.png and b/render.png differ diff --git a/render0.png b/render0.png deleted file mode 100644 index e8f98ba..0000000 Binary files a/render0.png and /dev/null differ diff --git a/render01a.png b/render01a.png deleted file mode 100644 index 37762c2..0000000 Binary files a/render01a.png and /dev/null differ diff --git a/render_robot.py b/render_robot.py index 17cc8f2..6c0615d 100644 --- a/render_robot.py +++ b/render_robot.py @@ -12,8 +12,6 @@ from mathutils import Matrix ROBOT_JSON_FILE = r"C:\Users\kech\SynologyDrive\2026-AppServer-AppRobot\appRobotRendering\robot.json" OUTPUT_FILE = r"C:\Users\kech\SynologyDrive\2026-AppServer-AppRobot\appRobotRendering\render.png" -RENDER_WIDTH = 1280 -RENDER_HEIGHT = 720 # ============================================================ # DEFAULT MATERIALS @@ -628,17 +626,17 @@ for link_name, link_info in links_def.items(): ) - # Marker-Normale im Welt-/Linkraum - normal_world = ( - marker_obj.matrix_world.to_quaternion() + # Marker-Normale im lokalen Link-Raum (aus Marker-Rotation) + normal_local = ( + marker_obj.rotation_quaternion @ mathutils.Vector((0, 0, 1)) ) - normal_world.normalize() + normal_local.normalize() - # minimal vorziehen gegen Z-Fighting + # minimal vorziehen gegen Z-Fighting (lokaler Versatz) marker_obj.location = ( mathutils.Vector(marker_pos) - + normal_world * mm_to_m(0.5) + + normal_local * mm_to_m(0.5) ) marker_mat = create_aruco_material( @@ -675,16 +673,14 @@ for link_name, link_info in links_def.items(): plate_obj.rotation_mode = "QUATERNION" plate_obj.rotation_quaternion = marker_obj.rotation_quaternion.copy() - # Normale des Markers im Welt-/Linkraum - normal_world = marker_obj.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, 1)) - normal_world.normalize() + # Normale des Markers im lokalen Link-Raum (aus Marker-Rotation) + normal_local = marker_obj.rotation_quaternion @ mathutils.Vector((0, 0, 1)) + normal_local.normalize() - - - # Platte liegt "hinter" dem Marker + # Platte liegt "hinter" dem Marker (lokaler Versatz) plate_obj.location = ( marker_obj.location - - normal_world * mm_to_m((plate_thickness_mm * 0.5) + gap_mm) + - normal_local * mm_to_m((plate_thickness_mm * 0.5) + gap_mm) ) # exakte Abmessungen: 26 x 26 x 1 mm diff --git a/render_robot_v00.py b/render_robot_v00.py deleted file mode 100644 index 94eb9d1..0000000 --- a/render_robot_v00.py +++ /dev/null @@ -1,365 +0,0 @@ -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) \ No newline at end of file diff --git a/render_robot_v01a.py b/render_robot_v01a.py deleted file mode 100644 index b43f297..0000000 --- a/render_robot_v01a.py +++ /dev/null @@ -1,580 +0,0 @@ -import bpy -import math -import mathutils -import json -from pathlib import Path -from typing import Any, Dict, Iterable, List, Optional, Tuple, Union - -# ============================================================ -# PATHS -# ============================================================ - -ROBOT_JSON_FILE = r"C:\Users\kech\SynologyDrive\2026-AppServer-AppRobot\appRobotRendering\robot.json" -OUTPUT_FILE = r"C:\Users\kech\SynologyDrive\2026-AppServer-AppRobot\appRobotRendering\render.png" -RENDER_WIDTH = 1200 -RENDER_HEIGHT = 800 - -# ============================================================ -# FALLBACK DEFAULT MATERIALS (placeholders) -# ============================================================ - -DEFAULT_MATERIALS = { - "wood": { - "baseColor": (0.72, 0.52, 0.33, 1.0), - "roughness": 0.85, - "metallic": 0.0, - }, - "plaWhite": { - "baseColor": (0.95, 0.95, 0.95, 1.0), - "roughness": 0.45, - "metallic": 0.0, - }, - "steel": { - "baseColor": (0.72, 0.72, 0.75, 1.0), - "roughness": 0.25, - "metallic": 1.0, - }, - "powderCoatBlue": { - "baseColor": (0.15, 0.25, 0.70, 1.0), - "roughness": 0.55, - "metallic": 0.0, - }, - "marbleStone": { - "baseColor": (0.85, 0.85, 0.87, 1.0), - "roughness": 0.95, - "metallic": 0.0, - }, - "defaultPlastic": { - "baseColor": (0.95, 0.95, 0.95, 1.0), - "roughness": 0.40, - "metallic": 0.0, - }, -} - -STATE_KEYS = ["x", "y", "z", "a", "b", "c", "e"] - -# ============================================================ -# JSON LOADING -# ============================================================ - -robot: Dict[str, Any] = {} -if Path(ROBOT_JSON_FILE).exists(): - with open(ROBOT_JSON_FILE, "r", encoding="utf-8") as f: - robot = json.load(f) -else: - # Minimal fallback so the script can still run during development. - robot = { - "renderingInfo": { - "cameraPosition": [-500, -1100, 900], - "cameraTarget": [0, 0, 200], - "cameraUpVector": [0, 0, 1], - "lightPosition": [-1000, -1000, 2000], - "lightTarget": [0, 0, 0], - "lightUpVector": [0, 0, 1], - "metric": "mm", - "materials": {}, - }, - "defaultPosition": {k: 0 for k in STATE_KEYS}, - "recognized": {k: None for k in STATE_KEYS}, - "movements": {k: None for k in STATE_KEYS}, - "links": {}, - } - -rendering_info = robot.get("renderingInfo", {}) -metric = rendering_info.get("metric", "mm") -scale_factor = 0.001 if metric == "mm" else 1.0 - -# Merge current state from multiple places. -# Priority: movements -> recognized -> defaultPosition -> 0 -state: Dict[str, float] = {k: 0.0 for k in STATE_KEYS} -for source_name in ("defaultPosition", "recognized", "movements"): - source = robot.get(source_name, {}) or {} - for k in STATE_KEYS: - v = source.get(k, None) - if v is not None: - state[k] = float(v) - -# ============================================================ -# HELPERS -# ============================================================ - -def mm_to_m(value: float) -> float: - return value * scale_factor - - -def resolve_scalar(value: Any, state_map: Dict[str, float]) -> float: - """Resolve numbers or symbolic placeholders like 'x'/'a'.""" - if value is None: - return 0.0 - if isinstance(value, (int, float)): - return float(value) - if isinstance(value, str): - key = value.strip().lower() - if key in state_map: - return float(state_map[key]) - try: - return float(key) - except ValueError: - return 0.0 - return 0.0 - - -def resolve_vector(value: Any, state_map: Dict[str, float], default_len: int = 3) -> Tuple[float, ...]: - if value is None: - return tuple(0.0 for _ in range(default_len)) - if isinstance(value, (int, float, str)): - return (resolve_scalar(value, state_map),) - if isinstance(value, (list, tuple)): - resolved = [resolve_scalar(v, state_map) for v in value] - if len(resolved) < default_len: - resolved.extend([0.0] * (default_len - len(resolved))) - return tuple(resolved[:default_len]) - return tuple(0.0 for _ in range(default_len)) - - -def resolve_vec3_m(value: Any, state_map: Dict[str, float]) -> Tuple[float, float, float]: - x, y, z = resolve_vector(value, state_map, default_len=3) - return mm_to_m(x), mm_to_m(y), mm_to_m(z) - - -def normalize_axis(axis: Iterable[Any]) -> mathutils.Vector: - ax = mathutils.Vector((float(axis[0]), float(axis[1]), float(axis[2]))) - if ax.length == 0: - return mathutils.Vector((1.0, 0.0, 0.0)) - return ax.normalized() - - -def create_or_get_material(name: str, fallback: str = "defaultPlastic") -> bpy.types.Material: - if name in bpy.data.materials: - return bpy.data.materials[name] - - info = (robot.get("renderingInfo", {}) or {}).get("materials", {}) or {} - spec = None - - # Support both dict-style and old list-style material definitions. - if isinstance(info, dict): - spec = info.get(name) - elif isinstance(info, list): - for entry in info: - if isinstance(entry, dict) and name in entry: - spec = entry[name] - break - - if not isinstance(spec, dict): - spec = DEFAULT_MATERIALS.get(name, DEFAULT_MATERIALS[fallback]) - else: - # Accept partial specs from JSON. - base = DEFAULT_MATERIALS.get(name, DEFAULT_MATERIALS[fallback]).copy() - if "baseColor" in spec: - color = tuple(spec["baseColor"]) - base["baseColor"] = (*color[:3], 1.0) if len(color) == 3 else tuple(color[:4]) - if "roughness" in spec: - base["roughness"] = float(spec["roughness"]) - if "metallic" in spec: - base["metallic"] = float(spec["metallic"]) - spec = base - - mat = bpy.data.materials.new(name=name) - mat.use_nodes = True - bsdf = mat.node_tree.nodes.get("Principled BSDF") - if bsdf is not None: - bsdf.inputs["Base Color"].default_value = spec["baseColor"] - bsdf.inputs["Roughness"].default_value = spec["roughness"] - bsdf.inputs["Metallic"].default_value = spec["metallic"] - return mat - -def import_stl(filepath: str): - - filepath = str(Path(filepath).resolve()) - - if not Path(filepath).exists(): - raise FileNotFoundError( - f"STL file not found:\\n{filepath}" - ) - - before = set(bpy.data.objects) - - bpy.ops.wm.stl_import(filepath=filepath) - - after = [ - obj for obj in bpy.data.objects - if obj not in before - ] - - return after - - -def link_object(obj: bpy.types.Object): - if obj.name not in bpy.context.collection.objects: - bpy.context.collection.objects.link(obj) - - -def create_empty(name: str, location=(0, 0, 0), rotation=(0, 0, 0)) -> bpy.types.Object: - empty = bpy.data.objects.new(name, None) - bpy.context.collection.objects.link(empty) - empty.location = location - empty.rotation_euler = rotation - return empty - - -def euler_deg_xyz(values: Any) -> Tuple[float, float, float]: - x, y, z = resolve_vector(values, state, default_len=3) - return math.radians(x), math.radians(y), math.radians(z) - - -def safe_parent(child: bpy.types.Object, parent: Optional[bpy.types.Object]): - if parent is not None: - child.parent = parent - # Keep current world transform visually stable after parenting. - child.matrix_parent_inverse = parent.matrix_world.inverted() - - -# ============================================================ -# CLEAN SCENE -# ============================================================ - -bpy.ops.object.select_all(action="SELECT") -bpy.ops.object.delete(use_global=False) - -# ============================================================ -# UNITS / WORLD -# ============================================================ - -scene = bpy.context.scene -scene.unit_settings.system = "METRIC" -scene.unit_settings.length_unit = "MILLIMETERS" -scene.unit_settings.scale_length = scale_factor - -world = scene.world -if world is None: - world = bpy.data.worlds.new("World") - scene.world = world -world.use_nodes = True -bg = world.node_tree.nodes["Background"] -bg.inputs[0].default_value = (0.70, 0.85, 1.0, 1.0) # light blue sky -bg.inputs[1].default_value = 0.2 - -# ============================================================ -# RENDER SETTINGS -# ============================================================ - -scene.render.engine = "CYCLES" -scene.view_settings.exposure = -1.5 -scene.cycles.samples = 64 -scene.cycles.preview_samples = 32 -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 -scene.render.film_transparent = False - -# ============================================================ -# FLOOR / CHECKERBOARD -# ============================================================ - -# 2m x 2m floor, centered at origin. -bpy.ops.mesh.primitive_plane_add(size=2.0, location=(0, 0, mm_to_m(-28.0))) -floor = bpy.context.active_object - -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_node.inputs["Color1"].default_value = (0.82, 0.82, 0.82, 1.0) -checker_node.inputs["Color2"].default_value = (0.18, 0.18, 0.18, 1.0) - -# 100mm checker squares across a 2m x 2m floor => 20 tiles each direction. -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) - -# ============================================================ -# 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 = resolve_vec3_m(rendering_info.get("cameraPosition", [-500, -1100, 900]), state) -cam_target = resolve_vec3_m(rendering_info.get("cameraTarget", [0, 0, 0]), state) - -cam_obj.location = cam_pos -cam_data.lens = 50 - -cam_vec = mathutils.Vector(cam_target) - mathutils.Vector(cam_pos) -if cam_vec.length == 0: - cam_vec = mathutils.Vector((1, 0, 0)) -cam_obj.rotation_euler = cam_vec.to_track_quat("-Z", "Y").to_euler() -scene.camera = cam_obj - -# ============================================================ -# LIGHTS -# ============================================================ - -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) - -sun_pos = resolve_vec3_m(rendering_info.get("lightPosition", [-1000, -1000, 2000]), state) -light_target = resolve_vec3_m(rendering_info.get("lightTarget", [0, 0, 0]), state) -sun_obj.location = sun_pos -light_vec = mathutils.Vector(light_target) - mathutils.Vector(sun_pos) -if light_vec.length == 0: - light_vec = mathutils.Vector((1, 0, -1)) -sun_obj.rotation_euler = light_vec.to_track_quat("-Z", "Y").to_euler() -sun_data.energy = 1.0 - -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 = (mm_to_m(-800), mm_to_m(-1200), mm_to_m(1500)) -area_obj.rotation_euler = (math.radians(60), 0.0, math.radians(-20)) -area_data.energy = 300 -area_data.size = 2.0 - -# ============================================================ -# ROBOT BUILDING -# ============================================================ - -links_def = robot.get("links") -if links_def is None: - # Backward compatibility with older name. - links_def = robot.get("ElementInfos", {}) - -created_nodes: Dict[str, bpy.types.Object] = {} - -# Create all link containers first. -for link_name in links_def.keys(): - created_nodes[link_name] = create_empty(f"{link_name}_link") - -# Parent/position link containers. -for link_name, link_info in links_def.items(): - parent_name = link_info.get("parent") - parent_obj = created_nodes.get(parent_name) if parent_name else None - - link_obj = created_nodes[link_name] - safe_parent(link_obj, parent_obj) - - # Static mounting transform relative to parent. - # Keep the extra info, but rename it to mountRotation in your JSON. - mount_pos = link_info.get("mountPosition", link_info.get("originInParentCoordinates", [0, 0, 0])) - mount_rot = link_info.get("mountRotation", link_info.get("rotationInParentCoordinates", [0, 0, 0])) - - link_obj.location = resolve_vec3_m(mount_pos, state) - link_obj.rotation_euler = euler_deg_xyz(mount_rot) - - # Joint transform (child-owned). - joint = link_info.get("jointToParent") or link_info.get("joint") - if isinstance(joint, dict): - joint_origin = joint.get("origin", [0, 0, 0]) - joint_rot = joint.get("rotation", [0, 0, 0]) - joint_type = joint.get("type", "fixed") - control_var = str(joint.get("variable", joint.get("control", ""))).lower() - axis = joint.get("axis", [1, 0, 0]) - - joint_offset = create_empty(f"{link_name}_joint") - safe_parent(joint_offset, link_obj) - joint_offset.location = resolve_vec3_m(joint_origin, state) - joint_offset.rotation_euler = euler_deg_xyz(joint_rot) - - # Motion node under the joint offset. - motion_node = create_empty(f"{link_name}_motion") - safe_parent(motion_node, joint_offset) - - if joint_type == "linear": - # Linear joint moves along its local axis by the control value. - move_val_mm = state.get(control_var, 0.0) if control_var else 0.0 - axis_v = normalize_axis(axis) - motion_node.location = axis_v * mm_to_m(move_val_mm) - elif joint_type == "revolute": - # Revolute joint rotates around its local axis by the control value. - angle_deg = state.get(control_var, 0.0) if control_var else 0.0 - axis_v = normalize_axis(axis) - # Convert axis-angle to Euler in local space by using rotation_difference. - quat = mathutils.Quaternion(axis_v, math.radians(angle_deg)) - motion_node.rotation_euler = quat.to_euler() - else: - # fixed / unknown => no motion - pass - - # The link container sits under the motion node. - safe_parent(link_obj, motion_node) - -# Import and attach all meshes for every link. -for link_name, link_info in links_def.items(): - link_obj = created_nodes[link_name] - model_list = link_info.get("model", []) - if not isinstance(model_list, list): - model_list = [] - - # Optional single-file shorthand. - if "stlFile" in link_info: - model_list = model_list + [{"stlFile": link_info["stlFile"]}] - - for idx, model_def in enumerate(model_list): - stl_file = model_def.get("stlFile") - if not stl_file: - continue - - base_dir = Path(ROBOT_JSON_FILE).parent if Path(ROBOT_JSON_FILE).exists() else Path.cwd() - stl_path = (base_dir / stl_file).resolve() - if not stl_path.exists(): - # Try the file as given. - stl_path = Path(stl_file).resolve() - - imported = import_stl(str(stl_path)) - - # Create a mesh container for each imported STL so a link can have many surfaces. - mesh_container = create_empty(f"{link_name}_mesh_{idx}") - safe_parent(mesh_container, link_obj) - - origin_of_model = model_def.get("originOfModel", [0, 0, 0]) - rot_of_model = model_def.get("rotationOfModelDegree", [0, 0, 0]) - mesh_container.location = resolve_vec3_m(origin_of_model, state) - mesh_container.rotation_euler = euler_deg_xyz(rot_of_model) - - material_name = model_def.get("material", "defaultPlastic") - material = create_or_get_material(material_name) - - for obj in imported: - if obj.type != "MESH": - continue - safe_parent(obj, mesh_container) - # Keep STL imports at their own local origin; only scale to meters. - obj.scale = (scale_factor, scale_factor, scale_factor) - if len(obj.data.materials) == 0: - obj.data.materials.append(material) - else: - obj.data.materials[0] = material - - -# ============================================================ -# DEBUG WORLD AXES -# ============================================================ - -def create_axis_arrow( - name, - direction, - color, - length_mm=200, - radius_mm=2, - cone_radius_mm=5, - cone_length_mm=20 -): - length = mm_to_m(length_mm) - radius = mm_to_m(radius_mm) - cone_radius = mm_to_m(cone_radius_mm) - cone_length = mm_to_m(cone_length_mm) - - dir_vec = mathutils.Vector(direction).normalized() - - # -------------------------------------------------------- - # CYLINDER - # -------------------------------------------------------- - - bpy.ops.mesh.primitive_cylinder_add( - radius=radius, - depth=length - cone_length - ) - - cyl = bpy.context.active_object - cyl.name = f"{name}_shaft" - - # Blender cylinder points along Z by default - cyl.rotation_mode = 'QUATERNION' - cyl.rotation_quaternion = ( - mathutils.Vector((0, 0, 1)) - .rotation_difference(dir_vec) - ) - - cyl.location = dir_vec * ((length - cone_length) * 0.5) - - # -------------------------------------------------------- - # CONE - # -------------------------------------------------------- - - bpy.ops.mesh.primitive_cone_add( - radius1=cone_radius, - depth=cone_length - ) - - cone = bpy.context.active_object - cone.name = f"{name}_tip" - - cone.rotation_mode = 'QUATERNION' - cone.rotation_quaternion = ( - mathutils.Vector((0, 0, 1)) - .rotation_difference(dir_vec) - ) - - cone.location = dir_vec * (length - cone_length * 0.5) - - # -------------------------------------------------------- - # MATERIAL - # -------------------------------------------------------- - - mat = bpy.data.materials.new(name=f"{name}_material") - mat.use_nodes = True - - bsdf = mat.node_tree.nodes["Principled BSDF"] - - bsdf.inputs["Base Color"].default_value = ( - color[0], - color[1], - color[2], - 1.0 - ) - - bsdf.inputs["Roughness"].default_value = 0.3 - bsdf.inputs["Metallic"].default_value = 0.0 - - cyl.data.materials.append(mat) - cone.data.materials.append(mat) - - -# ------------------------------------------------------------ -# CREATE XYZ AXES -# ------------------------------------------------------------ - -# X = red -create_axis_arrow( - "AxisX", - (1, 0, 0), - (1, 0, 0) -) - -# Y = green -create_axis_arrow( - "AxisY", - (0, 1, 0), - (0, 1, 0) -) - -# Z = blue -create_axis_arrow( - "AxisZ", - (0, 0, 1), - (0, 0, 1) -) - - -# ============================================================ -# FINAL RENDER -# ============================================================ - -bpy.ops.render.render(write_still=True) -print("Finished rendering:", OUTPUT_FILE) diff --git a/render_robot_v01b.py b/render_robot_v01b.py deleted file mode 100644 index 147c96e..0000000 --- a/render_robot_v01b.py +++ /dev/null @@ -1,595 +0,0 @@ -import bpy -import math -import mathutils -import json -from pathlib import Path -from typing import Any, Dict, Iterable, List, Optional, Tuple - -# ============================================================ -# PATHS -# ============================================================ - -ROBOT_JSON_FILE = r"C:\Users\kech\SynologyDrive\2026-AppServer-AppRobot\appRobotRendering\robot.json" -OUTPUT_FILE = r"C:\Users\kech\SynologyDrive\2026-AppServer-AppRobot\appRobotRendering\render.png" -RENDER_WIDTH = 1200 -RENDER_HEIGHT = 800 - -# ============================================================ -# DEFAULT MATERIALS -# ============================================================ - -DEFAULT_MATERIALS = { - "wood": {"baseColor": (0.72, 0.52, 0.33, 1.0), "roughness": 0.85, "metallic": 0.0}, - "plaWhite": {"baseColor": (0.95, 0.95, 0.95, 1.0), "roughness": 0.45, "metallic": 0.0}, - "steel": {"baseColor": (0.72, 0.72, 0.75, 1.0), "roughness": 0.25, "metallic": 1.0}, - "powderCoatBlue": {"baseColor": (0.15, 0.25, 0.70, 1.0), "roughness": 0.55, "metallic": 0.0}, - "defaultPlastic": {"baseColor": (0.95, 0.95, 0.95, 1.0), "roughness": 0.40, "metallic": 0.0}, - "skeletonRed": {"baseColor": (0.85, 0.20, 0.20, 1.0), "roughness": 0.35, "metallic": 0.0}, - "markerBlack": {"baseColor": (0.04, 0.04, 0.04, 1.0), "roughness": 0.80, "metallic": 0.0}, -} - -STATE_KEYS = ["x", "y", "z", "a", "b", "c", "e"] - -# ============================================================ -# JSON LOADING -# ============================================================ - -with open(ROBOT_JSON_FILE, "r", encoding="utf-8") as f: - robot: Dict[str, Any] = json.load(f) - -rendering_info = robot.get("renderingInfo", {}) -metric = rendering_info.get("metric", "mm") -scale_factor = 0.001 if metric == "mm" else 1.0 - -def as_bool(value: Any, default: bool = False) -> bool: - if value is None: - return default - if isinstance(value, bool): - return value - if isinstance(value, str): - return value.strip().lower() in ("1", "true", "yes", "on") - return bool(value) - -show_skeleton = as_bool(rendering_info.get("showSkeleton", False)) -show_markers = as_bool(rendering_info.get("showMarkers", False)) - -state: Dict[str, float] = {k: 0.0 for k in STATE_KEYS} -for source_name in ("defaultPosition", "recognized", "movements"): - source = robot.get(source_name, {}) or {} - for k in STATE_KEYS: - v = source.get(k, None) - if v is not None: - try: - state[k] = float(v) - except Exception: - pass - -links_def = robot.get("links", {}) -if not isinstance(links_def, dict): - raise ValueError("robot.json must contain a top-level 'links' object") - -# ============================================================ -# HELPERS -# ============================================================ - -def mm_to_m(value: float) -> float: - return value * scale_factor - -def resolve_scalar(value: Any, state_map: Dict[str, float]) -> float: - if value is None: - return 0.0 - if isinstance(value, (int, float)): - return float(value) - if isinstance(value, str): - key = value.strip().lower() - if key in state_map: - return float(state_map[key]) - try: - return float(key) - except ValueError: - return 0.0 - return 0.0 - -def resolve_vector(value: Any, state_map: Dict[str, float], default_len: int = 3) -> Tuple[float, ...]: - if value is None: - return tuple(0.0 for _ in range(default_len)) - if isinstance(value, (int, float, str)): - return (resolve_scalar(value, state_map),) - if isinstance(value, (list, tuple)): - resolved = [resolve_scalar(v, state_map) for v in value] - if len(resolved) < default_len: - resolved.extend([0.0] * (default_len - len(resolved))) - return tuple(resolved[:default_len]) - return tuple(0.0 for _ in range(default_len)) - -def resolve_vec3_m(value: Any, state_map: Dict[str, float]) -> Tuple[float, float, float]: - vec = list(resolve_vector(value, state_map, default_len=3)) - while len(vec) < 3: - vec.append(0.0) - x, y, z = vec[:3] - return mm_to_m(x), mm_to_m(y), mm_to_m(z) - -def normalize_axis(axis: Iterable[Any]) -> mathutils.Vector: - ax = mathutils.Vector((float(axis[0]), float(axis[1]), float(axis[2]))) - return ax.normalized() if ax.length > 0 else mathutils.Vector((1.0, 0.0, 0.0)) - -def euler_deg_xyz(values: Any) -> Tuple[float, float, float]: - vec = list(resolve_vector(values, state, default_len=3)) - while len(vec) < 3: - vec.append(0.0) - return math.radians(vec[0]), math.radians(vec[1]), math.radians(vec[2]) - -def create_or_get_material(name: str, fallback: str = "defaultPlastic") -> bpy.types.Material: - info = rendering_info.get("materials", {}) or {} - spec = None - - if isinstance(info, dict): - spec = info.get(name) - - if isinstance(spec, dict): - base = DEFAULT_MATERIALS.get(name, DEFAULT_MATERIALS[fallback]).copy() - if "baseColor" in spec: - color = tuple(spec["baseColor"]) - base["baseColor"] = (*color[:3], 1.0) if len(color) == 3 else tuple(color[:4]) - if "roughness" in spec: - base["roughness"] = float(spec["roughness"]) - if "metallic" in spec: - base["metallic"] = float(spec["metallic"]) - spec = base - else: - spec = DEFAULT_MATERIALS.get(name, DEFAULT_MATERIALS[fallback]) - - if name in bpy.data.materials: - mat = bpy.data.materials[name] - else: - mat = bpy.data.materials.new(name=name) - - mat.use_nodes = True - bsdf = mat.node_tree.nodes.get("Principled BSDF") - if bsdf is not None: - bsdf.inputs["Base Color"].default_value = spec["baseColor"] - bsdf.inputs["Roughness"].default_value = spec["roughness"] - bsdf.inputs["Metallic"].default_value = spec["metallic"] - return mat - -def import_stl(filepath: str) -> List[bpy.types.Object]: - path = Path(filepath).resolve() - if not path.exists(): - raise FileNotFoundError(f"STL file not found:\n{path}") - - before = set(bpy.data.objects) - bpy.ops.wm.stl_import(filepath=str(path)) - after = [obj for obj in bpy.data.objects if obj not in before] - return after - -def create_empty(name: str) -> bpy.types.Object: - empty = bpy.data.objects.new(name, None) - bpy.context.collection.objects.link(empty) - return empty - -def safe_parent(child: bpy.types.Object, parent: Optional[bpy.types.Object]): - if parent is not None: - child.parent = parent - child.matrix_parent_inverse = parent.matrix_world.inverted() - -def create_material_segment(name: str, color: Tuple[float, float, float], roughness: float = 0.35) -> bpy.types.Material: - if name in bpy.data.materials: - mat = bpy.data.materials[name] - else: - mat = bpy.data.materials.new(name=name) - mat.use_nodes = True - bsdf = mat.node_tree.nodes.get("Principled BSDF") - if bsdf is not None: - bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0) - bsdf.inputs["Roughness"].default_value = roughness - bsdf.inputs["Metallic"].default_value = 0.0 - return mat - -def create_cylinder_between( - name: str, - p1_local: Tuple[float, float, float], - p2_local: Tuple[float, float, float], - radius_m: float, - parent: bpy.types.Object, - material: bpy.types.Material -) -> bpy.types.Object: - v1 = mathutils.Vector(p1_local) - v2 = mathutils.Vector(p2_local) - delta = v2 - v1 - length = delta.length - if length <= 1e-9: - length = 1e-6 - delta = mathutils.Vector((0.0, 0.0, 1e-6)) - - bpy.ops.mesh.primitive_cylinder_add(radius=radius_m, depth=length) - obj = bpy.context.active_object - obj.name = name - safe_parent(obj, parent) - obj.location = (v1 + v2) * 0.5 - obj.rotation_mode = "QUATERNION" - obj.rotation_quaternion = mathutils.Vector((0, 0, 1)).rotation_difference(delta.normalized()) - if len(obj.data.materials) == 0: - obj.data.materials.append(material) - else: - obj.data.materials[0] = material - return obj - -def derive_default_skeleton_from_size(size_mm: List[float]) -> Dict[str, Any]: - sx, sy, sz = (float(size_mm[0]), float(size_mm[1]), float(size_mm[2])) - ax = max((abs(sx), 0), (abs(sy), 1), (abs(sz), 2), key=lambda x: x[0])[1] - - if ax == 0: - return {"from": [0, sy * 0.5, sz * 0.5], "to": [sx, sy * 0.5, sz * 0.5]} - if ax == 1: - return {"from": [sx * 0.5, 0, sz * 0.5], "to": [sx * 0.5, sy, sz * 0.5]} - return {"from": [sx * 0.5, sy * 0.5, 0], "to": [sx * 0.5, sy * 0.5, sz]} - -def resolve_stl_path(stl_file: str) -> Path: - base_dir = Path(ROBOT_JSON_FILE).parent - candidates = [ - base_dir / stl_file, - base_dir / "surfaces" / stl_file, - Path(stl_file), - ] - for c in candidates: - p = c.resolve() - if p.exists(): - return p - raise FileNotFoundError( - "STL file not found in any expected location:\n" + - "\n".join(str(c.resolve()) for c in candidates) - ) - -# ============================================================ -# SCENE RESET -# ============================================================ - -bpy.ops.object.select_all(action="SELECT") -bpy.ops.object.delete(use_global=False) - -scene = bpy.context.scene -scene.unit_settings.system = "METRIC" -scene.unit_settings.length_unit = "MILLIMETERS" -scene.unit_settings.scale_length = scale_factor - -# ============================================================ -# WORLD / RENDER SETTINGS -# ============================================================ - -world = scene.world or bpy.data.worlds.new("World") -scene.world = world -world.use_nodes = True -bg = world.node_tree.nodes["Background"] -bg.inputs[0].default_value = tuple(rendering_info.get("backgroundColor", [0.70, 0.85, 1.0])) + (1.0,) -bg.inputs[1].default_value = float(rendering_info.get("backgroundStrength", 0.20)) - -scene.render.engine = "CYCLES" -scene.view_settings.exposure = float(rendering_info.get("exposure", -1.5)) -scene.cycles.samples = 64 -scene.cycles.preview_samples = 32 -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 -scene.render.film_transparent = False - -# ============================================================ -# FLOOR -# ============================================================ - -bpy.ops.mesh.primitive_plane_add(size=2.0, location=(0, 0, mm_to_m(-28.0))) -floor = bpy.context.active_object -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_node.inputs["Color1"].default_value = (0.82, 0.82, 0.82, 1.0) -checker_node.inputs["Color2"].default_value = (0.18, 0.18, 0.18, 1.0) -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) - -# ============================================================ -# 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 = resolve_vec3_m(rendering_info.get("cameraPosition", [-400, -700, 300]), state) -cam_target = resolve_vec3_m(rendering_info.get("cameraTarget", [0, 0, 0]), state) -cam_obj.location = cam_pos -cam_data.lens = 50 -cam_vec = mathutils.Vector(cam_target) - mathutils.Vector(cam_pos) -if cam_vec.length == 0: - cam_vec = mathutils.Vector((1, 0, 0)) -cam_obj.rotation_euler = cam_vec.to_track_quat("-Z", "Y").to_euler() -scene.camera = cam_obj - -# ============================================================ -# LIGHTS -# ============================================================ - -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) -sun_pos = resolve_vec3_m(rendering_info.get("lightPosition", [-500, -500, 500]), state) -light_target = resolve_vec3_m(rendering_info.get("lightTarget", [0, 0, 0]), state) -sun_obj.location = sun_pos -light_vec = mathutils.Vector(light_target) - mathutils.Vector(sun_pos) -if light_vec.length == 0: - light_vec = mathutils.Vector((1, 0, -1)) -sun_obj.rotation_euler = light_vec.to_track_quat("-Z", "Y").to_euler() -sun_data.energy = float(rendering_info.get("sunEnergy", 0.35)) - -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 = (mm_to_m(-800), mm_to_m(-1200), mm_to_m(1500)) -area_obj.rotation_euler = (math.radians(60), 0.0, math.radians(-20)) -area_data.energy = float(rendering_info.get("areaEnergy", 120)) -area_data.size = 2.0 - -# ============================================================ -# ROBOT HIERARCHY -# ============================================================ - -link_frames: Dict[str, bpy.types.Object] = {} - -for link_name in links_def.keys(): - link_frames[link_name] = create_empty(f"{link_name}_frame") - -for link_name, link_info in links_def.items(): - parent_name = link_info.get("parent") - parent_frame = link_frames.get(parent_name) if parent_name else None - size_mm = link_info.get("size", [100, 100, 100]) - - mount = create_empty(f"{link_name}_mount") - safe_parent(mount, parent_frame) - mount.location = resolve_vec3_m(link_info.get("mountPosition", [0, 0, 0]), state) - mount.rotation_euler = euler_deg_xyz(link_info.get("mountRotation", [0, 0, 0])) - - joint_info = link_info.get("jointToParent", {}) or {} - joint = create_empty(f"{link_name}_joint") - safe_parent(joint, mount) - joint.location = resolve_vec3_m(joint_info.get("origin", [0, 0, 0]), state) - joint.rotation_euler = euler_deg_xyz(joint_info.get("rotation", [0, 0, 0])) - - motion = create_empty(f"{link_name}_motion") - safe_parent(motion, joint) - - joint_type = str(joint_info.get("type", "fixed")).lower() - control_var = str(joint_info.get("variable", joint_info.get("control", ""))).lower() - axis = joint_info.get("axis", [1, 0, 0]) - - if joint_type == "linear": - value_mm = state.get(control_var, 0.0) if control_var else 0.0 - motion.location = normalize_axis(axis) * mm_to_m(value_mm) - elif joint_type == "revolute": - value_deg = state.get(control_var, 0.0) if control_var else 0.0 - motion.rotation_mode = "QUATERNION" - motion.rotation_quaternion = mathutils.Quaternion(normalize_axis(axis), math.radians(value_deg)) - - link_frame = link_frames[link_name] - safe_parent(link_frame, motion) - - # -------------------------------------------------------- - # VISUAL MESHES - # -------------------------------------------------------- - - visual_root = create_empty(f"{link_name}_visual") - safe_parent(visual_root, link_frame) - - model_list = link_info.get("model", []) - if not isinstance(model_list, list): - model_list = [] - - for idx, model_def in enumerate(model_list): - stl_file = model_def.get("stlFile") - if not stl_file: - continue - - stl_path = resolve_stl_path(stl_file) - imported = import_stl(str(stl_path)) - - model_node = create_empty(f"{link_name}_model_{idx}") - safe_parent(model_node, visual_root) - model_node.location = resolve_vec3_m(model_def.get("originOfModel", [0, 0, 0]), state) - model_node.rotation_euler = euler_deg_xyz(model_def.get("rotationOfModelDegree", [0, 0, 0])) - - material_name = model_def.get("material", "defaultPlastic") - material = create_or_get_material(material_name) - - for obj in imported: - if obj.type != "MESH": - continue - safe_parent(obj, model_node) - obj.scale = (scale_factor, scale_factor, scale_factor) - if len(obj.data.materials) == 0: - obj.data.materials.append(material) - else: - obj.data.materials[0] = material - - # -------------------------------------------------------- - # SKELETON DEBUG - # -------------------------------------------------------- - - if show_skeleton: - skeleton_spec = link_info.get("skeleton") - if not isinstance(skeleton_spec, dict): - skeleton_spec = derive_default_skeleton_from_size(size_mm) - - p1_mm = skeleton_spec.get("from", [0, 0, 0]) - p2_mm = skeleton_spec.get("to", [0, 0, 0]) - p1 = resolve_vec3_m(p1_mm, state) - p2 = resolve_vec3_m(p2_mm, state) - - sk_radius_mm = float(skeleton_spec.get("radius", rendering_info.get("skeletonDefaults", {}).get("radius", 4))) - sk_color = skeleton_spec.get("color", rendering_info.get("skeletonDefaults", {}).get("color", [0.85, 0.20, 0.20])) - sk_mat = create_material_segment(f"{link_name}_skeletonMat", tuple(sk_color[:3])) - - create_cylinder_between( - f"{link_name}_skeleton", - p1, - p2, - mm_to_m(sk_radius_mm), - link_frame, - sk_mat - ) - - # -------------------------------------------------------- - # MARKERS - # -------------------------------------------------------- - - if show_markers: - marker_defaults = rendering_info.get("markerDefaults", {}) or {} - marker_mat = create_or_get_material("markerBlack") - - for m in link_info.get("markers", []): - if not isinstance(m, dict): - continue - - marker_name = m.get("name", f"{link_name}_marker_{m.get('id', 'x')}") - marker_size_mm = float(m.get("size", marker_defaults.get("size", 25))) - marker_pos = resolve_vec3_m(m.get("position", [0, 0, 0]), state) - marker_rot = euler_deg_xyz(m.get("rotation", [0, 0, 0])) - - bpy.ops.mesh.primitive_plane_add(size=mm_to_m(marker_size_mm)) - marker_obj = bpy.context.active_object - marker_obj.name = marker_name - safe_parent(marker_obj, link_frame) - marker_obj.location = marker_pos - marker_obj.rotation_euler = marker_rot - if len(marker_obj.data.materials) == 0: - marker_obj.data.materials.append(marker_mat) - else: - marker_obj.data.materials[0] = marker_mat -# ============================================================ -# DEBUG WORLD AXES -# ============================================================ - -def create_axis_arrow( - name, - direction, - color, - length_mm=200, - radius_mm=2, - cone_radius_mm=5, - cone_length_mm=20 -): - length = mm_to_m(length_mm) - radius = mm_to_m(radius_mm) - cone_radius = mm_to_m(cone_radius_mm) - cone_length = mm_to_m(cone_length_mm) - - dir_vec = mathutils.Vector(direction).normalized() - - # -------------------------------------------------------- - # CYLINDER - # -------------------------------------------------------- - - bpy.ops.mesh.primitive_cylinder_add( - radius=radius, - depth=length - cone_length - ) - - cyl = bpy.context.active_object - cyl.name = f"{name}_shaft" - - # Blender cylinder points along Z by default - cyl.rotation_mode = 'QUATERNION' - cyl.rotation_quaternion = ( - mathutils.Vector((0, 0, 1)) - .rotation_difference(dir_vec) - ) - - cyl.location = dir_vec * ((length - cone_length) * 0.5) - - # -------------------------------------------------------- - # CONE - # -------------------------------------------------------- - - bpy.ops.mesh.primitive_cone_add( - radius1=cone_radius, - depth=cone_length - ) - - cone = bpy.context.active_object - cone.name = f"{name}_tip" - - cone.rotation_mode = 'QUATERNION' - cone.rotation_quaternion = ( - mathutils.Vector((0, 0, 1)) - .rotation_difference(dir_vec) - ) - - cone.location = dir_vec * (length - cone_length * 0.5) - - # -------------------------------------------------------- - # MATERIAL - # -------------------------------------------------------- - - mat = bpy.data.materials.new(name=f"{name}_material") - mat.use_nodes = True - - bsdf = mat.node_tree.nodes["Principled BSDF"] - - bsdf.inputs["Base Color"].default_value = ( - color[0], - color[1], - color[2], - 1.0 - ) - - bsdf.inputs["Roughness"].default_value = 0.3 - bsdf.inputs["Metallic"].default_value = 0.0 - - cyl.data.materials.append(mat) - cone.data.materials.append(mat) - - -# ------------------------------------------------------------ -# CREATE XYZ AXES -# ------------------------------------------------------------ - -# X = red -create_axis_arrow( - "AxisX", - (1, 0, 0), - (1, 0, 0) -) - -# Y = green -create_axis_arrow( - "AxisY", - (0, 1, 0), - (0, 1, 0) -) - -# Z = blue -create_axis_arrow( - "AxisZ", - (0, 0, 1), - (0, 0, 1) -) - -# ============================================================ -# RENDER -# ============================================================ - -bpy.ops.render.render(write_still=True) -print("Finished rendering:", OUTPUT_FILE) \ No newline at end of file diff --git a/render_robot_v01c.py b/render_robot_v01c.py deleted file mode 100644 index 550457f..0000000 --- a/render_robot_v01c.py +++ /dev/null @@ -1,561 +0,0 @@ -import bpy -import math -import mathutils -import json -from pathlib import Path -from typing import Any, Dict, Iterable, List, Optional, Tuple -from mathutils import Matrix - -# ============================================================ -# PATHS -# ============================================================ - -ROBOT_JSON_FILE = r"C:\Users\kech\SynologyDrive\2026-AppServer-AppRobot\appRobotRendering\robot.json" -OUTPUT_FILE = r"C:\Users\kech\SynologyDrive\2026-AppServer-AppRobot\appRobotRendering\render.png" -RENDER_WIDTH = 1200 -RENDER_HEIGHT = 800 - -# ============================================================ -# DEFAULT MATERIALS -# ============================================================ - -DEFAULT_MATERIALS = { - "wood": {"baseColor": (0.72, 0.52, 0.33, 1.0), "roughness": 0.85, "metallic": 0.0}, - "plaWhite": {"baseColor": (0.95, 0.95, 0.95, 1.0), "roughness": 0.45, "metallic": 0.0}, - "steel": {"baseColor": (0.72, 0.72, 0.75, 1.0), "roughness": 0.25, "metallic": 1.0}, - "powderCoatBlue": {"baseColor": (0.15, 0.25, 0.70, 1.0), "roughness": 0.55, "metallic": 0.0}, - "defaultPlastic": {"baseColor": (0.95, 0.95, 0.95, 1.0), "roughness": 0.40, "metallic": 0.0}, - "skeletonRed": {"baseColor": (0.85, 0.20, 0.20, 1.0), "roughness": 0.35, "metallic": 0.0}, - "markerBlack": {"baseColor": (0.04, 0.04, 0.04, 1.0), "roughness": 0.80, "metallic": 0.0}, -} - -STATE_KEYS = ["x", "y", "z", "a", "b", "c", "e"] - -# ============================================================ -# JSON LOADING -# ============================================================ - -with open(ROBOT_JSON_FILE, "r", encoding="utf-8") as f: - robot: Dict[str, Any] = json.load(f) - -rendering_info = robot.get("renderingInfo", {}) -metric = rendering_info.get("metric", "mm") -scale_factor = 0.001 if metric == "mm" else 1.0 - -def as_bool(value: Any, default: bool = False) -> bool: - if value is None: - return default - if isinstance(value, bool): - return value - if isinstance(value, str): - return value.strip().lower() in ("1", "true", "yes", "on") - return bool(value) - -show_skeleton = as_bool(rendering_info.get("showSkeleton", False)) -show_markers = as_bool(rendering_info.get("showMarkers", False)) - -state: Dict[str, float] = {k: 0.0 for k in STATE_KEYS} -for source_name in ("defaultPosition", "recognized", "movements"): - source = robot.get(source_name, {}) or {} - for k in STATE_KEYS: - v = source.get(k, None) - if v is not None: - try: - state[k] = float(v) - except Exception: - pass - -links_def = robot.get("links", {}) -if not isinstance(links_def, dict): - raise ValueError("robot.json must contain a top-level 'links' object") - -# ============================================================ -# HELPERS -# ============================================================ - -def mm_to_m(value: float) -> float: - return value * scale_factor - -def resolve_scalar(value: Any, state_map: Dict[str, float]) -> float: - if value is None: - return 0.0 - if isinstance(value, (int, float)): - return float(value) - if isinstance(value, str): - key = value.strip().lower() - if key in state_map: - return float(state_map[key]) - try: - return float(key) - except ValueError: - return 0.0 - return 0.0 - -def resolve_vector(value: Any, state_map: Dict[str, float], default_len: int = 3) -> Tuple[float, ...]: - if value is None: - return tuple(0.0 for _ in range(default_len)) - if isinstance(value, (int, float, str)): - return (resolve_scalar(value, state_map),) - if isinstance(value, (list, tuple)): - resolved = [resolve_scalar(v, state_map) for v in value] - if len(resolved) < default_len: - resolved.extend([0.0] * (default_len - len(resolved))) - return tuple(resolved[:default_len]) - return tuple(0.0 for _ in range(default_len)) - -def resolve_vec3_m(value: Any, state_map: Dict[str, float]) -> Tuple[float, float, float]: - vec = list(resolve_vector(value, state_map, default_len=3)) - while len(vec) < 3: - vec.append(0.0) - x, y, z = vec[:3] - return mm_to_m(x), mm_to_m(y), mm_to_m(z) - -def normalize_axis(axis: Iterable[Any]) -> mathutils.Vector: - ax = mathutils.Vector((float(axis[0]), float(axis[1]), float(axis[2]))) - return ax.normalized() if ax.length > 0 else mathutils.Vector((1.0, 0.0, 0.0)) - -def euler_deg_xyz(values: Any) -> Tuple[float, float, float]: - vec = list(resolve_vector(values, state, default_len=3)) - while len(vec) < 3: - vec.append(0.0) - return math.radians(vec[0]), math.radians(vec[1]), math.radians(vec[2]) - -def create_or_get_material(name: str, fallback: str = "defaultPlastic") -> bpy.types.Material: - info = rendering_info.get("materials", {}) or {} - spec = None - - if isinstance(info, dict): - spec = info.get(name) - - if isinstance(spec, dict): - base = DEFAULT_MATERIALS.get(name, DEFAULT_MATERIALS[fallback]).copy() - if "baseColor" in spec: - color = tuple(spec["baseColor"]) - base["baseColor"] = (*color[:3], 1.0) if len(color) == 3 else tuple(color[:4]) - if "roughness" in spec: - base["roughness"] = float(spec["roughness"]) - if "metallic" in spec: - base["metallic"] = float(spec["metallic"]) - spec = base - else: - spec = DEFAULT_MATERIALS.get(name, DEFAULT_MATERIALS[fallback]) - - if name in bpy.data.materials: - mat = bpy.data.materials[name] - else: - mat = bpy.data.materials.new(name=name) - - mat.use_nodes = True - bsdf = mat.node_tree.nodes.get("Principled BSDF") - if bsdf is not None: - bsdf.inputs["Base Color"].default_value = spec["baseColor"] - bsdf.inputs["Roughness"].default_value = spec["roughness"] - bsdf.inputs["Metallic"].default_value = spec["metallic"] - return mat - -def import_stl(filepath: str) -> List[bpy.types.Object]: - path = Path(filepath).resolve() - if not path.exists(): - raise FileNotFoundError(f"STL file not found:\n{path}") - - before = set(bpy.data.objects) - bpy.ops.wm.stl_import(filepath=str(path)) - after = [obj for obj in bpy.data.objects if obj not in before] - return after - -def create_empty(name: str) -> bpy.types.Object: - empty = bpy.data.objects.new(name, None) - bpy.context.collection.objects.link(empty) - return empty - -def safe_parent(child: bpy.types.Object, parent: Optional[bpy.types.Object], keep_world: bool = False): - if parent is None: - return - world_matrix = child.matrix_world.copy() - child.parent = parent - if keep_world: - child.matrix_parent_inverse = parent.matrix_world.inverted() - child.matrix_world = world_matrix - else: - child.matrix_parent_inverse = Matrix.Identity(4) - -def create_material_segment(name: str, color: Tuple[float, float, float], roughness: float = 0.35) -> bpy.types.Material: - if name in bpy.data.materials: - mat = bpy.data.materials[name] - else: - mat = bpy.data.materials.new(name=name) - mat.use_nodes = True - bsdf = mat.node_tree.nodes.get("Principled BSDF") - if bsdf is not None: - bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0) - bsdf.inputs["Roughness"].default_value = roughness - bsdf.inputs["Metallic"].default_value = 0.0 - return mat - -def create_cylinder_between( - name: str, - p1_local: Tuple[float, float, float], - p2_local: Tuple[float, float, float], - radius_m: float, - parent: bpy.types.Object, - material: bpy.types.Material -) -> bpy.types.Object: - v1 = mathutils.Vector(p1_local) - v2 = mathutils.Vector(p2_local) - delta = v2 - v1 - length = delta.length - if length <= 1e-9: - length = 1e-6 - delta = mathutils.Vector((0.0, 0.0, 1e-6)) - - bpy.ops.mesh.primitive_cylinder_add(radius=radius_m, depth=length) - obj = bpy.context.active_object - obj.name = name - safe_parent(obj, parent, keep_world=False) - obj.location = (v1 + v2) * 0.5 - obj.rotation_mode = "QUATERNION" - obj.rotation_quaternion = mathutils.Vector((0, 0, 1)).rotation_difference(delta.normalized()) - if len(obj.data.materials) == 0: - obj.data.materials.append(material) - else: - obj.data.materials[0] = material - return obj - -def derive_default_skeleton_from_size(size_mm: List[float]) -> Dict[str, Any]: - sx, sy, sz = (float(size_mm[0]), float(size_mm[1]), float(size_mm[2])) - ax = max((abs(sx), 0), (abs(sy), 1), (abs(sz), 2), key=lambda x: x[0])[1] - - if ax == 0: - return {"from": [0, sy * 0.5, sz * 0.5], "to": [sx, sy * 0.5, sz * 0.5]} - if ax == 1: - return {"from": [sx * 0.5, 0, sz * 0.5], "to": [sx * 0.5, sy, sz * 0.5]} - return {"from": [sx * 0.5, sy * 0.5, 0], "to": [sx * 0.5, sy * 0.5, sz]} - -def resolve_stl_path(stl_file: str) -> Path: - base_dir = Path(ROBOT_JSON_FILE).parent - candidates = [ - base_dir / stl_file, - base_dir / "surfaces" / stl_file, - Path(stl_file), - ] - for c in candidates: - p = c.resolve() - if p.exists(): - return p - raise FileNotFoundError( - "STL file not found in any expected location:\n" + - "\n".join(str(c.resolve()) for c in candidates) - ) - -# ============================================================ -# SCENE RESET -# ============================================================ - -bpy.ops.object.select_all(action="SELECT") -bpy.ops.object.delete(use_global=False) - -scene = bpy.context.scene -scene.unit_settings.system = "METRIC" -scene.unit_settings.length_unit = "MILLIMETERS" -scene.unit_settings.scale_length = scale_factor - -# ============================================================ -# WORLD / RENDER SETTINGS -# ============================================================ - -world = scene.world or bpy.data.worlds.new("World") -scene.world = world -world.use_nodes = True -bg = world.node_tree.nodes["Background"] -bg.inputs[0].default_value = tuple(rendering_info.get("backgroundColor", [0.70, 0.85, 1.0])) + (1.0,) -bg.inputs[1].default_value = float(rendering_info.get("backgroundStrength", 0.20)) - -scene.render.engine = "CYCLES" -scene.view_settings.exposure = float(rendering_info.get("exposure", -1.5)) -scene.cycles.samples = 16 -scene.cycles.preview_samples = 32 -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 -scene.render.film_transparent = False - -# ============================================================ -# FLOOR -# ============================================================ - -bpy.ops.mesh.primitive_plane_add(size=2.0, location=(0, 0, mm_to_m(-28.0))) -floor = bpy.context.active_object -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_node.inputs["Color1"].default_value = (0.82, 0.82, 0.82, 1.0) -checker_node.inputs["Color2"].default_value = (0.18, 0.18, 0.18, 1.0) -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) - -# ============================================================ -# 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 = resolve_vec3_m(rendering_info.get("cameraPosition", [-400, -700, 300]), state) -cam_target = resolve_vec3_m(rendering_info.get("cameraTarget", [0, 0, 0]), state) -cam_obj.location = cam_pos -cam_data.lens = 50 -cam_vec = mathutils.Vector(cam_target) - mathutils.Vector(cam_pos) -if cam_vec.length == 0: - cam_vec = mathutils.Vector((1, 0, 0)) -cam_obj.rotation_euler = cam_vec.to_track_quat("-Z", "Y").to_euler() -scene.camera = cam_obj - -# ============================================================ -# LIGHTS -# ============================================================ - -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) -sun_pos = resolve_vec3_m(rendering_info.get("lightPosition", [-500, -500, 500]), state) -light_target = resolve_vec3_m(rendering_info.get("lightTarget", [0, 0, 0]), state) -sun_obj.location = sun_pos -light_vec = mathutils.Vector(light_target) - mathutils.Vector(sun_pos) -if light_vec.length == 0: - light_vec = mathutils.Vector((1, 0, -1)) -sun_obj.rotation_euler = light_vec.to_track_quat("-Z", "Y").to_euler() -sun_data.energy = float(rendering_info.get("sunEnergy", 0.35)) - -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 = (mm_to_m(-800), mm_to_m(-1200), mm_to_m(1500)) -area_obj.rotation_euler = (math.radians(60), 0.0, math.radians(-20)) -area_data.energy = float(rendering_info.get("areaEnergy", 120)) -area_data.size = 2.0 - -# ============================================================ -# ROBOT HIERARCHY -# ============================================================ - -link_frames: Dict[str, bpy.types.Object] = {} -for link_name in links_def.keys(): - link_frames[link_name] = create_empty(f"{link_name}_frame") - -for link_name, link_info in links_def.items(): - parent_name = link_info.get("parent") - parent_frame = link_frames.get(parent_name) if parent_name else None - size_mm = link_info.get("size", [100, 100, 100]) - - # mount: static position/rotation in parent coordinates - mount = create_empty(f"{link_name}_mount") - safe_parent(mount, parent_frame, keep_world=False) - mount.location = resolve_vec3_m(link_info.get("mountPosition", [0, 0, 0]), state) - mount.rotation_euler = euler_deg_xyz(link_info.get("mountRotation", [0, 0, 0])) - - # joint: sits inside the mount, defines pivot/orientation - joint_info = link_info.get("jointToParent", {}) or {} - joint = create_empty(f"{link_name}_joint") - safe_parent(joint, mount, keep_world=False) - joint.location = resolve_vec3_m(joint_info.get("origin", [0, 0, 0]), state) - joint.rotation_euler = euler_deg_xyz(joint_info.get("rotation", [0, 0, 0])) - - # motion: only this node gets the commanded position/angle - motion = create_empty(f"{link_name}_motion") - safe_parent(motion, joint, keep_world=False) - - joint_type = str(joint_info.get("type", "fixed")).lower() - control_var = str(joint_info.get("variable", joint_info.get("control", ""))).lower() - axis = joint_info.get("axis", [1, 0, 0]) - - if joint_type == "linear": - value_mm = state.get(control_var, 0.0) if control_var else 0.0 - motion.location = normalize_axis(axis) * mm_to_m(value_mm) - elif joint_type == "revolute": - value_deg = state.get(control_var, 0.0) if control_var else 0.0 - motion.rotation_mode = "QUATERNION" - motion.rotation_quaternion = mathutils.Quaternion(normalize_axis(axis), math.radians(value_deg)) - - # link frame: everything belonging to this link follows motion - link_frame = link_frames[link_name] - safe_parent(link_frame, motion, keep_world=False) - - # -------------------------------------------------------- - # VISUAL MESHES - # -------------------------------------------------------- - - visual_root = create_empty(f"{link_name}_visual") - safe_parent(visual_root, link_frame, keep_world=False) - - model_list = link_info.get("model", []) - if not isinstance(model_list, list): - model_list = [] - - for idx, model_def in enumerate(model_list): - stl_file = model_def.get("stlFile") - if not stl_file: - continue - - stl_path = resolve_stl_path(stl_file) - imported = import_stl(str(stl_path)) - - model_node = create_empty(f"{link_name}_model_{idx}") - safe_parent(model_node, visual_root, keep_world=False) - model_node.location = resolve_vec3_m(model_def.get("originOfModel", [0, 0, 0]), state) - model_node.rotation_euler = euler_deg_xyz(model_def.get("rotationOfModelDegree", [0, 0, 0])) - - material_name = model_def.get("material", "defaultPlastic") - material = create_or_get_material(material_name) - - for obj in imported: - if obj.type != "MESH": - continue - safe_parent(obj, model_node, keep_world=True) - obj.scale = (scale_factor, scale_factor, scale_factor) - if len(obj.data.materials) == 0: - obj.data.materials.append(material) - else: - obj.data.materials[0] = material - - # -------------------------------------------------------- - # SKELETON DEBUG - # -------------------------------------------------------- - - if show_skeleton: - skeleton_spec = link_info.get("skeleton") - if not isinstance(skeleton_spec, dict): - skeleton_spec = derive_default_skeleton_from_size(size_mm) - - p1_mm = skeleton_spec.get("from", [0, 0, 0]) - p2_mm = skeleton_spec.get("to", [0, 0, 0]) - p1 = resolve_vec3_m(p1_mm, state) - p2 = resolve_vec3_m(p2_mm, state) - - sk_radius_mm = float( - skeleton_spec.get( - "radius", - rendering_info.get("skeletonDefaults", {}).get("radius", 4) - ) - ) - sk_color = skeleton_spec.get( - "color", - rendering_info.get("skeletonDefaults", {}).get("color", [0.85, 0.20, 0.20]) - ) - sk_mat = create_material_segment(f"{link_name}_skeletonMat", tuple(sk_color[:3])) - - create_cylinder_between( - f"{link_name}_skeleton", - p1, - p2, - mm_to_m(sk_radius_mm), - link_frame, - sk_mat - ) - - # -------------------------------------------------------- - # MARKERS - # -------------------------------------------------------- - - if show_markers: - marker_defaults = rendering_info.get("markerDefaults", {}) or {} - marker_mat = create_or_get_material("markerBlack") - - for m in link_info.get("markers", []): - if not isinstance(m, dict): - continue - - marker_name = m.get("name", f"{link_name}_marker_{m.get('id', 'x')}") - marker_size_mm = float(m.get("size", marker_defaults.get("size", 25))) - marker_pos = resolve_vec3_m(m.get("position", [0, 0, 0]), state) - marker_rot = euler_deg_xyz(m.get("rotation", [0, 0, 0])) - - bpy.ops.mesh.primitive_plane_add(size=mm_to_m(marker_size_mm)) - marker_obj = bpy.context.active_object - marker_obj.name = marker_name - safe_parent(marker_obj, link_frame, keep_world=False) - marker_obj.location = marker_pos - marker_obj.rotation_euler = marker_rot - if len(marker_obj.data.materials) == 0: - marker_obj.data.materials.append(marker_mat) - else: - marker_obj.data.materials[0] = marker_mat - -# ============================================================ -# DEBUG WORLD AXES -# ============================================================ - -def create_axis_arrow( - name, - direction, - color, - length_mm=200, - radius_mm=2, - cone_radius_mm=5, - cone_length_mm=20 -): - length = mm_to_m(length_mm) - radius = mm_to_m(radius_mm) - cone_radius = mm_to_m(cone_radius_mm) - cone_length = mm_to_m(cone_length_mm) - - dir_vec = mathutils.Vector(direction).normalized() - - bpy.ops.mesh.primitive_cylinder_add( - radius=radius, - depth=length - cone_length - ) - - cyl = bpy.context.active_object - cyl.name = f"{name}_shaft" - cyl.rotation_mode = 'QUATERNION' - cyl.rotation_quaternion = mathutils.Vector((0, 0, 1)).rotation_difference(dir_vec) - cyl.location = dir_vec * ((length - cone_length) * 0.5) - - bpy.ops.mesh.primitive_cone_add( - radius1=cone_radius, - depth=cone_length - ) - - cone = bpy.context.active_object - cone.name = f"{name}_tip" - cone.rotation_mode = 'QUATERNION' - cone.rotation_quaternion = mathutils.Vector((0, 0, 1)).rotation_difference(dir_vec) - cone.location = dir_vec * (length - cone_length * 0.5) - - mat = bpy.data.materials.new(name=f"{name}_material") - mat.use_nodes = True - bsdf = mat.node_tree.nodes["Principled BSDF"] - bsdf.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0) - bsdf.inputs["Roughness"].default_value = 0.3 - bsdf.inputs["Metallic"].default_value = 0.0 - - cyl.data.materials.append(mat) - cone.data.materials.append(mat) - -create_axis_arrow("AxisX", (1, 0, 0), (1, 0, 0)) -create_axis_arrow("AxisY", (0, 1, 0), (0, 1, 0)) -create_axis_arrow("AxisZ", (0, 0, 1), (0, 0, 1)) - -# ============================================================ -# RENDER -# ============================================================ - -bpy.ops.render.render(write_still=True) -print("Finished rendering:", OUTPUT_FILE) \ No newline at end of file diff --git a/render_v01c.png b/render_v01c.png deleted file mode 100644 index 041aa92..0000000 Binary files a/render_v01c.png and /dev/null differ diff --git a/robot.json b/robot.json index 9168eaf..ad65e5c 100644 --- a/robot.json +++ b/robot.json @@ -80,8 +80,8 @@ "defaultPosition": { "x": 150, "y": 30, - "z": -30, - "a": 90, + "z": -40, + "a": 260, "b": 0, "c": 0, "e": 0 @@ -320,23 +320,24 @@ "radius": 4, "color": [0.95, 0.85, 0.20] }, - "markers":[ - { - "id": 17, - "name": "aruco_17", - "position": [0, -150, 0], - "normal": [-1, 0, 0], - "size": 25, - "spin": 0 - }, + "model": [ { - "id": 18, - "name": "aruco_18", - "position": [0, -180, 0], - "normal": [1, 0, 0], - "size": 25, - "spin": 0 + "stlFile": "surfaces/Unterarm.stl", + "originOfModel": [0,-250,0], + "rotationOfModelDegree": [180, 0, -90], + "material": "defaultPlastic" } + ], + "markers":[ + + {"id":228, "position":[-24.75, -112, 24.75],"normal":[-1,0,1], "relPosSource":["Fusion","Fusion","Fusion"]}, + {"id": 122, "name": "aruco_122", "position":[-24.75, -182, 24.75],"normal":[-1,0,1], "relPosSource":["Fusion","Fusion","Fusion"]}, + {"id": 122, "name": "aruco_122", "position":[-35,-112,0], "normal":[-1,0,0], "relPosSource":["Fusion","Fusion","Fusion"]}, + {"id": 124, "name": "aruco_124", "position":[-35,-219,0], "normal":[-1,0,0], "relPosSource":["Fusion","Fusion","Fusion"]}, + {"id":223,"name": "aruco_223", "position":[-28.67,-112,-20.08], "normal":[-28.67,0,-20.08], "relPosSource":["Fusion","Fusion","Fusion"]}, + {"id":218,"name": "aruco_218", "position":[35,-112,0], "normal":[1,0,0], "relPosSource":["Fusion","Fusion","Fusion"]}, + {"id":219, "name": "aruco_219", "position":[35,-219,0], "normal":[1,0,0], "relPosSource":["Fusion","Fusion","Fusion"]} + ] }, "Hand": { diff --git a/robot_v01a.json b/robot_v01a.json deleted file mode 100644 index 80bf585..0000000 --- a/robot_v01a.json +++ /dev/null @@ -1,138 +0,0 @@ -{ - "vision_config": { - "MarkerType": "DICT_4X4_250", - "MarkerSize": 0.025 - }, - "renderingInfo": { - "cameraPosition": [-400, -700, 300], - "cameraTarget": [300, 0, 90], - "cameraUpVector": [0, 0, 1], - "lightPosition": [-500, -500, 500], - "lightTarget": [0, 0, 0], - "lightUpVector": [0, 0, 1], - "metric": "mm", - "showSkeleton": true, - "materials": { - "wood": { - "baseColor": [0.72, 0.52, 0.33], - "roughness": 0.85, - "metallic": 0.0 - }, - "plaWhite": { - "baseColor": [0.95, 0.95, 0.95], - "roughness": 0.45, - "metallic": 0.0 - }, - "steel": { - "baseColor": [0.72, 0.72, 0.75], - "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.95, - "metallic": 0.0 - }, - "defaultPlastic": { - "baseColor": [0.95, 0.95, 0.95], - "roughness": 0.4, - "metallic": 0.0 - } - } - }, - "defaultPosition": { - "x": 0, - "y": 0, - "z": 0, - "a": 0, - "b": 0, - "c": 0, - "e": 0 - }, - "recognized": { - "x": null, - "y": null, - "z": null, - "a": null, - "b": null, - "c": null, - "e": null - }, - "movements": { - "x": null, - "y": null, - "z": null, - "a": null, - "b": null, - "c": null, - "e": null - }, - "links": { - "Board": { - "parent": null, - "mountPosition": [0, 0, 0], - "mountRotation": [0, 0, 0], - "model": [ - { - "stlFile": "surfaces/Board.stl", - "originOfModel": [0, 0, 0], - "rotationOfModelDegree": [0, 0, -90], - "material": "wood" - }, - { - "stlFile": "surfaces/BoardRail.stl", - "originOfModel": [0, 0, 0], - "rotationOfModelDegree": [0, 0, -90], - "material": "steel" - } - ] - }, - "Base": { - "parent": "Board", - "mountPosition": [0, 0, 0], - "mountRotation": [0, 0, 0], - "model": [ - { - "stlFile": "surfaces/Base.stl", - "originOfModel": [0, 0, 0], - "rotationOfModelDegree": [0, 0, 0], - "material": "plaWhite" - } - ], - "jointToParent": { - "name": "Slider", - "type": "linear", - "axis": [1, 0, 0], - "origin": [0, 0, 0], - "rotation": [0, 0, 0], - "variable": "x" - } - }, - "Arm1": { - "parent": "Base", - "mountPosition": [0, 0, 0], - "mountRotation": [0, 0, 0], - "model": [ - { - "stlFile": "surfaces/Holm.stl", - "originOfModel": [0, 0, 0], - "rotationOfModelDegree": [0, 0, 0], - "material": "powderCoatBlue" - } - ], - "jointToParent": { - "name": "Joint1", - "type": "revolute", - "axis": [1, 0, 0], - "origin": [0, 0, 0], - "rotation": [0, 0, 0], - "variable": "a" - } - } - } -} \ No newline at end of file diff --git a/robot_v01c.json b/robot_v01c.json deleted file mode 100644 index a280c3c..0000000 --- a/robot_v01c.json +++ /dev/null @@ -1,313 +0,0 @@ -{ - "coordinateSystem": { - "handedness": "right", - "x": "right", - "y": "backward", - "z": "up" - }, - "units": { - "length": "mm", - "rotation": "degree" - }, - "vision_config": { - "MarkerType": "DICT_4X4_250", - "MarkerSize": 0.025 - }, - "renderingInfo": { - "cameraPosition": [-150, -800, 600], - "cameraTarget": [200, 0, 60], - "cameraUpVector": [0, 0, 1], - "lightPosition": [-500, -500, 500], - "lightTarget": [0, 0, 0], - "lightUpVector": [0, 0, 1], - "metric": "mm", - "showSkeleton": true, - "showMarkers": true, - "backgroundColor": [0.70, 0.85, 1.0], - "backgroundStrength": 0.20, - "sunEnergy": 0.35, - "areaEnergy": 120, - "exposure": -1.5, - "materials": { - "wood": { - "baseColor": [0.72, 0.52, 0.33], - "roughness": 0.85, - "metallic": 0.0 - }, - "plaWhite": { - "baseColor": [0.95, 0.95, 0.95], - "roughness": 0.45, - "metallic": 0.0 - }, - "steel": { - "baseColor": [0.72, 0.72, 0.75], - "roughness": 0.25, - "metallic": 1.0 - }, - "powderCoatBlue": { - "baseColor": [0.15, 0.25, 0.7], - "roughness": 0.55, - "metallic": 0.0 - }, - "defaultPlastic": { - "baseColor": [0.95, 0.95, 0.95], - "roughness": 0.4, - "metallic": 0.0 - }, - "skeletonRed": { - "baseColor": [0.85, 0.20, 0.20], - "roughness": 0.35, - "metallic": 0.0 - }, - "markerBlack": { - "baseColor": [0.04, 0.04, 0.04], - "roughness": 0.80, - "metallic": 0.0 - } - }, - "skeletonDefaults": { - "radius": 4, - "color": [0.85, 0.20, 0.20] - }, - "markerDefaults": { - "size": 25, - "thickness": 1, - "color": [0.04, 0.04, 0.04] - } - }, - "defaultPosition": { - "x": 50, - "y": 30, - "z": -60, - "a": 220, - "b": 30, - "c": 70, - "e": 0 - }, - "recognized": { - "x": null, - "y": null, - "z": null, - "a": null, - "b": null, - "c": null, - "e": null - }, - "movements": { - "x": null, - "y": null, - "z": null, - "a": null, - "b": null, - "c": null, - "e": null - }, - "links": { - "Board": { - "parent": null, - "size": [1000, 200, 25], - "mountPosition": [0, 0, 0], - "mountRotation": [0, 0, 0], - "skeleton": { - "from": [0, 0, 16], - "to": [1000, 0, 16], - "radius": 4, - "color": [0.85, 0.20, 0.20] - }, - "markers": [ - ], - "model": [ - { - "stlFile": "surfaces/Board.stl", - "originOfModel": [0, 0, 0], - "rotationOfModelDegree": [0, 0, -90], - "material": "wood" - }, - { - "stlFile": "surfaces/BoardRail.stl", - "originOfModel": [0, 0, 0], - "rotationOfModelDegree": [0, 0, -90], - "material": "steel" - } - ] - }, - "Base": { - "parent": "Board", - "size": [150, 200, 150], - "mountPosition": [0, 0, 0], - "mountRotation": [0, 0, 0], - "jointToParent": { - "name": "Slider", - "type": "linear", - "axis": [1, 0, 0], - "origin": [0, 0, 16], - "rotation": [0, 0, 0], - "variable": "x" - }, - "skeleton": { - "from": [0, 108, 45], - "to": [110, 108, 45], - "radius": 4, - "color": [0.20, 0.80, 0.20] - }, - "markers": [ - ], - "model": [ - { - "stlFile": "surfaces/Base.stl", - "originOfModel": [-30, 0, -35], - "rotationOfModelDegree": [0, 0, 0], - "material": "plaWhite" - } - ] - }, - "Arm1": { - "parent": "Base", - "size": [70, 250, 70], - "mountPosition": [0, 0, 0], - "mountRotation": [0, 0, 0], - "jointToParent": { - "name": "Joint1", - "type": "revolute", - "axis": [-1, 0, 0], - "origin": [110, 108, 45], - "rotation": [0, 0, 0], - "variable": "y" - }, - "skeleton": { - "from": [0, 0, 0], - "to": [0, -250, 0], - "radius": 4, - "color": [0.20, 0.20, 0.90] - }, - "markers": [ - { - "id": 198, - "name": "aruco_198", - "position": [-89.5, -160, 35], - "normal": [0, 0, 1], - "size": 25 - }, - { - "id": 229, - "name": "aruco_229", - "position": [-89.5, -250, 35], - "normal": [0, 0, 1], - "size": 25 - }, - { - "id": 242, - "name": "aruco_242", - "position": [-89.5, -250, -35], - "normal": [0, 0, -1], - "size": 25 - }, - { - "id": 243, - "name": "aruco_243", - "position": [-89.5, -285, 0], - "normal": [0, -1, 0], - "size": 25 - } - ], - "model": [ - { - "stlFile": "surfaces/Holm.stl", - "originOfModel__": [-25,29,-28.5], - "originOfModel": [-29,25,28.5], - "rotationOfModelDegree__": [0, 0, 0], - "rotationOfModelDegree": [180, 0, -90], - "material": "powderCoatBlue" - } - ] - }, - "Ellbow": { - "parent": "Arm1", - - "mountPosition": [0, 0, 0], - "mountRotation": [0, 0, 0], - - "jointToParent": { - "name": "Joint2", - "type": "revolute", - "axis": [-1, 0, 0], - "origin": [0, -250, 0], - "rotation": [0, 0, 0], - "variable": "z" - }, - - "skeleton": { - "from": [0, 0, 0], - "to": [70, 0, 0], - "radius": 4, - "color": [0.90, 0.20, 0.20] - } - }, - "Arm2": { - "parent": "Ellbow", - - "mountPosition": [0, 0, 0], - "mountRotation": [0, 0, 0], - - "jointToParent": { - "name": "Joint3", - "type": "revolute", - "axis": [0, -1, 0], - "origin": [70, 0, 0], - "rotation": [0, 0, 0], - "variable": "a" - }, - - "skeleton": { - "from": [0, 0, 0], - "to": [0, -250, 0], - "radius": 4, - "color": [0.95, 0.85, 0.20] - } - }, - "Hand": { - "parent": "Arm2", - - "mountPosition": [0, 0, 0], - "mountRotation": [0, 0, 0], - - "jointToParent": { - "name": "Joint4", - "type": "revolute", - "axis": [1, 0, 0], - "origin": [0, -250, 0], - "rotation": [0, 0, 0], - "variable": "b" - }, - - "skeleton": { - "from": [0, 0, 0], - "to": [0, -35, 0], - "radius": 4, - "color": [0.95, 0.55, 0.15] - } - }, - "Palm": { - "parent": "Hand", - - "mountPosition": [0, 0, 0], - "mountRotation": [0, 0, 0], - - "jointToParent": { - "name": "Joint3", - "type": "revolute", - "axis": [0, -1, 0], - "origin": [0, 0, 0], - "rotation": [0, 0, 0], - "variable": "c" - }, - - "skeleton": { - "from": [-50, -35, 0], - "to": [50, -35, 0], - "radius": 7, - "color": [0.95, 0.20, 0.20] - } - } - } -} \ No newline at end of file