first commit
This commit is contained in:
246
json_to_urdf.py
Normal file
246
json_to_urdf.py
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# json_to_urdf.py
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from pathlib import Path
|
||||||
|
from xml.dom import minidom
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CONFIG
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
ROBOT_JSON = "robot.json"
|
||||||
|
OUTPUT_URDF = "robot.urdf"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# HELPERS
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def mm_to_m(v):
|
||||||
|
return float(v) / 1000.0
|
||||||
|
|
||||||
|
|
||||||
|
def vec_mm_to_m(v):
|
||||||
|
return [mm_to_m(x) for x in v]
|
||||||
|
|
||||||
|
|
||||||
|
def deg_to_rad(v):
|
||||||
|
return math.radians(float(v))
|
||||||
|
|
||||||
|
|
||||||
|
def vec_deg_to_rad(v):
|
||||||
|
return [deg_to_rad(x) for x in v]
|
||||||
|
|
||||||
|
|
||||||
|
def xyz_str(v):
|
||||||
|
return f"{v[0]:.6f} {v[1]:.6f} {v[2]:.6f}"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# LOAD JSON
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
with open(ROBOT_JSON, "r", encoding="utf-8") as f:
|
||||||
|
robot_json = json.load(f)
|
||||||
|
|
||||||
|
links = robot_json.get("links", {})
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ROOT
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
robot_name = Path(ROBOT_JSON).stem
|
||||||
|
robot = ET.Element("robot")
|
||||||
|
robot.set("name", robot_name)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CREATE LINKS
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
for link_name, link_info in links.items():
|
||||||
|
|
||||||
|
urdf_link = ET.SubElement(robot, "link")
|
||||||
|
urdf_link.set("name", link_name)
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# VISUALS
|
||||||
|
# --------------------------------------------------------
|
||||||
|
|
||||||
|
models = link_info.get("model", [])
|
||||||
|
|
||||||
|
for model in models:
|
||||||
|
|
||||||
|
stl_file = model.get("stlFile")
|
||||||
|
if not stl_file:
|
||||||
|
continue
|
||||||
|
|
||||||
|
visual = ET.SubElement(urdf_link, "visual")
|
||||||
|
|
||||||
|
origin = ET.SubElement(visual, "origin")
|
||||||
|
|
||||||
|
xyz = vec_mm_to_m(model.get("originOfModel", [0, 0, 0]))
|
||||||
|
rpy = vec_deg_to_rad(model.get("rotationOfModelDegree", [0, 0, 0]))
|
||||||
|
|
||||||
|
origin.set("xyz", xyz_str(xyz))
|
||||||
|
origin.set("rpy", xyz_str(rpy))
|
||||||
|
|
||||||
|
geometry = ET.SubElement(visual, "geometry")
|
||||||
|
|
||||||
|
mesh = ET.SubElement(geometry, "mesh")
|
||||||
|
mesh.set("filename", stl_file)
|
||||||
|
mesh.set("scale", "0.001 0.001 0.001")
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# SIMPLE COLLISION FROM SKELETON
|
||||||
|
# --------------------------------------------------------
|
||||||
|
|
||||||
|
skeleton = link_info.get("skeleton")
|
||||||
|
|
||||||
|
if isinstance(skeleton, dict):
|
||||||
|
|
||||||
|
p1 = skeleton.get("from", [0, 0, 0])
|
||||||
|
p2 = skeleton.get("to", [0, 0, 0])
|
||||||
|
|
||||||
|
dx = p2[0] - p1[0]
|
||||||
|
dy = p2[1] - p1[1]
|
||||||
|
dz = p2[2] - p1[2]
|
||||||
|
|
||||||
|
length_mm = math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||||
|
length_m = mm_to_m(length_mm)
|
||||||
|
|
||||||
|
radius_mm = skeleton.get("radius", 4)
|
||||||
|
radius_m = mm_to_m(radius_mm)
|
||||||
|
|
||||||
|
cx = (p1[0] + p2[0]) * 0.5
|
||||||
|
cy = (p1[1] + p2[1]) * 0.5
|
||||||
|
cz = (p1[2] + p2[2]) * 0.5
|
||||||
|
|
||||||
|
collision = ET.SubElement(urdf_link, "collision")
|
||||||
|
|
||||||
|
collision_origin = ET.SubElement(collision, "origin")
|
||||||
|
collision_origin.set(
|
||||||
|
"xyz",
|
||||||
|
xyz_str(vec_mm_to_m([cx, cy, cz]))
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# CYLINDER ORIENTATION
|
||||||
|
# URDF cylinder points along Z
|
||||||
|
# ----------------------------------------------------
|
||||||
|
|
||||||
|
roll = 0.0
|
||||||
|
pitch = 0.0
|
||||||
|
yaw = 0.0
|
||||||
|
|
||||||
|
if abs(dx) >= abs(dy) and abs(dx) >= abs(dz):
|
||||||
|
pitch = math.radians(90)
|
||||||
|
yaw = 0.0
|
||||||
|
|
||||||
|
elif abs(dy) >= abs(dx) and abs(dy) >= abs(dz):
|
||||||
|
pitch = math.radians(90)
|
||||||
|
yaw = math.radians(90)
|
||||||
|
|
||||||
|
collision_origin.set(
|
||||||
|
"rpy",
|
||||||
|
xyz_str([roll, pitch, yaw])
|
||||||
|
)
|
||||||
|
|
||||||
|
geometry = ET.SubElement(collision, "geometry")
|
||||||
|
|
||||||
|
cylinder = ET.SubElement(geometry, "cylinder")
|
||||||
|
cylinder.set("radius", f"{radius_m:.6f}")
|
||||||
|
cylinder.set("length", f"{length_m:.6f}")
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CREATE JOINTS
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
for link_name, link_info in links.items():
|
||||||
|
|
||||||
|
parent_name = link_info.get("parent")
|
||||||
|
|
||||||
|
if parent_name is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
joint_info = link_info.get("jointToParent", {})
|
||||||
|
|
||||||
|
joint_name = joint_info.get("name", f"joint_{parent_name}_{link_name}")
|
||||||
|
joint_type = joint_info.get("type", "fixed")
|
||||||
|
|
||||||
|
if joint_type == "linear":
|
||||||
|
urdf_joint_type = "prismatic"
|
||||||
|
elif joint_type == "revolute":
|
||||||
|
urdf_joint_type = "revolute"
|
||||||
|
else:
|
||||||
|
urdf_joint_type = "fixed"
|
||||||
|
|
||||||
|
joint = ET.SubElement(robot, "joint")
|
||||||
|
joint.set("name", joint_name)
|
||||||
|
joint.set("type", urdf_joint_type)
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# PARENT / CHILD
|
||||||
|
# --------------------------------------------------------
|
||||||
|
|
||||||
|
parent = ET.SubElement(joint, "parent")
|
||||||
|
parent.set("link", parent_name)
|
||||||
|
|
||||||
|
child = ET.SubElement(joint, "child")
|
||||||
|
child.set("link", link_name)
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# ORIGIN
|
||||||
|
# --------------------------------------------------------
|
||||||
|
|
||||||
|
origin_xyz = joint_info.get("origin", [0, 0, 0])
|
||||||
|
origin_rpy = joint_info.get("rotation", [0, 0, 0])
|
||||||
|
|
||||||
|
origin = ET.SubElement(joint, "origin")
|
||||||
|
origin.set(
|
||||||
|
"xyz",
|
||||||
|
xyz_str(vec_mm_to_m(origin_xyz))
|
||||||
|
)
|
||||||
|
origin.set(
|
||||||
|
"rpy",
|
||||||
|
xyz_str(vec_deg_to_rad(origin_rpy))
|
||||||
|
)
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# AXIS
|
||||||
|
# --------------------------------------------------------
|
||||||
|
|
||||||
|
axis = joint_info.get("axis", [1, 0, 0])
|
||||||
|
|
||||||
|
axis_tag = ET.SubElement(joint, "axis")
|
||||||
|
axis_tag.set("xyz", xyz_str(axis))
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# DEFAULT LIMITS
|
||||||
|
# --------------------------------------------------------
|
||||||
|
|
||||||
|
if urdf_joint_type in ["revolute", "prismatic"]:
|
||||||
|
|
||||||
|
limit = ET.SubElement(joint, "limit")
|
||||||
|
|
||||||
|
if urdf_joint_type == "revolute":
|
||||||
|
limit.set("lower", f"{-math.pi:.6f}")
|
||||||
|
limit.set("upper", f"{math.pi:.6f}")
|
||||||
|
else:
|
||||||
|
limit.set("lower", "-1.0")
|
||||||
|
limit.set("upper", "1.0")
|
||||||
|
|
||||||
|
limit.set("effort", "100")
|
||||||
|
limit.set("velocity", "10")
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# PRETTY XML
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
xml_string = ET.tostring(robot, encoding="utf-8")
|
||||||
|
pretty = minidom.parseString(xml_string).toprettyxml(indent=" ")
|
||||||
|
|
||||||
|
with open(OUTPUT_URDF, "w", encoding="utf-8") as f:
|
||||||
|
f.write(pretty)
|
||||||
|
|
||||||
|
print("URDF written:", OUTPUT_URDF)
|
||||||
BIN
render.npz
Normal file
BIN
render.npz
Normal file
Binary file not shown.
BIN
render.png
Normal file
BIN
render.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
render0.png
Normal file
BIN
render0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
BIN
render01a.png
Normal file
BIN
render01a.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
760
render_robot.py
Normal file
760
render_robot.py
Normal file
@@ -0,0 +1,760 @@
|
|||||||
|
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 = 1280
|
||||||
|
RENDER_HEIGHT = 720
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 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
|
||||||
|
|
||||||
|
RENDER_WIDTH = int(rendering_info.get("width", 1200))
|
||||||
|
RENDER_HEIGHT = int(rendering_info.get("height", 800))
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# EXPORT CAMERA CALIBRATION (.npz)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
CALIBRATION_OUTPUT = str(
|
||||||
|
Path(OUTPUT_FILE).with_suffix(".npz")
|
||||||
|
)
|
||||||
|
|
||||||
|
render = scene.render
|
||||||
|
cam = cam_obj.data
|
||||||
|
|
||||||
|
width_px = render.resolution_x
|
||||||
|
height_px = render.resolution_y
|
||||||
|
|
||||||
|
scale = render.resolution_percentage / 100.0
|
||||||
|
|
||||||
|
width_px *= scale
|
||||||
|
height_px *= scale
|
||||||
|
|
||||||
|
sensor_width_mm = cam.sensor_width
|
||||||
|
sensor_height_mm = cam.sensor_height
|
||||||
|
|
||||||
|
focal_mm = cam.lens
|
||||||
|
|
||||||
|
# focal length in pixels
|
||||||
|
fx = (width_px * focal_mm) / sensor_width_mm
|
||||||
|
fy = (height_px * focal_mm) / sensor_height_mm
|
||||||
|
|
||||||
|
cx = width_px / 2.0
|
||||||
|
cy = height_px / 2.0
|
||||||
|
|
||||||
|
camera_matrix = np.array([
|
||||||
|
[fx, 0, cx],
|
||||||
|
[0, fy, cy],
|
||||||
|
[0, 0, 1]
|
||||||
|
], dtype=np.float32)
|
||||||
|
|
||||||
|
# ideal synthetic camera
|
||||||
|
dist_coeffs = np.zeros((5, 1), dtype=np.float32)
|
||||||
|
|
||||||
|
np.savez(
|
||||||
|
CALIBRATION_OUTPUT,
|
||||||
|
|
||||||
|
# common names
|
||||||
|
camera_matrix=camera_matrix,
|
||||||
|
dist_coeffs=dist_coeffs,
|
||||||
|
|
||||||
|
# compatibility aliases
|
||||||
|
K=camera_matrix,
|
||||||
|
mtx=camera_matrix,
|
||||||
|
dist=dist_coeffs
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Saved camera calibration:", CALIBRATION_OUTPUT)
|
||||||
|
print(camera_matrix)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 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
|
||||||
|
# --------------------------------------------------------
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
def create_aruco_material(marker_id: int, marker_name: str):
|
||||||
|
dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_250)
|
||||||
|
|
||||||
|
img = np.zeros((256, 256), dtype=np.uint8)
|
||||||
|
cv2.aruco.generateImageMarker(dictionary, marker_id, 256, img, 1)
|
||||||
|
|
||||||
|
tmpfile = Path(tempfile.gettempdir()) / f"aruco_{marker_id}.png"
|
||||||
|
cv2.imwrite(str(tmpfile), img)
|
||||||
|
|
||||||
|
image = bpy.data.images.load(str(tmpfile))
|
||||||
|
|
||||||
|
mat = bpy.data.materials.new(name=f"{marker_name}_mat")
|
||||||
|
mat.use_nodes = True
|
||||||
|
|
||||||
|
nodes = mat.node_tree.nodes
|
||||||
|
links = mat.node_tree.links
|
||||||
|
nodes.clear()
|
||||||
|
|
||||||
|
out = nodes.new(type="ShaderNodeOutputMaterial")
|
||||||
|
bsdf = nodes.new(type="ShaderNodeBsdfPrincipled")
|
||||||
|
tex = nodes.new(type="ShaderNodeTexImage")
|
||||||
|
|
||||||
|
tex.image = image
|
||||||
|
|
||||||
|
links.new(tex.outputs["Color"], bsdf.inputs["Base Color"])
|
||||||
|
links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
|
||||||
|
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
def normal_to_quaternion(normal_vec):
|
||||||
|
normal = mathutils.Vector(normal_vec).normalized()
|
||||||
|
|
||||||
|
default_normal = mathutils.Vector((0, 0, 1))
|
||||||
|
|
||||||
|
return default_normal.rotation_difference(normal)
|
||||||
|
|
||||||
|
|
||||||
|
if show_markers:
|
||||||
|
|
||||||
|
marker_defaults = rendering_info.get("markerDefaults", {}) or {}
|
||||||
|
|
||||||
|
for m in link_info.get("markers", []):
|
||||||
|
|
||||||
|
if not isinstance(m, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
marker_id = int(m.get("id", 0))
|
||||||
|
|
||||||
|
marker_name = m.get(
|
||||||
|
"name",
|
||||||
|
f"{link_name}_marker_{marker_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
marker_size_mm = float(
|
||||||
|
m.get(
|
||||||
|
"size",
|
||||||
|
marker_defaults.get("size", 25)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
marker_pos = resolve_vec3_m(
|
||||||
|
m.get("position", [0, 0, 0]),
|
||||||
|
state
|
||||||
|
)
|
||||||
|
|
||||||
|
normal = m.get("normal", [0, 0, 1])
|
||||||
|
marker_spin_deg = float(m.get("spin", 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.rotation_mode = "QUATERNION"
|
||||||
|
base_quat = normal_to_quaternion(normal)
|
||||||
|
|
||||||
|
spin_quat = mathutils.Quaternion(
|
||||||
|
mathutils.Vector((0, 0, 1)),
|
||||||
|
math.radians(marker_spin_deg)
|
||||||
|
)
|
||||||
|
|
||||||
|
marker_obj.rotation_quaternion = (
|
||||||
|
base_quat @ spin_quat
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Marker-Normale im Welt-/Linkraum
|
||||||
|
normal_world = (
|
||||||
|
marker_obj.matrix_world.to_quaternion()
|
||||||
|
@ mathutils.Vector((0, 0, 1))
|
||||||
|
)
|
||||||
|
normal_world.normalize()
|
||||||
|
|
||||||
|
# minimal vorziehen gegen Z-Fighting
|
||||||
|
marker_obj.location = (
|
||||||
|
mathutils.Vector(marker_pos)
|
||||||
|
+ normal_world * mm_to_m(0.05)
|
||||||
|
)
|
||||||
|
|
||||||
|
marker_mat = create_aruco_material(
|
||||||
|
marker_id,
|
||||||
|
marker_name
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(marker_obj.data.materials) == 0:
|
||||||
|
marker_obj.data.materials.append(marker_mat)
|
||||||
|
else:
|
||||||
|
marker_obj.data.materials[0] = marker_mat
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# BACKING PLATE (white PLA behind marker)
|
||||||
|
# --------------------------------------------------------
|
||||||
|
|
||||||
|
plate_side_mm = 28.0
|
||||||
|
plate_thickness_mm = 1.0
|
||||||
|
gap_mm = 0.2 # kleiner Abstand gegen Z-Fighting
|
||||||
|
|
||||||
|
bpy.ops.mesh.primitive_cube_add(size=1.0)
|
||||||
|
plate_obj = bpy.context.active_object
|
||||||
|
plate_obj.name = marker_name + "_plate"
|
||||||
|
|
||||||
|
safe_parent(plate_obj, link_frame, keep_world=False)
|
||||||
|
|
||||||
|
# gleiche Orientierung wie der Marker
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Platte liegt "hinter" dem Marker
|
||||||
|
plate_obj.location = (
|
||||||
|
marker_obj.location
|
||||||
|
- normal_world * mm_to_m((plate_thickness_mm * 0.5) + gap_mm)
|
||||||
|
)
|
||||||
|
|
||||||
|
# exakte Abmessungen: 26 x 26 x 1 mm
|
||||||
|
plate_obj.dimensions = (
|
||||||
|
mm_to_m(plate_side_mm),
|
||||||
|
mm_to_m(plate_side_mm),
|
||||||
|
mm_to_m(plate_thickness_mm)
|
||||||
|
)
|
||||||
|
|
||||||
|
pla_mat = create_or_get_material("plaWhite")
|
||||||
|
if len(plate_obj.data.materials) == 0:
|
||||||
|
plate_obj.data.materials.append(pla_mat)
|
||||||
|
else:
|
||||||
|
plate_obj.data.materials[0] = pla_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)
|
||||||
365
render_robot_v00.py
Normal file
365
render_robot_v00.py
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
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)
|
||||||
580
render_robot_v01a.py
Normal file
580
render_robot_v01a.py
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
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)
|
||||||
595
render_robot_v01b.py
Normal file
595
render_robot_v01b.py
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
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)
|
||||||
561
render_robot_v01c.py
Normal file
561
render_robot_v01c.py
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
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)
|
||||||
BIN
render_v01c.png
Normal file
BIN
render_v01c.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
360
robot.json
Normal file
360
robot.json
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
{
|
||||||
|
"coordinateSystem": {
|
||||||
|
"handedness": "right",
|
||||||
|
"x": "right",
|
||||||
|
"y": "backward",
|
||||||
|
"z": "up"
|
||||||
|
},
|
||||||
|
"units": {
|
||||||
|
"length": "mm",
|
||||||
|
"rotation": "degree"
|
||||||
|
},
|
||||||
|
"vision_config": {
|
||||||
|
"MarkerType": "DICT_4X4_250",
|
||||||
|
"MarkerSize": 0.025
|
||||||
|
},
|
||||||
|
"renderingInfo": {
|
||||||
|
"width": 1280,
|
||||||
|
"height": 720,
|
||||||
|
"cameraPosition": [-150, -400, 800],
|
||||||
|
"cameraTarget": [230, -180, 180],
|
||||||
|
"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": 150,
|
||||||
|
"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": [0, -160, 35],
|
||||||
|
"normal": [0, 0, 1],
|
||||||
|
"size": 25,
|
||||||
|
"spin": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 229,
|
||||||
|
"name": "aruco_229",
|
||||||
|
"position": [0, -250, 35],
|
||||||
|
"normal": [0, 0, 1],
|
||||||
|
"size": 25,
|
||||||
|
"spin": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 242,
|
||||||
|
"name": "aruco_242",
|
||||||
|
"position": [0, -250, -35],
|
||||||
|
"normal": [0, 0, -1],
|
||||||
|
"size": 25,
|
||||||
|
"spin": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 243,
|
||||||
|
"name": "aruco_243",
|
||||||
|
"position": [0, -285, 0],
|
||||||
|
"normal": [0, -1, 0],
|
||||||
|
"size": 25,
|
||||||
|
"spin": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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": [90, 0, 0],
|
||||||
|
"radius": 4,
|
||||||
|
"color": [0.90, 0.20, 0.20]
|
||||||
|
},
|
||||||
|
"model": [
|
||||||
|
{
|
||||||
|
"stlFile": "surfaces/Ellebogen.stl",
|
||||||
|
"originOfModel": [90,0,0],
|
||||||
|
"rotationOfModelDegree": [0,-90,-90],
|
||||||
|
"material": "defaultPlastic"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"markers": [
|
||||||
|
{
|
||||||
|
"id": 244,
|
||||||
|
"name": "aruco_244",
|
||||||
|
"position": [125, 0, 0],
|
||||||
|
"normal": [1, 0, 0],
|
||||||
|
"size": 25,
|
||||||
|
"spin": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 245,
|
||||||
|
"name": "aruco_245",
|
||||||
|
"position": [90, 0, -35],
|
||||||
|
"normal": [0, 0, -1],
|
||||||
|
"size": 25,
|
||||||
|
"spin": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 246,
|
||||||
|
"name": "aruco_246",
|
||||||
|
"position": [90, 0, 35],
|
||||||
|
"normal": [0, 0, 1],
|
||||||
|
"size": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 247,
|
||||||
|
"name": "aruco_247",
|
||||||
|
"position": [52.5, 0, 35],
|
||||||
|
"normal": [0, 0, 1],
|
||||||
|
"size": 25
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
},
|
||||||
|
"Arm2": {
|
||||||
|
"parent": "Ellbow",
|
||||||
|
|
||||||
|
"mountPosition": [0, 0, 0],
|
||||||
|
"mountRotation": [0, 0, 0],
|
||||||
|
|
||||||
|
"jointToParent": {
|
||||||
|
"name": "Joint3",
|
||||||
|
"type": "revolute",
|
||||||
|
"axis": [0, -1, 0],
|
||||||
|
"origin": [90, 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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
robot.urdf
Normal file
95
robot.urdf
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
<robot name="robot">
|
||||||
|
<link name="Board">
|
||||||
|
<visual>
|
||||||
|
<origin xyz="0.000000 0.000000 0.000000" rpy="0.000000 0.000000 -1.570796"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="surfaces/Board.stl" scale="0.001 0.001 0.001"/>
|
||||||
|
</geometry>
|
||||||
|
</visual>
|
||||||
|
<visual>
|
||||||
|
<origin xyz="0.000000 0.000000 0.000000" rpy="0.000000 0.000000 -1.570796"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="surfaces/BoardRail.stl" scale="0.001 0.001 0.001"/>
|
||||||
|
</geometry>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin xyz="0.500000 0.000000 0.016000" rpy="0.000000 1.570796 0.000000"/>
|
||||||
|
<geometry>
|
||||||
|
<cylinder radius="0.004000" length="1.000000"/>
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<link name="Base">
|
||||||
|
<visual>
|
||||||
|
<origin xyz="-0.030000 0.000000 -0.035000" rpy="0.000000 0.000000 0.000000"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="surfaces/Base.stl" scale="0.001 0.001 0.001"/>
|
||||||
|
</geometry>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin xyz="0.055000 0.108000 0.045000" rpy="0.000000 1.570796 0.000000"/>
|
||||||
|
<geometry>
|
||||||
|
<cylinder radius="0.004000" length="0.110000"/>
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<link name="Arm1">
|
||||||
|
<visual>
|
||||||
|
<origin xyz="-0.029000 0.025000 0.028500" rpy="3.141593 0.000000 -1.570796"/>
|
||||||
|
<geometry>
|
||||||
|
<mesh filename="surfaces/Holm.stl" scale="0.001 0.001 0.001"/>
|
||||||
|
</geometry>
|
||||||
|
</visual>
|
||||||
|
<collision>
|
||||||
|
<origin xyz="0.000000 -0.125000 0.000000" rpy="0.000000 1.570796 1.570796"/>
|
||||||
|
<geometry>
|
||||||
|
<cylinder radius="0.004000" length="0.250000"/>
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<link name="Ellbow">
|
||||||
|
<collision>
|
||||||
|
<origin xyz="0.035000 0.000000 0.000000" rpy="0.000000 1.570796 0.000000"/>
|
||||||
|
<geometry>
|
||||||
|
<cylinder radius="0.004000" length="0.070000"/>
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<link name="Arm2">
|
||||||
|
<collision>
|
||||||
|
<origin xyz="0.000000 -0.125000 0.000000" rpy="0.000000 1.570796 1.570796"/>
|
||||||
|
<geometry>
|
||||||
|
<cylinder radius="0.004000" length="0.250000"/>
|
||||||
|
</geometry>
|
||||||
|
</collision>
|
||||||
|
</link>
|
||||||
|
<joint name="Slider" type="prismatic">
|
||||||
|
<parent link="Board"/>
|
||||||
|
<child link="Base"/>
|
||||||
|
<origin xyz="0.000000 0.000000 0.016000" rpy="0.000000 0.000000 0.000000"/>
|
||||||
|
<axis xyz="1.000000 0.000000 0.000000"/>
|
||||||
|
<limit lower="-1.0" upper="1.0" effort="100" velocity="10"/>
|
||||||
|
</joint>
|
||||||
|
<joint name="Joint1" type="revolute">
|
||||||
|
<parent link="Base"/>
|
||||||
|
<child link="Arm1"/>
|
||||||
|
<origin xyz="0.110000 0.108000 0.045000" rpy="0.000000 0.000000 0.000000"/>
|
||||||
|
<axis xyz="-1.000000 0.000000 0.000000"/>
|
||||||
|
<limit lower="-3.141593" upper="3.141593" effort="100" velocity="10"/>
|
||||||
|
</joint>
|
||||||
|
<joint name="Joint2" type="revolute">
|
||||||
|
<parent link="Arm1"/>
|
||||||
|
<child link="Ellbow"/>
|
||||||
|
<origin xyz="0.000000 -0.250000 0.000000" rpy="0.000000 0.000000 0.000000"/>
|
||||||
|
<axis xyz="-1.000000 0.000000 0.000000"/>
|
||||||
|
<limit lower="-3.141593" upper="3.141593" effort="100" velocity="10"/>
|
||||||
|
</joint>
|
||||||
|
<joint name="Joint3" type="revolute">
|
||||||
|
<parent link="Ellbow"/>
|
||||||
|
<child link="Arm2"/>
|
||||||
|
<origin xyz="0.070000 0.000000 0.000000" rpy="0.000000 0.000000 0.000000"/>
|
||||||
|
<axis xyz="0.000000 -1.000000 0.000000"/>
|
||||||
|
<limit lower="-3.141593" upper="3.141593" effort="100" velocity="10"/>
|
||||||
|
</joint>
|
||||||
|
</robot>
|
||||||
334
robot_commented.json
Normal file
334
robot_commented.json
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Robot definition file for Blender/Robot/URDF-style kinematic rendering",
|
||||||
|
|
||||||
|
"coordinateSystem": {
|
||||||
|
"_comment": "Global coordinate system definition",
|
||||||
|
|
||||||
|
"system": "right-handed",
|
||||||
|
|
||||||
|
"axes": {
|
||||||
|
"x": "right",
|
||||||
|
"y": "backward",
|
||||||
|
"z": "up"
|
||||||
|
},
|
||||||
|
|
||||||
|
"_important": [
|
||||||
|
"This coordinate system is intentionally identical to Blender.",
|
||||||
|
"All positions are expressed in millimeters.",
|
||||||
|
"All rotations are expressed in degrees.",
|
||||||
|
"Positive rotations follow the right-hand rule."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"units": {
|
||||||
|
"length": "mm",
|
||||||
|
"rotation": "degree"
|
||||||
|
},
|
||||||
|
|
||||||
|
"renderingInfo": {
|
||||||
|
"_comment": "Pure rendering settings. Does NOT affect robot kinematics.",
|
||||||
|
|
||||||
|
"cameraPosition": [-400, -700, 300],
|
||||||
|
|
||||||
|
"cameraTarget": [0, 0, 150],
|
||||||
|
|
||||||
|
"_cameraTargetInfo": [
|
||||||
|
"cameraTarget defines the world-space point the camera looks at.",
|
||||||
|
"Values may also reference state variables like 'x'."
|
||||||
|
],
|
||||||
|
|
||||||
|
"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.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.70],
|
||||||
|
"roughness": 0.55,
|
||||||
|
"metallic": 0.0
|
||||||
|
},
|
||||||
|
|
||||||
|
"defaultPlastic": {
|
||||||
|
"baseColor": [0.95, 0.95, 0.95],
|
||||||
|
"roughness": 0.40,
|
||||||
|
"metallic": 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"defaultPosition": {
|
||||||
|
"_comment": "Robot zero/home position",
|
||||||
|
|
||||||
|
"_important": [
|
||||||
|
"These values define the robot's neutral pose.",
|
||||||
|
"All joints are evaluated relative to this pose."
|
||||||
|
],
|
||||||
|
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"z": 0,
|
||||||
|
|
||||||
|
"a": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": 0,
|
||||||
|
|
||||||
|
"e": 0
|
||||||
|
},
|
||||||
|
|
||||||
|
"recognized": {
|
||||||
|
"_comment": "Pose reconstructed from machine vision / markers",
|
||||||
|
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"z": null,
|
||||||
|
|
||||||
|
"a": null,
|
||||||
|
"b": null,
|
||||||
|
"c": null,
|
||||||
|
|
||||||
|
"e": null
|
||||||
|
},
|
||||||
|
|
||||||
|
"movements": {
|
||||||
|
"_comment": "Current commanded movement state (e.g. from GCode)",
|
||||||
|
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"z": null,
|
||||||
|
|
||||||
|
"a": null,
|
||||||
|
"b": null,
|
||||||
|
"c": null,
|
||||||
|
|
||||||
|
"e": null
|
||||||
|
},
|
||||||
|
|
||||||
|
"_statePriority": [
|
||||||
|
"movements overrides recognized",
|
||||||
|
"recognized overrides defaultPosition",
|
||||||
|
"defaultPosition overrides zero"
|
||||||
|
],
|
||||||
|
|
||||||
|
"links": {
|
||||||
|
"Board": {
|
||||||
|
"_comment": "Static world/base object",
|
||||||
|
|
||||||
|
"parent": null,
|
||||||
|
|
||||||
|
"_mountPositionMeaning": [
|
||||||
|
"Position of THIS LINK coordinate system",
|
||||||
|
"relative to the PARENT LINK coordinate system"
|
||||||
|
],
|
||||||
|
|
||||||
|
"mountPosition": [0, 0, 0],
|
||||||
|
|
||||||
|
"_mountRotationMeaning": [
|
||||||
|
"Mechanical installation rotation",
|
||||||
|
"Defines how the LINK coordinate system",
|
||||||
|
"is rotated relative to its parent link",
|
||||||
|
"This is NOT the joint movement."
|
||||||
|
],
|
||||||
|
|
||||||
|
"mountRotation": [0, 0, 0],
|
||||||
|
|
||||||
|
"_modelMeaning": [
|
||||||
|
"One link may consist of multiple STL surfaces.",
|
||||||
|
"Each STL is positioned relative to the LINK coordinate system."
|
||||||
|
],
|
||||||
|
|
||||||
|
"model": [
|
||||||
|
{
|
||||||
|
"_comment": "Visual geometry only",
|
||||||
|
|
||||||
|
"stlFile": "surfaces/Board.stl",
|
||||||
|
|
||||||
|
"_originOfModelMeaning": [
|
||||||
|
"Position of STL inside the LINK coordinate system",
|
||||||
|
"Purely visual adjustment",
|
||||||
|
"Used to compensate CAD export offsets"
|
||||||
|
],
|
||||||
|
|
||||||
|
"originOfModel": [0, 0, 0],
|
||||||
|
|
||||||
|
"_rotationOfModelMeaning": [
|
||||||
|
"Rotation of STL inside LINK coordinate system",
|
||||||
|
"Purely visual adjustment",
|
||||||
|
"Used to compensate CAD export orientation"
|
||||||
|
],
|
||||||
|
|
||||||
|
"rotationOfModelDegree": [0, 0, 90],
|
||||||
|
|
||||||
|
"material": "wood"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Base": {
|
||||||
|
"_comment": "Linear axis mounted on Board",
|
||||||
|
|
||||||
|
"parent": "Board",
|
||||||
|
|
||||||
|
"mountPosition": [0, 0, 25],
|
||||||
|
|
||||||
|
"mountRotation": [0, 0, 0],
|
||||||
|
|
||||||
|
"_jointToParentMeaning": [
|
||||||
|
"Defines the kinematic transformation",
|
||||||
|
"between parent link and this link"
|
||||||
|
],
|
||||||
|
|
||||||
|
"jointToParent": {
|
||||||
|
"name": "Slider",
|
||||||
|
|
||||||
|
"_jointTypeMeaning": [
|
||||||
|
"fixed = no movement",
|
||||||
|
"linear = translational movement",
|
||||||
|
"revolute = rotational movement"
|
||||||
|
],
|
||||||
|
|
||||||
|
"type": "linear",
|
||||||
|
|
||||||
|
"_axisMeaning": [
|
||||||
|
"Joint movement axis in LOCAL joint coordinates",
|
||||||
|
"[1,0,0] = local X axis",
|
||||||
|
"[0,1,0] = local Y axis",
|
||||||
|
"[0,0,1] = local Z axis"
|
||||||
|
],
|
||||||
|
|
||||||
|
"axis": [1, 0, 0],
|
||||||
|
|
||||||
|
"_originMeaning": [
|
||||||
|
"Position of joint coordinate system",
|
||||||
|
"inside the parent link coordinate system"
|
||||||
|
],
|
||||||
|
|
||||||
|
"origin": [0, 0, 0],
|
||||||
|
|
||||||
|
"_rotationMeaning": [
|
||||||
|
"Orientation of joint coordinate system",
|
||||||
|
"inside the parent link coordinate system"
|
||||||
|
],
|
||||||
|
|
||||||
|
"rotation": [0, 0, 0],
|
||||||
|
|
||||||
|
"_variableMeaning": [
|
||||||
|
"Maps robot state variable",
|
||||||
|
"to this joint"
|
||||||
|
],
|
||||||
|
|
||||||
|
"variable": "x"
|
||||||
|
},
|
||||||
|
|
||||||
|
"model": [
|
||||||
|
{
|
||||||
|
"stlFile": "surfaces/Base.stl",
|
||||||
|
|
||||||
|
"originOfModel": [0, 0, 0],
|
||||||
|
|
||||||
|
"rotationOfModelDegree": [0, 0, 0],
|
||||||
|
|
||||||
|
"material": "plaWhite"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Arm1": {
|
||||||
|
"_comment": "Rotational arm",
|
||||||
|
|
||||||
|
"parent": "Base",
|
||||||
|
|
||||||
|
"mountPosition": [150, 0, 150],
|
||||||
|
|
||||||
|
"mountRotation": [0, 0, 0],
|
||||||
|
|
||||||
|
"jointToParent": {
|
||||||
|
"name": "Joint1",
|
||||||
|
|
||||||
|
"type": "revolute",
|
||||||
|
|
||||||
|
"_important": [
|
||||||
|
"Positive rotation follows right-hand rule."
|
||||||
|
],
|
||||||
|
|
||||||
|
"axis": [1, 0, 0],
|
||||||
|
|
||||||
|
"origin": [0, 0, 0],
|
||||||
|
|
||||||
|
"rotation": [0, 0, 0],
|
||||||
|
|
||||||
|
"variable": "a"
|
||||||
|
},
|
||||||
|
|
||||||
|
"model": [
|
||||||
|
{
|
||||||
|
"stlFile": "surfaces/Holm.stl",
|
||||||
|
|
||||||
|
"originOfModel": [0, 0, 0],
|
||||||
|
|
||||||
|
"rotationOfModelDegree": [0, 0, 0],
|
||||||
|
|
||||||
|
"material": "powderCoatBlue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"_kinematicStructure": {
|
||||||
|
"_comment": "Conceptual hierarchy",
|
||||||
|
|
||||||
|
"hierarchy": [
|
||||||
|
"ParentLink",
|
||||||
|
"-> JointOrigin",
|
||||||
|
"-> JointMotion",
|
||||||
|
"-> ChildLink",
|
||||||
|
"-> VisualMeshes"
|
||||||
|
],
|
||||||
|
|
||||||
|
"_important": [
|
||||||
|
"All children automatically inherit parent movement.",
|
||||||
|
"This creates full forward kinematics automatically.",
|
||||||
|
"No manual propagation of child transformations is required."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"_futureExtensions": {
|
||||||
|
"_comment": "Planned future capabilities",
|
||||||
|
|
||||||
|
"possibleFeatures": [
|
||||||
|
"marker definitions",
|
||||||
|
"inverse kinematics",
|
||||||
|
"collision geometry",
|
||||||
|
"joint limits",
|
||||||
|
"joint damping",
|
||||||
|
"mass/inertia",
|
||||||
|
"vision calibration",
|
||||||
|
"urdf export",
|
||||||
|
"gcode import",
|
||||||
|
"trajectory playback"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
138
robot_v01a.json
Normal file
138
robot_v01a.json
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
313
robot_v01c.json
Normal file
313
robot_v01c.json
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
{
|
||||||
|
"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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
run.bat
Normal file
1
run.bat
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"C:\Program Files\Blender Foundation\Blender 4.5\blender.exe" -b --python render_robot.py --log-level 2
|
||||||
BIN
surfaces/Base.stl
Normal file
BIN
surfaces/Base.stl
Normal file
Binary file not shown.
BIN
surfaces/Board.stl
Normal file
BIN
surfaces/Board.stl
Normal file
Binary file not shown.
BIN
surfaces/BoardRail.stl
Normal file
BIN
surfaces/BoardRail.stl
Normal file
Binary file not shown.
BIN
surfaces/Ellebogen.3mf
Normal file
BIN
surfaces/Ellebogen.3mf
Normal file
Binary file not shown.
BIN
surfaces/Ellebogen.stl
Normal file
BIN
surfaces/Ellebogen.stl
Normal file
Binary file not shown.
BIN
surfaces/Holm.stl
Normal file
BIN
surfaces/Holm.stl
Normal file
Binary file not shown.
BIN
surfaces/Unterarm.3mf
Normal file
BIN
surfaces/Unterarm.3mf
Normal file
Binary file not shown.
BIN
surfaces/Unterarm.stl
Normal file
BIN
surfaces/Unterarm.stl
Normal file
Binary file not shown.
Reference in New Issue
Block a user