| | 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 |
| | 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 |
| | |
| | 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) |
| | """ |
| | |
| | b = getattr(obb_box, "primitive", obb_box) |
| | T = np.asarray(b.transform, dtype=np.float64) |
| | ex = np.asarray(b.extents, dtype=np.float64) |
| |
|
| | R = T[:3, :3] |
| | t = T[:3, 3] |
| |
|
| | c = t[:2].copy() |
| |
|
| | |
| | u = _safe_unit(R[:2, 0]) |
| | v = _safe_unit(R[:2, 1]) |
| | A = np.stack([u, v], axis=1) |
| |
|
| | 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) |
| | |
| | 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 |
| | return True |
| |
|
| | 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) |
| | """ |
| | |
| | angles = torch.tensor([[0.0, 0.0, float(yaw)]], dtype=torch.float32, device=device) |
| | R = euler_angles_to_matrix(angles,convention="XYZ") |
| | q = matrix_to_quaternion(R) |
| | 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) |
| | 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. |
| | """ |
| | |
| | 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) |
| |
|
| | |
| | if region_half_size is None: |
| | region_half_size = self.cube_spawn_half_size |
| |
|
| | |
| | if isinstance(region_half_size, (list, tuple, np.ndarray)): |
| | |
| | area_half = np.array(region_half_size, dtype=np.float64) |
| | if area_half.shape == (): |
| | 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: |
| | |
| | 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) |
| |
|
| | |
| | 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.") |
| |
|
| | |
| | obb2d_list = [] |
| |
|
| | def _push_actor_as_obb2d(actor, pad=0.0): |
| | try: |
| | |
| | if hasattr(actor, '_board_side') and hasattr(actor, '_hole_side'): |
| | |
| | board_side = actor._board_side |
| | hole_side = actor._hole_side |
| |
|
| | |
| | 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 |
| |
|
| | |
| | |
| | if board_half > hole_half: |
| | top_height = board_half - hole_half |
| | top_center = board_center + np.array([0, hole_half + top_height / 2]) |
| | A_top = np.eye(2) |
| | h_top = np.array([board_half + pad, top_height / 2 + pad]) |
| | obb2d_list.append((top_center, A_top, h_top)) |
| |
|
| | |
| | bottom_center = board_center + np.array([0, -(hole_half + top_height / 2)]) |
| | obb2d_list.append((bottom_center, A_top, h_top)) |
| |
|
| | |
| | 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_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: |
| | |
| | pass |
| |
|
| | if include_existing: |
| | |
| | if hasattr(self, "cube") and self.cube is not None: |
| | _push_actor_as_obb2d(self.cube, pad=0.0) |
| |
|
| | |
| | for ac in self._spawned_cubes: |
| | _push_actor_as_obb2d(ac, pad=0.0) |
| |
|
| | |
| | if avoid: |
| | for it in avoid: |
| | if isinstance(it, tuple): |
| | |
| | if len(it) == 3 and isinstance(it[0], np.ndarray) and isinstance(it[1], np.ndarray): |
| | |
| | obb2d_list.append(it) |
| | else: |
| | |
| | act_i, pad_i = it |
| | _push_actor_as_obb2d(act_i, pad=float(pad_i)) |
| | else: |
| | _push_actor_as_obb2d(it, pad=0.0) |
| |
|
| | |
| | circle_list = [] |
| | 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: |
| | |
| | _push_actor_as_obb2d(self.goal_site, pad=0.0) |
| | except Exception: |
| | |
| | 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)) |
| |
|
| | |
| | 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)): |
| | |
| | |
| |
|
| | u1 = torch.rand(1, generator=generator).item() |
| | u2 = torch.rand(1, generator=generator).item() |
| |
|
| | |
| | x = float(x_low + u1 * (x_high - x_low)) |
| | y = float(y_low + u2 * (y_high - y_low)) |
| |
|
| | if random_yaw: |
| | |
| | yaw_sample = torch.rand(1, generator=generator).item() |
| | yaw = float(yaw_sample * 2 * np.pi) |
| | else: |
| | yaw = 0.0 |
| |
|
| | |
| | c_new, A_new, h_new = _build_new_cube_obb2d(x, y, hs_new, yaw, pad_xy=float(min_gap)) |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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 |
| |
|
| | |
| | q = _yaw_to_quat_tensor(yaw, device=device) |
| |
|
| | cube = actors.build_cube( |
| | self.scene, |
| | half_size=hs_new, |
| | color=color, |
| | name=name_prefix, |
| | 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) |
| | 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, |
| | include_existing=True, |
| | include_goal=True, |
| | generator=None, |
| | randomize=True, |
| | target_style="purple", |
| | ): |
| | """ |
| | 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. |
| | """ |
| | |
| | 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)) |
| |
|
| | |
| | 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.") |
| |
|
| | |
| | obb2d_list = [] |
| |
|
| | def _push_actor_as_obb2d(actor, pad=0.0): |
| | try: |
| | |
| | if hasattr(actor, '_board_side') and hasattr(actor, '_hole_side'): |
| | |
| | board_side = actor._board_side |
| | hole_side = actor._hole_side |
| |
|
| | |
| | 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 |
| |
|
| | |
| | |
| | if board_half > hole_half: |
| | top_height = board_half - hole_half |
| | top_center = board_center + np.array([0, hole_half + top_height / 2]) |
| | A_top = np.eye(2) |
| | h_top = np.array([board_half + pad, top_height / 2 + pad]) |
| | obb2d_list.append((top_center, A_top, h_top)) |
| |
|
| | |
| | bottom_center = board_center + np.array([0, -(hole_half + top_height / 2)]) |
| | obb2d_list.append((bottom_center, A_top, h_top)) |
| |
|
| | |
| | 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_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: |
| | |
| | pass |
| |
|
| | if include_existing: |
| | |
| | if hasattr(self, "cube") and self.cube is not None: |
| | _push_actor_as_obb2d(self.cube, pad=0.0) |
| |
|
| | |
| | if hasattr(self, "target") and self.target is not None: |
| | _push_actor_as_obb2d(self.target, pad=0.0) |
| |
|
| | |
| | if hasattr(self, "_spawned_cubes"): |
| | for ac in self._spawned_cubes: |
| | _push_actor_as_obb2d(ac, pad=0.0) |
| |
|
| | |
| | circle_list = [] |
| | 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_existing: |
| | for ac in self._spawned_targets: |
| | target_r = getattr(ac, "_target_radius", target_radius) |
| | circle_list.append((_actor_xy(ac), target_r)) |
| |
|
| | |
| | if avoid: |
| | for it in avoid: |
| | if isinstance(it, tuple): |
| | |
| | if len(it) == 3 and isinstance(it[0], np.ndarray) and isinstance(it[1], np.ndarray): |
| | |
| | obb2d_list.append(it) |
| | else: |
| | |
| | act_i, pad_i = it |
| | |
| | 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: |
| | |
| | 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: |
| | |
| | _push_actor_as_obb2d(self.goal_site, pad=0.0) |
| | except Exception: |
| | |
| | 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)) |
| |
|
| | |
| | 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 |
| |
|
| | |
| | target_pos = np.array([x, y], dtype=np.float64) |
| | target_collision_radius = target_radius + min_gap |
| |
|
| | |
| | hit = False |
| | for (c_obs, A_obs, h_obs) in obb2d_list: |
| | |
| | |
| | local_pos = A_obs.T @ (target_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(target_pos - closest_world) |
| | if dist < target_collision_radius: |
| | hit = True |
| | break |
| | if hit: |
| | continue |
| |
|
| | |
| | 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 |
| |
|
| | |
| | rotate = np.array([np.cos(yaw/2), 0, 0, np.sin(yaw/2)]) |
| | angles = torch.deg2rad(torch.tensor([0.0, 90.0, 0.0], dtype=torch.float32)) |
| | 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, |
| | body_type="kinematic", |
| | add_collision=False, |
| | 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), |
| | np.eye(2, dtype=np.float64), |
| | np.array([half_size, half_size], dtype=np.float64) |
| | ) |
| |
|
| | def build_button( |
| | self, |
| | center_xy=(0.15, 0.10), |
| | base_half=[0.025, 0.025, 0.005], |
| | cap_radius=0.015, |
| | cap_half_len=0.006, |
| | travel=None, |
| | stiffness=800.0, |
| | damping=40.0, |
| | scale: float = None, |
| | generator=None, |
| | name: str = "button", |
| | randomize: bool = True, |
| | randomize_range=(0.1, 0.4), |
| | ): |
| | |
| | if scale is None: |
| | |
| | scale = getattr(self, "button_scale", 1.0) |
| | scale = float(scale) |
| |
|
| | |
| | if travel is None: |
| | |
| | base_travel = getattr(self, "_button_travel_base", 0.1) |
| | travel = base_travel * scale |
| | else: |
| | |
| | travel = float(travel) * scale |
| |
|
| | |
| | base_half = [bh * scale for bh in base_half] |
| | cap_radius = float(cap_radius) * scale |
| | cap_half_len = float(cap_half_len) * scale |
| |
|
| | |
| | self.button_travel = float(travel) |
| |
|
| | |
| | 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() |
| |
|
| | |
| | builder.initial_pose = sapien.Pose(p=[cx, cy, base_half[2]]) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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) |
| |
|
| | cap.set_joint_properties( |
| | type="prismatic", |
| | limits=[[-travel, 0.0]], |
| | 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] |
| | self.cap_link = self.cap_links[name] |
| |
|
| | |
| | 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, |
| | wall_thickness: float = 0.005, |
| | wall_height: float = 0.05, |
| | floor_thickness: float = 0.004, |
| | callsign=None, |
| | position=None, |
| | z_rotation_deg=0.0 |
| | ): |
| | """ |
| | 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 |
| |
|
| | |
| | inner_half = inner_side * 0.5 |
| | t = wall_thickness * 0.5 |
| | h = wall_height * 0.5 |
| | tf = floor_thickness * 0.5 |
| |
|
| | |
| | |
| | bottom_half = [inner_half + t, inner_half + t, tf] |
| | |
| | lr_wall_half = [t, inner_half + t, h] |
| | |
| | fb_wall_half = [inner_half + t, t, h] |
| |
|
| | |
| | if position is None: |
| | base_pos = [0.0, 0.0, 0.0] |
| | else: |
| | base_pos = list(position) |
| |
|
| | |
| | |
| | base_z = tf |
| |
|
| | |
| | |
| | offset = inner_half + t |
| | |
| | z_wall = tf + h |
| |
|
| | poses = [ |
| | sapien.Pose([0.0, 0.0, 0]), |
| | |
| | sapien.Pose([0.0, 0.0, base_z]), |
| | |
| | sapien.Pose([-offset, 0.0, z_wall]), |
| | sapien.Pose([+offset, 0.0, z_wall]), |
| | |
| | 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, |
| | lr_wall_half, |
| | fb_wall_half, |
| | fb_wall_half, |
| | ] |
| |
|
| | builder = self.scene.create_actor_builder() |
| |
|
| | |
| | angles = torch.deg2rad(torch.tensor([180.0, 0.0, z_rotation_deg], dtype=torch.float32)) |
| | rotate = matrix_to_quaternion( |
| | euler_angles_to_matrix(angles, convention="XYZ") |
| | ) |
| | |
| | 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) |
| |
|
| | |
| | inner_side = self.cube_half_size * 2.5 |
| | wall_thickness = 0.005 |
| | bin_half_size = (inner_side + wall_thickness) * 0.5 |
| |
|
| | |
| | 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.") |
| |
|
| | |
| | obb2d_list = [] |
| |
|
| | 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: |
| | |
| | pass |
| |
|
| | |
| | for item in avoid: |
| | if isinstance(item, tuple): |
| | |
| | if len(item) == 3 and isinstance(item[0], np.ndarray) and isinstance(item[1], np.ndarray): |
| | |
| | obb2d_list.append(item) |
| | else: |
| | |
| | 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) |
| |
|
| | |
| | bin_pos = np.array([x, y], dtype=np.float64) |
| | bin_collision_half_size = bin_half_size + min_gap |
| |
|
| | |
| | hit = False |
| | for (c_obs, A_obs, h_obs) in obb2d_list: |
| | |
| | |
| | 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 |
| |
|
| | |
| | z_rotation = float(torch.rand(1, generator=generator).item() * 90.0) |
| | 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, |
| | half_size=None, |
| | color=(1, 0, 0, 1), |
| | name_prefix="fixed_cube", |
| | yaw=0.0, |
| | 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) |
| |
|
| | |
| | pos = np.array(position, dtype=np.float64) |
| | if len(pos) == 2: |
| | |
| | pos = np.append(pos, hs) |
| |
|
| | |
| | builder = self.scene.create_actor_builder() |
| |
|
| | |
| | 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]) |
| |
|
| | |
| | builder.set_initial_pose( |
| | sapien.Pose( |
| | p=[pos[0], pos[1], pos[2]], |
| | q=rotate.numpy() if isinstance(rotate, torch.Tensor) else rotate |
| | ) |
| | ) |
| |
|
| | |
| | half_size_list = [hs, hs, hs] |
| | if dynamic==True: |
| | |
| | builder.add_box_collision(sapien.Pose([0, 0, 0]), half_size_list) |
| |
|
| | |
| | material = sapien.render.RenderMaterial() |
| | material.set_base_color(color) |
| | builder.add_box_visual(sapien.Pose([0, 0, 0]), half_size_list, material=material) |
| |
|
| | |
| | if dynamic==True: |
| | cube = builder.build_dynamic(name=name_prefix) |
| | else: |
| | cube = builder.build_kinematic(name=name_prefix) |
| |
|
| | |
| | cube._cube_half_size = hs |
| |
|
| | return cube |
| |
|
| | def build_board_with_hole( |
| | self, |
| | *, |
| | board_side=0.01, |
| | hole_side=0.06, |
| | thickness=0.02, |
| | position=None, |
| | rotation_quat=None, |
| | 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] |
| |
|
| |
|
| | |
| | board_half = board_side / 2 |
| | hole_half = hole_side / 2 |
| | thickness_half = thickness / 2 |
| |
|
| | |
| | |
| | center_position = [position[0], position[1], position[2] + thickness_half] |
| |
|
| | |
| | builder = self.scene.create_actor_builder() |
| |
|
| | |
| | if rotation_quat is None: |
| | rotation_quat = [1.0, 0.0, 0.0, 0.0] |
| | builder.set_initial_pose( |
| | sapien.Pose( |
| | p=center_position, |
| | q=rotation_quat |
| | ) |
| | ) |
| |
|
| | |
| | material = sapien.render.RenderMaterial() |
| | material.set_base_color([0.8, 0.6, 0.4, 1.0]) |
| |
|
| | |
| | |
| | top_width = board_side |
| | top_height = board_half - hole_half |
| | 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_width = board_side |
| | bottom_height = board_half - hole_half |
| | 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_width = board_half - hole_half |
| | left_height = hole_side |
| | 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_width = board_half - hole_half |
| | right_height = hole_side |
| | 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 |
| | ) |
| |
|
| | |
| | hole_cube_half_size_xy = hole_half |
| | hole_cube_half_height = thickness_half / 2 |
| |
|
| | |
| | black_material = sapien.render.RenderMaterial() |
| | black_material.set_base_color([0.0, 0.0, 0.0, 1.0]) |
| |
|
| | |
| | |
| | cube_center_z = -thickness_half + hole_cube_half_height |
| | builder.add_box_visual( |
| | sapien.Pose([0, 0, cube_center_z]), |
| | [hole_cube_half_size_xy, hole_cube_half_size_xy, hole_cube_half_height], |
| | material=black_material |
| | ) |
| |
|
| | |
| | board_actor = builder.build_kinematic(name=name) |
| |
|
| | |
| | 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 |
| |
|