RoboMME / src /robomme /robomme_env /utils /object_generation.py
HongzeFu's picture
HF Space: code-only (no binary assets)
06c11b0
import numpy as np
import torch
import sapien
from typing import Optional, Tuple, Sequence, Union
from mani_skill.utils.structs.pose import Pose
from mani_skill.utils.geometry.rotation_conversions import (
euler_angles_to_matrix,
matrix_to_quaternion,
)
import mani_skill.envs.utils.randomization as randomization # Only used if needed elsewhere
from mani_skill.examples.motionplanning.base_motionplanner.utils import (
compute_grasp_info_by_obb,
get_actor_obb,
)
from mani_skill.utils.building import actors
from mani_skill.utils.geometry.rotation_conversions import (
euler_angles_to_matrix,
matrix_to_quaternion,
)
from transforms3d.euler import euler2quat
from mani_skill.utils import sapien_utils
from mani_skill.envs.scene import ManiSkillScene
from mani_skill.utils.building.actor_builder import ActorBuilder
from mani_skill.utils.structs.pose import Pose
from mani_skill.utils.structs.types import Array
from typing import Optional, Union
def _color_to_rgba(color: Union[str, Sequence[float]]) -> Tuple[float, float, float, float]:
"""Convert a hex string or RGB/RGBA tuple to an RGBA tuple accepted by SAPIEN."""
if isinstance(color, str):
return sapien_utils.hex2rgba(color)
if len(color) == 3:
return (float(color[0]), float(color[1]), float(color[2]), 1.0)
if len(color) == 4:
return tuple(float(c) for c in color)
raise ValueError("color must be a hex string or a sequence of 3/4 floats")
def build_peg(
env_or_scene,
length: float,
radius: float,
*,
initial_pose: Optional["sapien.Pose"] = None,
head_color: str = "#EC7357",
tail_color: str = "#F5F5F5",
density: float = 1200.0,
name: str = "peg",
) -> Tuple["sapien.Articulation", "sapien.Link", "sapien.Link"]:
"""Construct a peg articulation with head and tail links tied by a fixed joint.
Args:
env_or_scene: Environment or scene providing `create_articulation_builder`.
length: Total length of the peg (meters).
radius: Half-width of the rectangular cross section (meters).
initial_pose: Optional pose for the articulation root; defaults to placing
the head centered at positive x.
head_color: Hex color for the head visual.
tail_color: Hex color for the tail visual.
density: Collision density (kg/m^3) shared by both links.
name: Name assigned to the articulation.
Returns:
The articulation along with the head and tail links.
"""
scene = getattr(env_or_scene, "scene", env_or_scene)
if initial_pose is None:
initial_pose = sapien.Pose(p=[length / 2, 0.0, radius], q=[1, 0, 0, 0])
builder = scene.create_articulation_builder()
builder.initial_pose = initial_pose
head_builder = builder.create_link_builder()
head_builder.set_name("peg_head")
head_builder.add_box_collision(
half_size=[length / 2 * 0.9, radius, radius], density=density
)
head_material = sapien.render.RenderMaterial(
base_color=_color_to_rgba(head_color),
roughness=0.5,
specular=0.5,
)
head_builder.add_box_visual(
half_size=[length / 2, radius, radius],
material=head_material,
)
tail_builder = builder.create_link_builder(head_builder)
tail_builder.set_name("peg_tail")
tail_builder.set_joint_name("peg_fixed_joint")
tail_builder.set_joint_properties(
type="fixed",
limits=[[0.0, 0.0]],
pose_in_parent=sapien.Pose(p=[-length, 0.0, 0.0], q=[1, 0, 0, 0]),
pose_in_child=sapien.Pose(p=[0.0, 0.0, 0.0], q=[1, 0, 0, 0]),
friction=0.0,
damping=0.0,
)
tail_builder.add_box_collision(
half_size=[length / 2 * 0.9, radius, radius], density=density
)
tail_material = sapien.render.RenderMaterial(
base_color=_color_to_rgba(tail_color),
roughness=0.5,
specular=0.5,
)
tail_builder.add_box_visual(
half_size=[length / 2, radius, radius],
material=tail_material,
)
peg = builder.build(name=name, fix_root_link=False)
link_map = {link.get_name(): link for link in peg.get_links()}
peg_head = link_map["peg_head"]
peg_tail = link_map["peg_tail"]
return peg, peg_head, peg_tail
def build_box_with_hole(self, inner_radius, outer_radius, depth, center=(0, 0)):
builder = self.scene.create_actor_builder()
thickness = (outer_radius - inner_radius) * 0.5
# x-axis is hole direction
half_center = [x * 0.5 for x in center]
half_sizes = [
[depth, thickness - half_center[0], outer_radius],
[depth, thickness + half_center[0], outer_radius],
[depth, outer_radius, thickness - half_center[1]],
[depth, outer_radius, thickness + half_center[1]],
]
offset = thickness + inner_radius
poses = [
sapien.Pose([0, offset + half_center[0], 0]),
sapien.Pose([0, -offset + half_center[0], 0]),
sapien.Pose([0, 0, offset + half_center[1]]),
sapien.Pose([0, 0, -offset + half_center[1]]),
]
mat = sapien.render.RenderMaterial(
base_color=sapien_utils.hex2rgba("#FFD289"), roughness=0.5, specular=0.5
)
for half_size, pose in zip(half_sizes, poses):
builder.add_box_collision(pose, half_size)
builder.add_box_visual(pose, half_size, material=mat)
box=builder.build_kinematic(f"box_with_hole")
return box
def _safe_unit(v, eps=1e-12):
n = np.linalg.norm(v)
if n < eps:
return v
return v / n
def _trimesh_box_to_obb2d(obb_box, extra_pad=0.0):
"""
Convert trimesh.primitives.Box (world frame) to 2D OBB representation: center c(2,), axes A(2x2), half-extents h(2,)
extra_pad: Margins to expand outward on XY plane (meters)
"""
# Compatible with obb potentially wrapped in .primitive
b = getattr(obb_box, "primitive", obb_box)
T = np.asarray(b.transform, dtype=np.float64) # 4x4
ex = np.asarray(b.extents, dtype=np.float64) # 3
R = T[:3, :3]
t = T[:3, 3]
c = t[:2].copy()
# Take projection of X, Y axes on plane as two axes of 2D OBB
u = _safe_unit(R[:2, 0]) # x-axis projection
v = _safe_unit(R[:2, 1]) # y-axis projection
A = np.stack([u, v], axis=1) # 2x2, each column is an axis
h = 0.5 * ex[:2].astype(np.float64)
if extra_pad > 0:
h = h + float(extra_pad)
return c, A, h
def _obb2d_intersect(c1, A1, h1, c2, A2, h2):
"""
2D OBB SAT detection. c*: (2,), A*: (2x2) columns are axes, h*: (2,)
Returns True indicating intersection (including contact), False indicating separation
"""
d = c2 - c1
axes = [A1[:, 0], A1[:, 1], A2[:, 0], A2[:, 1]]
for a in axes:
a = _safe_unit(a)
# Projected radius
r1 = abs(np.dot(A1[:, 0], a)) * h1[0] + abs(np.dot(A1[:, 1], a)) * h1[1]
r2 = abs(np.dot(A2[:, 0], a)) * h2[0] + abs(np.dot(A2[:, 1], a)) * h2[1]
dist = abs(np.dot(d, a))
if dist > (r1 + r2):
return False # Separating axis exists -> No intersection
return True # All axes overlap -> Intersection/Contact
def _yaw_to_quat_tensor(yaw: float, device):
"""
Get quaternion consistent with ManiSkill/your conversion tools using z-axis Euler angle (shape [1,4], float32, device aligned)
"""
# euler_angles_to_matrix accepts [roll, pitch, yaw] (radians), returns Nx3x3
angles = torch.tensor([[0.0, 0.0, float(yaw)]], dtype=torch.float32, device=device)
R = euler_angles_to_matrix(angles,convention="XYZ") # (1, 3, 3)
q = matrix_to_quaternion(R) # (1, 4) Convention same as ManiSkill
return q
def _build_new_cube_obb2d(x, y, half_size_xy, yaw, pad_xy=0.0):
"""
Construct 2D OBB for "cube ready to be placed": center/axes/half-extents
half_size_xy: float, half length of cube on XY
yaw: rotation around z-axis (radians)
pad_xy: extra padding on half length on XY (for minimum gap)
"""
c = np.array([x, y], dtype=np.float64)
cos_y = np.cos(yaw)
sin_y = np.sin(yaw)
A = np.array([[cos_y, -sin_y],
[sin_y, cos_y]], dtype=np.float64) # Columns are axes
h = np.array([half_size_xy + pad_xy, half_size_xy + pad_xy], dtype=np.float64)
return c, A, h
def spawn_random_cube(
self,
region_center=[0, 0],
region_half_size=0.1,
half_size=0.01,
color=(1, 0, 0, 1),
name_prefix="cube_extra",
min_gap=0.005,
max_trials=256,
avoid=None,
random_yaw=True,
include_existing=True,
include_goal=True,
generator=None
):
"""
Drop a cube (onto table) in rectangular region using rejection sampling, and return the cube actor.
- Uses OBB precise collision (2D projection + SAT), places only if min_gap is satisfied.
- avoid: Input a list of objects. Can be [actor, ...] or [(actor, pad), ...] (pad in meters).
- generator: Must pass torch.Generator for randomization.
"""
# Cache
if not hasattr(self, "_spawned_cubes"):
self._spawned_cubes = []
self._spawned_count = 0
center = np.array(region_center if region_center is not None else self.cube_spawn_center, dtype=np.float64)
# Support two types of input: scalar or 2D array
if region_half_size is None:
region_half_size = self.cube_spawn_half_size
# Compatible with two input formats
if isinstance(region_half_size, (list, tuple, np.ndarray)):
# 2D array input: independent control for xy
area_half = np.array(region_half_size, dtype=np.float64)
if area_half.shape == (): # Handle 0-dim array
area_half = np.array([float(area_half), float(area_half)], dtype=np.float64)
elif len(area_half) == 1:
area_half = np.array([float(area_half[0]), float(area_half[0])], dtype=np.float64)
elif len(area_half) != 2:
raise ValueError("region_half_size array must contain 1 or 2 elements [x_half, y_half]")
else:
# Scalar input: xy remain consistent
area_half = np.array([float(region_half_size), float(region_half_size)], dtype=np.float64)
hs_new = float(half_size if half_size is not None else self.cube_half_size)
# Let cube fall completely inside region (independent control for xy)
x_low = center[0] - area_half[0] + hs_new
x_high = center[0] + area_half[0] - hs_new
y_low = center[1] - area_half[1] + hs_new
y_high = center[1] + area_half[1] - hs_new
if x_low > x_high or y_low > y_high:
raise ValueError("spawn_random_cube: Sampling region too small, cannot fit cube of this size.")
# === Assemble Obstacle OBB (2D) List ===
obb2d_list = [] # [(c, A, h), ...]
def _push_actor_as_obb2d(actor, pad=0.0):
try:
# Special handling for board_with_hole
if hasattr(actor, '_board_side') and hasattr(actor, '_hole_side'):
# This is our board with hole, manually add its OBB
board_side = actor._board_side
hole_side = actor._hole_side
# Get board world position
actor_pos = actor.pose.p
if isinstance(actor_pos, torch.Tensor):
actor_pos = actor_pos[0].detach().cpu().numpy()
board_center = np.array(actor_pos[:2], dtype=np.float64)
board_half = board_side / 2
hole_half = hole_side / 2
# Add OBBs for four rectangular strips
# Top strip
if board_half > hole_half: # Ensure enough space
top_height = board_half - hole_half
top_center = board_center + np.array([0, hole_half + top_height / 2])
A_top = np.eye(2) # No rotation
h_top = np.array([board_half + pad, top_height / 2 + pad])
obb2d_list.append((top_center, A_top, h_top))
# Bottom strip
bottom_center = board_center + np.array([0, -(hole_half + top_height / 2)])
obb2d_list.append((bottom_center, A_top, h_top))
# Left strip
left_width = board_half - hole_half
left_center = board_center + np.array([-(hole_half + left_width / 2), 0])
h_left = np.array([left_width / 2 + pad, hole_half + pad])
obb2d_list.append((left_center, A_top, h_left))
# Right strip
right_center = board_center + np.array([hole_half + left_width / 2, 0])
obb2d_list.append((right_center, A_top, h_left))
return
obb = get_actor_obb(actor, to_world_frame=True, vis=False)
obb2d = _trimesh_box_to_obb2d(obb, extra_pad=float(pad))
obb2d_list.append(obb2d)
except Exception:
# Some objects (like site/marker) do not have physical mesh, ignore or use circle approximation below
pass
if include_existing:
# Main cube
if hasattr(self, "cube") and self.cube is not None:
_push_actor_as_obb2d(self.cube, pad=0.0)
# Historically spawned cubes
for ac in self._spawned_cubes:
_push_actor_as_obb2d(ac, pad=0.0)
# User specified extra avoidance
if avoid:
for it in avoid:
if isinstance(it, tuple):
# Check if it's a pre-made OBB tuple (c, A, h) or (actor, pad)
if len(it) == 3 and isinstance(it[0], np.ndarray) and isinstance(it[1], np.ndarray):
# Pre-made OBB: (center, axes, half_sizes)
obb2d_list.append(it)
else:
# Actor with padding
act_i, pad_i = it
_push_actor_as_obb2d(act_i, pad=float(pad_i))
else:
_push_actor_as_obb2d(it, pad=0.0)
# Target point (if no mesh), supplement with "circle + circumscribed circle" conservative approximation (optional)
circle_list = [] # [(xy(2,), R)], for objects without mesh
def _actor_xy(actor):
p = actor.pose.p
if isinstance(p, torch.Tensor):
p = p[0].detach().cpu().numpy()
return np.array(p[:2], dtype=np.float64)
if include_goal and hasattr(self, "goal_site") and self.goal_site is not None:
try:
# If goal_site has mesh, it will be covered in _push_actor_as_obb2d, here only as a fallback
_push_actor_as_obb2d(self.goal_site, pad=0.0)
except Exception:
# Degrade to circle approximation: goal radius + new cube circumscribed circle radius
R_goal = float(getattr(self, "goal_thresh", 0.03))
R_new_ext = np.sqrt(2.0) * hs_new
circle_list.append((_actor_xy(self.goal_site), R_goal + R_new_ext + min_gap))
# === Sampling Iteration ===
if generator is None:
raise ValueError("spawn_random_cube: generator argument must be explicitly passed for randomization")
device = self.device
for trial in range(int(max_trials)):
# Use simple uniform sampling to ensure good spatial coverage
# Complex sampling strategies often reduce coverage
u1 = torch.rand(1, generator=generator).item()
u2 = torch.rand(1, generator=generator).item()
# Map directly to sampling region - Uniform distribution provides best spatial coverage
x = float(x_low + u1 * (x_high - x_low))
y = float(y_low + u2 * (y_high - y_low))
if random_yaw:
# Use more random yaw generation, sampling from full [0, 2π] range
yaw_sample = torch.rand(1, generator=generator).item()
yaw = float(yaw_sample * 2 * np.pi)
else:
yaw = 0.0
# New cube's 2D OBB (reflect min_gap in "new object half length expansion", avoid adding to both sides causing double)
c_new, A_new, h_new = _build_new_cube_obb2d(x, y, hs_new, yaw, pad_xy=float(min_gap))
# Check collision one by one with OBB obstacles
hit = False
for (c_obs, A_obs, h_obs) in obb2d_list:
if _obb2d_intersect(c_obs, A_obs, h_obs, c_new, A_new, h_new):
hit = True
break
if hit:
continue
# Check circular conservative obstacles (if present)
for (xy_c, R_c) in circle_list:
if np.linalg.norm(np.asarray([x, y], dtype=np.float64) - xy_c) < R_c:
hit = True
break
if hit:
continue
# Passing detection, create cube (pose and collision detection use same yaw to ensure consistency)
q = _yaw_to_quat_tensor(yaw, device=device)
cube = actors.build_cube(
self.scene,
half_size=hs_new,
color=color,
name=name_prefix, # Use name_prefix directly, do not add counter
initial_pose=Pose.create_from_pq(
torch.tensor([[x, y, hs_new]], device=device, dtype=torch.float32),
q,
),
)
cube._cube_half_size = hs_new
self._spawned_cubes.append(cube)
self._spawned_count += 1
return cube
raise RuntimeError("spawn_random_cube: Region crowded or constraints too tight, no feasible position found. Try: increase region/decrease cube/decrease min_gap.")
def _build_new_target_obb2d(x, y, half_size_xy, yaw, pad_xy=0.0):
"""
Construct 2D OBB for "target ready to be placed": center/axes/half-extents
half_size_xy: float, half length of target on XY
yaw: rotation around z-axis (radians)
pad_xy: extra padding on half length on XY (for minimum gap)
"""
c = np.array([x, y], dtype=np.float64)
cos_y = np.cos(yaw)
sin_y = np.sin(yaw)
A = np.array([[cos_y, -sin_y],
[sin_y, cos_y]], dtype=np.float64) # Each column is an axis
h = np.array([half_size_xy + pad_xy, half_size_xy + pad_xy], dtype=np.float64)
return c, A, h
def spawn_random_target(
self,
region_center=[0, 0],
region_half_size=0.1,
radius=0.01,
thickness=0.005,
name_prefix="target_extra",
min_gap=0.005,
max_trials=256,
avoid=None, # Supports [actor, ...] or [(actor, pad), ...]
include_existing=True, # Whether to automatically avoid existing main target and generated extra targets
include_goal=True, # Whether to treat goal_site as obstacle (approximate with circle, conservative)
generator=None,
randomize=True, # Control whether to randomize position
target_style="purple", # Choose which color scheme target to create
):
"""
Drop a target (onto table) in rectangular region using rejection sampling, and return the target actor.
- Uses OBB precise collision (2D projection + SAT), places only if min_gap is satisfied.
- avoid: Input a list of objects. Can be [actor, ...] or [(actor, pad), ...] (pad in meters).
- generator: Must pass torch.Generator for randomization (when randomize=True).
- randomize: Control whether to randomize position. If False, generate directly at region_center.
"""
# Cache
random_yaw=False
if not hasattr(self, "_spawned_targets"):
self._spawned_targets = []
self._spawned_target_count = 0
center = np.array(region_center if region_center is not None else getattr(self, 'target_spawn_center', [0, 0]), dtype=np.float64)
area_half = float(region_half_size if region_half_size is not None else getattr(self, 'target_spawn_half_size', 0.1))
target_radius = float(radius if radius is not None else getattr(self, 'target_radius', 0.01))
target_thickness = float(thickness if thickness is not None else getattr(self, 'target_thickness', 0.005))
# Let target fall completely inside region
x_low = center[0] - area_half + target_radius
x_high = center[0] + area_half - target_radius
y_low = center[1] - area_half + target_radius
y_high = center[1] + area_half - target_radius
if x_low > x_high or y_low > y_high:
raise ValueError("spawn_random_target: Sampling region too small, cannot fit target of this size.")
# === Assemble Obstacle OBB (2D) List ===
obb2d_list = [] # [(c, A, h), ...]
def _push_actor_as_obb2d(actor, pad=0.0):
try:
# Special handling for board_with_hole
if hasattr(actor, '_board_side') and hasattr(actor, '_hole_side'):
# This is our board with hole, manually add its OBB
board_side = actor._board_side
hole_side = actor._hole_side
# Get board world position
actor_pos = actor.pose.p
if isinstance(actor_pos, torch.Tensor):
actor_pos = actor_pos[0].detach().cpu().numpy()
board_center = np.array(actor_pos[:2], dtype=np.float64)
board_half = board_side / 2
hole_half = hole_side / 2
# Add OBBs for four rectangular strips
# Top strip
if board_half > hole_half: # Ensure enough space
top_height = board_half - hole_half
top_center = board_center + np.array([0, hole_half + top_height / 2])
A_top = np.eye(2) # No rotation
h_top = np.array([board_half + pad, top_height / 2 + pad])
obb2d_list.append((top_center, A_top, h_top))
# Bottom strip
bottom_center = board_center + np.array([0, -(hole_half + top_height / 2)])
obb2d_list.append((bottom_center, A_top, h_top))
# Left strip
left_width = board_half - hole_half
left_center = board_center + np.array([-(hole_half + left_width / 2), 0])
h_left = np.array([left_width / 2 + pad, hole_half + pad])
obb2d_list.append((left_center, A_top, h_left))
# Right strip
right_center = board_center + np.array([hole_half + left_width / 2, 0])
obb2d_list.append((right_center, A_top, h_left))
return
obb = get_actor_obb(actor, to_world_frame=True, vis=False)
obb2d = _trimesh_box_to_obb2d(obb, extra_pad=float(pad))
obb2d_list.append(obb2d)
except Exception:
# Some objects (like site/marker) do not have physical mesh, ignore or use circle approximation below
pass
if include_existing:
# Main cube
if hasattr(self, "cube") and self.cube is not None:
_push_actor_as_obb2d(self.cube, pad=0.0)
# Main target
if hasattr(self, "target") and self.target is not None:
_push_actor_as_obb2d(self.target, pad=0.0)
# Historically spawned cubes
if hasattr(self, "_spawned_cubes"):
for ac in self._spawned_cubes:
_push_actor_as_obb2d(ac, pad=0.0)
# Target point (if no mesh), supplement with "circle + circumscribed circle" conservative approximation (optional)
circle_list = [] # [(xy(2,), R)], for objects without mesh
def _actor_xy(actor):
p = actor.pose.p
if isinstance(p, torch.Tensor):
p = p[0].detach().cpu().numpy()
return np.array(p[:2], dtype=np.float64)
# Historically spawned targets - Treat as circular obstacles
if include_existing:
for ac in self._spawned_targets:
target_r = getattr(ac, "_target_radius", target_radius)
circle_list.append((_actor_xy(ac), target_r))
# User specified extra avoidance
if avoid:
for it in avoid:
if isinstance(it, tuple):
# Check if it's a pre-made OBB tuple (c, A, h) or (actor, pad)
if len(it) == 3 and isinstance(it[0], np.ndarray) and isinstance(it[1], np.ndarray):
# Pre-made OBB: (center, axes, half_sizes)
obb2d_list.append(it)
else:
# Actor with padding
act_i, pad_i = it
# Check if it is a target (circular)
if hasattr(act_i, "_target_radius"):
target_r = getattr(act_i, "_target_radius", target_radius)
circle_list.append((_actor_xy(act_i), target_r + float(pad_i)))
else:
_push_actor_as_obb2d(act_i, pad=float(pad_i))
else:
# Check if it is a target (circular)
if hasattr(it, "_target_radius"):
target_r = getattr(it, "_target_radius", target_radius)
circle_list.append((_actor_xy(it), target_r))
else:
_push_actor_as_obb2d(it, pad=0.0)
if include_goal and hasattr(self, "goal_site") and self.goal_site is not None:
try:
# If goal_site has mesh, it will be covered in _push_actor_as_obb2d, here only as a fallback
_push_actor_as_obb2d(self.goal_site, pad=0.0)
except Exception:
# Degrade to circle approximation: goal radius + new target circumscribed circle radius
R_goal = float(getattr(self, "goal_thresh", 0.03))
R_new_ext = target_radius
circle_list.append((_actor_xy(self.goal_site), R_goal + R_new_ext + min_gap))
# === Sampling Iteration ===
if generator is None:
raise ValueError("spawn_random_target: generator argument must be explicitly passed for randomization")
device = self.device
target_builders = {
"purple": build_purple_white_target,
"gray": build_gray_white_target,
"green": build_green_white_target,
"red": build_red_white_target,
}
if isinstance(target_style, str):
builder_key = target_style.lower()
if builder_key not in target_builders:
raise ValueError(f"spawn_random_target: Unknown target_style '{target_style}'. Supported: {list(target_builders.keys())}")
target_builder = target_builders[builder_key]
elif callable(target_style):
target_builder = target_style
else:
raise ValueError("spawn_random_target: target_style must be a string or callable builder function")
for _ in range(int(max_trials)):
x = float(torch.rand(1, generator=generator).item() * (x_high - x_low) + x_low)
y = float(torch.rand(1, generator=generator).item() * (y_high - y_low) + y_low)
if random_yaw:
yaw = float(torch.rand(1, generator=generator).item() * 2 * np.pi - np.pi)
else:
yaw = 0.0
# New target's circular collision detection (target is circular, circular detection is more accurate)
target_pos = np.array([x, y], dtype=np.float64)
target_collision_radius = target_radius + min_gap
# Check collision with OBB obstacles (check circular target against square obstacles)
hit = False
for (c_obs, A_obs, h_obs) in obb2d_list:
# Calculate minimum distance from circle center to OBB
# Convert circle center to OBB local coordinate system
local_pos = A_obs.T @ (target_pos - c_obs)
# Calculate closest point from circle center to OBB
closest_point = np.clip(local_pos, -h_obs, h_obs)
# Convert back to world coordinate system
closest_world = c_obs + A_obs @ closest_point
# Calculate distance
dist = np.linalg.norm(target_pos - closest_world)
if dist < target_collision_radius:
hit = True
break
if hit:
continue
# Check collision with circular obstacles (circle vs circle)
for (xy_c, R_c) in circle_list:
if np.linalg.norm(target_pos - xy_c) < (target_collision_radius + R_c):
hit = True
break
if hit:
continue
# Passed detection, create target (pose and collision detection use same yaw to ensure consistency)
rotate = np.array([np.cos(yaw/2), 0, 0, np.sin(yaw/2)]) # Quaternion for z-axis rotation
angles = torch.deg2rad(torch.tensor([0.0, 90.0, 0.0], dtype=torch.float32)) # (3,)
rotate = matrix_to_quaternion(
euler_angles_to_matrix(angles, convention="XYZ")
)
target = target_builder(
scene=self.scene,
radius=target_radius,
thickness=target_thickness,
name=name_prefix, # Use name_prefix directly, do not add counter
body_type="kinematic", # Visualization only
add_collision=False, # Disable collision
initial_pose=sapien.Pose(p=[x, y, target_thickness], q=rotate),
)
target._target_radius = target_radius
self._spawned_targets.append(target)
self._spawned_target_count += 1
return target
raise RuntimeError("spawn_random_target: Region crowded or constraints too tight, no feasible position found. Try: increase region/decrease target/decrease min_gap.")
def create_button_obb(center_xy=(-0.3, 0), half_size=0.05):
"""
Create a manual OBB for button collision avoidance.
Args:
center_xy: Button center position (x, y)
half_size: Safe zone half-size around button (default 0.05m)
Returns:
Tuple (center, axes, half_sizes) for use in avoid lists
"""
return (
np.array(center_xy, dtype=np.float64), # center
np.eye(2, dtype=np.float64), # axes (identity for axis-aligned)
np.array([half_size, half_size], dtype=np.float64) # half-sizes
)
def build_button(
self,
center_xy=(0.15, 0.10), # Button (x,y) on table
base_half=[0.025, 0.025, 0.005], # Base half-size [x,y,z]
cap_radius=0.015, # Button cap radius
cap_half_len=0.006, # Button cap half-length
travel=None, # Press travel
stiffness=800.0,
damping=40.0,
scale: float = None, # ⭐ New: scaling factor
generator=None,
name: str = "button", # ⭐ New: button name
randomize: bool = True, # ⭐ New: whether to randomize position
randomize_range=(0.1, 0.4), # ⭐ New: randomization range, (range_x, range_y)
):
# ------- Scaling and Travel -------
if scale is None:
# If not passed, use default scaling from environment
scale = getattr(self, "button_scale", 1.0)
scale = float(scale)
# Travel priority: argument > environment base
if travel is None:
# Scale proportionally using base travel
base_travel = getattr(self, "_button_travel_base", 0.1)
travel = base_travel * scale
else:
# If travel explicitly passed, also follow scale (to keep absolute value, change next line to pass)
travel = float(travel) * scale
# Size scaling
base_half = [bh * scale for bh in base_half]
cap_radius = float(cap_radius) * scale
cap_half_len = float(cap_half_len) * scale
# Record current button travel for other functions
self.button_travel = float(travel)
# ------- Position Randomization -------
cx, cy = float(center_xy[0]), float(center_xy[1])
if randomize:
if not isinstance(randomize_range, (tuple, list, np.ndarray)):
raise TypeError("randomize_range must be a sequence of length 2.")
if len(randomize_range) != 2:
raise ValueError("randomize_range must contain exactly two elements.")
range_x, range_y = float(randomize_range[0]), float(randomize_range[1])
offset = torch.rand(2, generator=generator) - 0.5
cx += float(offset[0]) * range_x
cy += float(offset[1]) * range_y
center_xy = (cx, cy)
scene = self.scene
builder = scene.create_articulation_builder()
# Initial pose: lift base center to z=base_half[2]
builder.initial_pose = sapien.Pose(p=[cx, cy, base_half[2]])
# Root: Base
base = builder.create_link_builder()
base.set_name("button_base")
base.add_box_collision(half_size=base_half, density=200000)
base.add_box_visual(half_size=base_half)
# Child: Button cap (vertical sliding)
cap = builder.create_link_builder(base)
cap.set_name("button_cap")
cap.set_joint_name("button_joint")
R_up = euler2quat(0, -np.pi / 2, 0) # Align joint x-axis with world z
cap.set_joint_properties(
type="prismatic",
limits=[[-travel, 0.0]], # Negative direction is pressed
pose_in_parent=sapien.Pose(p=[0, 0, base_half[2]], q=R_up),
pose_in_child=sapien.Pose(p=[0, 0, 0.0], q=R_up),
friction=0.0,
damping=0.0,
)
cap.add_cylinder_collision(
half_length=cap_half_len, radius=cap_radius,
pose=sapien.Pose(p=[0, 0, cap_half_len], q=R_up), density=1500
)
material = sapien.render.RenderMaterial()
material.set_base_color([0.5, 0.5, 0.5, 1.0])
cap.add_cylinder_visual(
half_length=cap_half_len, radius=cap_radius,
pose=sapien.Pose(p=[0, 0, cap_half_len], q=R_up), material=material
)
button = builder.build(name=name, fix_root_link=True)
j = {j.name: j for j in button.get_joints()}["button_joint"]
j.set_drive_properties(stiffness=stiffness, damping=damping)
j.set_drive_target(0.0)
self.button = button
self.button_joint = j
cap_link = next(
link for link in button.get_links()
if link.get_name() == "button_cap"
)
cap_link = next(link for link in button.get_links()
if link.get_name() == "button_cap")
if not hasattr(self, "cap_links"):
self.cap_links = {}
self.cap_links[name] = [cap_link] # name is "button_left", "button_right", etc.
self.cap_link = self.cap_links[name] # Compatible with old logic
# Provide an OBB for downstream placement logic using the scaled button footprint
button_obb = create_button_obb(
center_xy=center_xy,
half_size=max(base_half[0], base_half[1]) * 1.5,
)
return button_obb
def build_bin(
self,
*,
inner_side: float = 0.04, # Inner opening side length (full length, meters), originally 2*inner_side_half_len = 0.04
wall_thickness: float = 0.005, # Wall thickness (full thickness, meters)
wall_height: float = 0.05, # Wall height (full height, meters)
floor_thickness: float = 0.004, # Floor thickness (full thickness, meters)
callsign=None,
position=None, # Add position argument
z_rotation_deg=0.0 # Add z-axis rotation angle argument (degrees)
):
"""
Assemble an "open box" using 1 floor + 4 wall strips.
All dimensions use "full size (meters)", automatically converted to half-size internally.
Refer to cube generation method, let bin bottom sit on table (z=0).
"""
inner_side = self.cube_half_size * 2.5
wall_height = self.cube_half_size * 2.5
# ---- Convert full size to half size (consistent with add_box_* interface) ----
inner_half = inner_side * 0.5
t = wall_thickness * 0.5 # Half wall thickness
h = wall_height * 0.5 # Half wall height
tf = floor_thickness * 0.5 # Half floor thickness
# ---- Component half sizes (in world coordinates [x, y, z]) ----
# Floor: covers inner opening + two side wall thicknesses
bottom_half = [inner_half + t, inner_half + t, tf]
# Left/Right Wall: thickness along x, height along z, length along y
lr_wall_half = [t, inner_half + t, h]
# Front/Back Wall: thickness along y, height along z, length along x
fb_wall_half = [inner_half + t, t, h]
# ---- Determine bin position (refer to cube way) ----
if position is None:
base_pos = [0.0, 0.0, 0.0]
else:
base_pos = list(position)
# Build geometry as "opening up" in local coordinate system, then flip to "opening down" globally
# Floor on table, walls extend up from floor top (flipped becomes down)
base_z = tf # Floor center height (half of floor thickness)
# ---- Component placement positions (relative to bin builder origin) ----
# Wall center horizontal offset = inner half + half wall thickness
offset = inner_half + t
# Wall center vertical position = floor thickness + half wall height
z_wall = tf + h
poses = [
sapien.Pose([0.0, 0.0, 0]),
# Floor: on table, half thickness height
sapien.Pose([0.0, 0.0, base_z]),
# Left/Right Wall (+/- x direction): extend up from floor top
sapien.Pose([-offset, 0.0, z_wall]),
sapien.Pose([+offset, 0.0, z_wall]),
# Front/Back Wall (+/- y direction): extend up from floor top
sapien.Pose([0.0, -offset, z_wall]),
sapien.Pose([0.0, +offset, z_wall]),
]
half_sizes = [
[self.cube_half_size,self.cube_half_size,self.cube_half_size],
bottom_half,
lr_wall_half, # Left
lr_wall_half, # Right
fb_wall_half, # Front
fb_wall_half, # Back
]
builder = self.scene.create_actor_builder()
# Let bin "clasp" on table: flip 180 degrees around x-axis, opening down, then rotate around z-axis
angles = torch.deg2rad(torch.tensor([180.0, 0.0, z_rotation_deg], dtype=torch.float32)) # (3,)
rotate = matrix_to_quaternion(
euler_angles_to_matrix(angles, convention="XYZ")
)
# Lowest point after rotation is -(tf + 2h), translate to z=0 to sit on table
builder.set_initial_pose(
sapien.Pose(
p=[base_pos[0], base_pos[1], tf + 2 * h],
q=rotate,
)
)
for pose, half_size in zip(poses, half_sizes):
builder.add_box_collision(pose, half_size)
builder.add_box_visual(pose, half_size)
bin_actor = builder.build_dynamic(name=callsign)
return bin_actor
def spawn_random_bin(
self,
avoid=None,
region_center=[-0.1, 0],
region_half_size=0.3,
min_gap=0.05,
name_prefix="bin",
max_trials=256,
generator=None
):
"""
Drop a bin in rectangular region using rejection sampling, and return the bin actor.
Use OBB precise collision detection, place only if min_gap is satisfied.
"""
if avoid is None:
avoid = []
center = np.array(region_center, dtype=np.float64)
area_half = float(region_half_size)
# Calculate bin size (for collision detection)
inner_side = self.cube_half_size * 2.5
wall_thickness = 0.005
bin_half_size = (inner_side + wall_thickness) * 0.5 # Half of bin total size
# Let bin fall completely inside region
x_low = center[0] - area_half + bin_half_size
x_high = center[0] + area_half - bin_half_size
y_low = center[1] - area_half + bin_half_size
y_high = center[1] + area_half - bin_half_size
if x_low > x_high or y_low > y_high:
raise ValueError("_spawn_random_bin: Sampling region too small, cannot fit bin of this size.")
# === Assemble Obstacle OBB (2D) List ===
obb2d_list = [] # [(c, A, h), ...]
def _push_actor_as_obb2d(actor, pad=0.0):
try:
obb = get_actor_obb(actor, to_world_frame=True, vis=False)
obb2d = _trimesh_box_to_obb2d(obb, extra_pad=float(pad))
obb2d_list.append(obb2d)
except Exception:
# Some objects (like site/marker) do not have physical mesh, ignore
pass
# Collect avoidance object OBBs
for item in avoid:
if isinstance(item, tuple):
# Check if it's a pre-made OBB tuple (c, A, h) or (actor, pad)
if len(item) == 3 and isinstance(item[0], np.ndarray) and isinstance(item[1], np.ndarray):
# Pre-made OBB: (center, axes, half_sizes)
obb2d_list.append(item)
else:
# Actor with padding
actor, pad = item
_push_actor_as_obb2d(actor, pad)
else:
_push_actor_as_obb2d(item, min_gap)
for trial in range(int(max_trials)):
x = float(torch.rand(1, generator=generator).item() * (x_high - x_low) + x_low)
y = float(torch.rand(1, generator=generator).item() * (y_high - y_low) + y_low)
# New bin square collision detection
bin_pos = np.array([x, y], dtype=np.float64)
bin_collision_half_size = bin_half_size + min_gap
# Detect collision with other OBB obstacles
hit = False
for (c_obs, A_obs, h_obs) in obb2d_list:
# Simplify: treat bin as square, detect collision with OBB
# Calculate bin center to OBB closest distance
local_pos = A_obs.T @ (bin_pos - c_obs)
closest_point = np.clip(local_pos, -h_obs, h_obs)
closest_world = c_obs + A_obs @ closest_point
dist = np.linalg.norm(bin_pos - closest_world)
if dist < bin_collision_half_size:
hit = True
break
if hit:
continue
# Passing detection, create bin (at specified position), with random z-axis rotation
z_rotation = float(torch.rand(1, generator=generator).item() * 90.0) # 0-360 degrees
bin_actor = build_bin(self, callsign=name_prefix, position=[x, y, 0.002], z_rotation_deg=z_rotation)
return bin_actor
raise RuntimeError("_spawn_random_bin: Region crowded or constraints too tight, no feasible position found. Try: increase region/decrease bin/decrease min_gap.")
def spawn_fixed_cube(
self,
position, # [x, y, z] fixed position
half_size=None,
color=(1, 0, 0, 1),
name_prefix="fixed_cube",
yaw=0.0, # rotation around z-axis (radians)
dynamic=False,
):
"""
Generate a cube at fixed position, no collision detection.
Use builder pattern to create dynamic object, refer to build_bin implementation.
"""
hs = float(half_size if half_size is not None else self.cube_half_size)
# Ensure position is array format
pos = np.array(position, dtype=np.float64)
if len(pos) == 2:
# If only x,y provided, set z to cube half height (let cube bottom sit on table)
pos = np.append(pos, hs)
# Create actor builder
builder = self.scene.create_actor_builder()
# Generate rotation quaternion (rotate yaw angle around z-axis)
if yaw != 0.0:
angles = torch.tensor([0.0, 0.0, float(yaw)], dtype=torch.float32)
R = euler_angles_to_matrix(angles.unsqueeze(0), convention="XYZ")[0]
q = matrix_to_quaternion(R.unsqueeze(0))[0]
rotate = q
else:
rotate = torch.tensor([1.0, 0.0, 0.0, 0.0]) # Identity quaternion
# Set initial position and rotation
builder.set_initial_pose(
sapien.Pose(
p=[pos[0], pos[1], pos[2]],
q=rotate.numpy() if isinstance(rotate, torch.Tensor) else rotate
)
)
# Add box geometry (collision and visual)
half_size_list = [hs, hs, hs]
if dynamic==True:
# Collision geometry stays at builder origin; initial pose already positions the actor
builder.add_box_collision(sapien.Pose([0, 0, 0]), half_size_list)
# Create material
material = sapien.render.RenderMaterial()
material.set_base_color(color)
builder.add_box_visual(sapien.Pose([0, 0, 0]), half_size_list, material=material)
# Choose build method based on dynamic argument
if dynamic==True:
cube = builder.build_dynamic(name=name_prefix)
else:
cube = builder.build_kinematic(name=name_prefix)
# Set cube attribute
cube._cube_half_size = hs
return cube
def build_board_with_hole(
self,
*,
board_side=0.01, # Square board side length
hole_side=0.06, # Square hole side length
thickness=0.02, # Board thickness
position=None, # Board position [x, y] or [x, y, z]
rotation_quat=None, # Rotation quaternion [w, x, y, z]
name="board_with_hole"
):
"""
Create a square board with a square hole
Combine four rectangular strips: top, bottom, left, right
Args:
height: If provided, overwrite z coordinate in position
"""
if position is None:
position = [0.3, 0, 0] # Default position, bottom on table
# Board and hole half lengths
board_half = board_side / 2
hole_half = hole_side / 2
thickness_half = thickness / 2
# Use input position as board bottom, calculate board center position
# Input position is bottom position, need to add thickness_half to get center position
center_position = [position[0], position[1], position[2] + thickness_half]
# Create actor builder
builder = self.scene.create_actor_builder()
# Set board initial position (use center position)
if rotation_quat is None:
rotation_quat = [1.0, 0.0, 0.0, 0.0] # No rotation
builder.set_initial_pose(
sapien.Pose(
p=center_position,
q=rotation_quat
)
)
# Create material - brown board
material = sapien.render.RenderMaterial()
material.set_base_color([0.8, 0.6, 0.4, 1.0]) # Light brown
# Four rectangular strips dimensions and positions
# Top strip
top_width = board_side # Full board width
top_height = board_half - hole_half # From hole top to board top
top_center_y = hole_half + top_height / 2
builder.add_box_collision(
sapien.Pose([0, top_center_y, 0]),
[top_width / 2, top_height / 2, thickness_half]
)
builder.add_box_visual(
sapien.Pose([0, top_center_y, 0]),
[top_width / 2, top_height / 2, thickness_half],
material=material
)
# Bottom strip
bottom_width = board_side # Full board width
bottom_height = board_half - hole_half # From board bottom to hole bottom
bottom_center_y = -(hole_half + bottom_height / 2)
builder.add_box_collision(
sapien.Pose([0, bottom_center_y, 0]),
[bottom_width / 2, bottom_height / 2, thickness_half]
)
builder.add_box_visual(
sapien.Pose([0, bottom_center_y, 0]),
[bottom_width / 2, bottom_height / 2, thickness_half],
material=material
)
# Left strip - only within hole height range
left_width = board_half - hole_half # From board left to hole left
left_height = hole_side # Hole height
left_center_x = -(hole_half + left_width / 2)
builder.add_box_collision(
sapien.Pose([left_center_x, 0, 0]),
[left_width / 2, left_height / 2, thickness_half]
)
builder.add_box_visual(
sapien.Pose([left_center_x, 0, 0]),
[left_width / 2, left_height / 2, thickness_half],
material=material
)
# Right strip - only within hole height range
right_width = board_half - hole_half # From hole right to board right
right_height = hole_side # Hole height
right_center_x = hole_half + right_width / 2
builder.add_box_collision(
sapien.Pose([right_center_x, 0, 0]),
[right_width / 2, right_height / 2, thickness_half]
)
builder.add_box_visual(
sapien.Pose([right_center_x, 0, 0]),
[right_width / 2, right_height / 2, thickness_half],
material=material
)
# Add a black cube at hole center with same size as hole but half height (visual only, no collision)
hole_cube_half_size_xy = hole_half # cube half size same as hole
hole_cube_half_height = thickness_half / 2 # cube height is half of board thickness
# Create black material
black_material = sapien.render.RenderMaterial()
black_material.set_base_color([0.0, 0.0, 0.0, 1.0]) # Black
# Add black cube (visual only, no collision)
# Position: cube bottom at board bottom, so cube center at -thickness_half + hole_cube_half_height
cube_center_z = -thickness_half + hole_cube_half_height
builder.add_box_visual(
sapien.Pose([0, 0, cube_center_z]), # Black cube bottom at board bottom
[hole_cube_half_size_xy, hole_cube_half_size_xy, hole_cube_half_height],
material=black_material
)
# Build actor
board_actor = builder.build_kinematic(name=name)
# Store board attributes
board_actor._board_side = board_side
board_actor._hole_side = hole_side
board_actor._thickness = thickness
return board_actor
def build_purple_white_target(
scene: ManiSkillScene,
radius: float,
thickness: float,
name: str,
body_type: str = "dynamic",
add_collision: bool = True,
scene_idxs: Optional[Array] = None,
initial_pose: Optional[Union[Pose, sapien.Pose]] = None,
):
TARGET_PURPLE = (np.array([160, 32, 240, 255]) / 255).tolist()
builder = scene.create_actor_builder()
builder.add_cylinder_visual(
radius=radius,
half_length=thickness / 2,
material=sapien.render.RenderMaterial(base_color=TARGET_PURPLE),
)
builder.add_cylinder_visual(
radius=radius * 4 / 5,
half_length=thickness / 2 + 1e-5,
material=sapien.render.RenderMaterial(base_color=[1, 1, 1, 1]),
)
builder.add_cylinder_visual(
radius=radius * 3 / 5,
half_length=thickness / 2 + 2e-5,
material=sapien.render.RenderMaterial(base_color=TARGET_PURPLE),
)
builder.add_cylinder_visual(
radius=radius * 2 / 5,
half_length=thickness / 2 + 3e-5,
material=sapien.render.RenderMaterial(base_color=[1, 1, 1, 1]),
)
builder.add_cylinder_visual(
radius=radius * 1 / 5,
half_length=thickness / 2 + 4e-5,
material=sapien.render.RenderMaterial(base_color=TARGET_PURPLE),
)
if add_collision:
builder.add_cylinder_collision(
radius=radius,
half_length=thickness / 2,
)
builder.add_cylinder_collision(
radius=radius * 4 / 5,
half_length=thickness / 2 + 1e-5,
)
builder.add_cylinder_collision(
radius=radius * 3 / 5,
half_length=thickness / 2 + 2e-5,
)
builder.add_cylinder_collision(
radius=radius * 2 / 5,
half_length=thickness / 2 + 3e-5,
)
builder.add_cylinder_collision(
radius=radius * 1 / 5,
half_length=thickness / 2 + 4e-5,
)
return _build_by_type(builder, name, body_type, scene_idxs, initial_pose)
def build_gray_white_target(
scene: ManiSkillScene,
radius: float,
thickness: float,
name: str,
body_type: str = "dynamic",
add_collision: bool = True,
scene_idxs: Optional[Array] = None,
initial_pose: Optional[Union[Pose, sapien.Pose]] = None,
):
TARGET_GRAY = (np.array([128, 128, 128, 255]) / 255).tolist()
builder = scene.create_actor_builder()
builder.add_cylinder_visual(
radius=radius,
half_length=thickness / 2,
material=sapien.render.RenderMaterial(base_color=TARGET_GRAY),
)
builder.add_cylinder_visual(
radius=radius * 4 / 5,
half_length=thickness / 2 + 1e-5,
material=sapien.render.RenderMaterial(base_color=[1, 1, 1, 1]),
)
builder.add_cylinder_visual(
radius=radius * 3 / 5,
half_length=thickness / 2 + 2e-5,
material=sapien.render.RenderMaterial(base_color=TARGET_GRAY),
)
builder.add_cylinder_visual(
radius=radius * 2 / 5,
half_length=thickness / 2 + 3e-5,
material=sapien.render.RenderMaterial(base_color=[1, 1, 1, 1]),
)
builder.add_cylinder_visual(
radius=radius * 1 / 5,
half_length=thickness / 2 + 4e-5,
material=sapien.render.RenderMaterial(base_color=TARGET_GRAY),
)
if add_collision:
builder.add_cylinder_collision(
radius=radius,
half_length=thickness / 2,
)
builder.add_cylinder_collision(
radius=radius * 4 / 5,
half_length=thickness / 2 + 1e-5,
)
builder.add_cylinder_collision(
radius=radius * 3 / 5,
half_length=thickness / 2 + 2e-5,
)
builder.add_cylinder_collision(
radius=radius * 2 / 5,
half_length=thickness / 2 + 3e-5,
)
builder.add_cylinder_collision(
radius=radius * 1 / 5,
half_length=thickness / 2 + 4e-5,
)
return _build_by_type(builder, name, body_type, scene_idxs, initial_pose)
def build_green_white_target(
scene: ManiSkillScene,
radius: float,
thickness: float,
name: str,
body_type: str = "dynamic",
add_collision: bool = True,
scene_idxs: Optional[Array] = None,
initial_pose: Optional[Union[Pose, sapien.Pose]] = None,
):
TARGET_GREEN = (np.array([34, 139, 34, 255]) / 255).tolist()
builder = scene.create_actor_builder()
builder.add_cylinder_visual(
radius=radius,
half_length=thickness / 2,
material=sapien.render.RenderMaterial(base_color=TARGET_GREEN),
)
builder.add_cylinder_visual(
radius=radius * 4 / 5,
half_length=thickness / 2 + 1e-5,
material=sapien.render.RenderMaterial(base_color=[1, 1, 1, 1]),
)
builder.add_cylinder_visual(
radius=radius * 3 / 5,
half_length=thickness / 2 + 2e-5,
material=sapien.render.RenderMaterial(base_color=TARGET_GREEN),
)
builder.add_cylinder_visual(
radius=radius * 2 / 5,
half_length=thickness / 2 + 3e-5,
material=sapien.render.RenderMaterial(base_color=[1, 1, 1, 1]),
)
builder.add_cylinder_visual(
radius=radius * 1 / 5,
half_length=thickness / 2 + 4e-5,
material=sapien.render.RenderMaterial(base_color=TARGET_GREEN),
)
if add_collision:
builder.add_cylinder_collision(
radius=radius,
half_length=thickness / 2,
)
builder.add_cylinder_collision(
radius=radius * 4 / 5,
half_length=thickness / 2 + 1e-5,
)
builder.add_cylinder_collision(
radius=radius * 3 / 5,
half_length=thickness / 2 + 2e-5,
)
builder.add_cylinder_collision(
radius=radius * 2 / 5,
half_length=thickness / 2 + 3e-5,
)
builder.add_cylinder_collision(
radius=radius * 1 / 5,
half_length=thickness / 2 + 4e-5,
)
return _build_by_type(builder, name, body_type, scene_idxs, initial_pose)
def build_red_white_target(
scene: ManiSkillScene,
radius: float,
thickness: float,
name: str,
body_type: str = "dynamic",
add_collision: bool = True,
scene_idxs: Optional[Array] = None,
initial_pose: Optional[Union[Pose, sapien.Pose]] = None,
):
TARGET_RED = (np.array([200, 33, 33, 255]) / 255).tolist()
builder = scene.create_actor_builder()
builder.add_cylinder_visual(
radius=radius,
half_length=thickness / 2,
material=sapien.render.RenderMaterial(base_color=TARGET_RED),
)
builder.add_cylinder_visual(
radius=radius * 4 / 5,
half_length=thickness / 2 + 1e-5,
material=sapien.render.RenderMaterial(base_color=[1, 1, 1, 1]),
)
builder.add_cylinder_visual(
radius=radius * 3 / 5,
half_length=thickness / 2 + 2e-5,
material=sapien.render.RenderMaterial(base_color=TARGET_RED),
)
builder.add_cylinder_visual(
radius=radius * 2 / 5,
half_length=thickness / 2 + 3e-5,
material=sapien.render.RenderMaterial(base_color=[1, 1, 1, 1]),
)
builder.add_cylinder_visual(
radius=radius * 1 / 5,
half_length=thickness / 2 + 4e-5,
material=sapien.render.RenderMaterial(base_color=TARGET_RED),
)
if add_collision:
builder.add_cylinder_collision(
radius=radius,
half_length=thickness / 2,
)
builder.add_cylinder_collision(
radius=radius * 4 / 5,
half_length=thickness / 2 + 1e-5,
)
builder.add_cylinder_collision(
radius=radius * 3 / 5,
half_length=thickness / 2 + 2e-5,
)
builder.add_cylinder_collision(
radius=radius * 2 / 5,
half_length=thickness / 2 + 3e-5,
)
builder.add_cylinder_collision(
radius=radius * 1 / 5,
half_length=thickness / 2 + 4e-5,
)
return _build_by_type(builder, name, body_type, scene_idxs, initial_pose)
def _build_by_type(
builder: ActorBuilder,
name,
body_type,
scene_idxs: Optional[Array] = None,
initial_pose: Optional[Union[Pose, sapien.Pose]] = None,
):
if scene_idxs is not None:
builder.set_scene_idxs(scene_idxs)
if initial_pose is not None:
builder.set_initial_pose(initial_pose)
if body_type == "dynamic":
actor = builder.build(name=name)
elif body_type == "static":
actor = builder.build_static(name=name)
elif body_type == "kinematic":
actor = builder.build_kinematic(name=name)
else:
raise ValueError(f"Unknown body type {body_type}")
return actor