first commit
This commit is contained in:
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)
|
||||
Reference in New Issue
Block a user