| 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 |
|
|