What feature or enhancement are you proposing?
IsaacLab supports mesh ray-caster, there are also some rl workflows in which points sampled from the surface of the mesh(using some USD specific properties) are used as the observation for the RL policy.
I guess this would look something like the grid_raycaster, but only the ray intersections with the collision geometry of the attached entity should be returned by the sensor read.
Motivation
This sensor is somewhat common in RL, it would be great if this could be supported. Particularly helpful for manipulation tasks and less memory intensive than a grid_raycaster which will help with scaling the number of parallel envs
Potential Benefit
Faster simulation, Faster training helpful for non end-to-end RL training
What is the expected outcome of the implementation work?
- A mesh raycaster
OR
- add an argument for the general ray-caster to only 'see' certain entities or ignore certain entities
Additional information
an example of object only point sampling from IsaacLab
def sample_object_point_cloud(num_envs: int, num_points: int, prim_path: str, device: str = "cpu") -> torch.Tensor:
"""
Samples point clouds for each environment instance by collecting points
from all matching USD prims under `prim_path`, then downsamples to
exactly `num_points` per env using farthest-point sampling.
Caching is in-memory within this module:
- per-prim raw samples: _PRIM_SAMPLE_CACHE[(prim_hash, num_points)]
- final downsampled env: _FINAL_SAMPLE_CACHE[env_hash]
Returns:
torch.Tensor: Shape (num_envs, num_points, 3) on `device`.
"""
points = torch.zeros((num_envs, num_points, 3), dtype=torch.float32, device=device)
for i in range(num_envs):
# Resolve prim path
obj_path = prim_path.replace(".*", str(i))
# Gather prims
prims = get_all_matching_child_prims(
obj_path, predicate=lambda p: p.GetTypeName() in ("Mesh", "Cube", "Sphere", "Cylinder", "Capsule", "Cone")
)
if not prims:
raise KeyError(f"No valid prims under {obj_path}")
# hash each child prim by its rel transform + geometry
prim_hashes = []
for prim in prims:
prim_type = prim.GetTypeName()
hasher = hashlib.sha256()
rel = world_root.GetInverse() * xform_cache.GetLocalToWorldTransform(prim) # prim -> root
mat_np = np.array([[rel[r][c] for c in range(4)] for r in range(4)], dtype=np.float32)
hasher.update(mat_np.tobytes())
if prim_type == "Mesh":
mesh = UsdGeom.Mesh(prim)
verts = np.asarray(mesh.GetPointsAttr().Get(), dtype=np.float32)
hasher.update(verts.tobytes())
else:
if prim_type == "Cube":
size = UsdGeom.Cube(prim).GetSizeAttr().Get()
hasher.update(np.float32(size).tobytes())
elif prim_type == "Sphere":
r = UsdGeom.Sphere(prim).GetRadiusAttr().Get()
hasher.update(np.float32(r).tobytes())
elif prim_type == "Cylinder":
c = UsdGeom.Cylinder(prim)
hasher.update(np.float32(c.GetRadiusAttr().Get()).tobytes())
hasher.update(np.float32(c.GetHeightAttr().Get()).tobytes())
elif prim_type == "Capsule":
c = UsdGeom.Capsule(prim)
hasher.update(np.float32(c.GetRadiusAttr().Get()).tobytes())
hasher.update(np.float32(c.GetHeightAttr().Get()).tobytes())
elif prim_type == "Cone":
c = UsdGeom.Cone(prim)
hasher.update(np.float32(c.GetRadiusAttr().Get()).tobytes())
hasher.update(np.float32(c.GetHeightAttr().Get()).tobytes())
prim_hashes.append(hasher.hexdigest())
# scale on root (default to 1 if missing)
attr = object_prim.GetAttribute("xformOp:scale")
scale_val = attr.Get() if attr else None
if scale_val is None:
base_scale = torch.ones(3, dtype=torch.float32, device=device)
else:
base_scale = torch.tensor(scale_val, dtype=torch.float32, device=device)
# env-level cache key (includes num_points)
env_key = "_".join(sorted(prim_hashes)) + f"_{num_points}"
env_hash = hashlib.sha256(env_key.encode()).hexdigest()
# load from env-level in-memory cache
if env_hash in _FINAL_SAMPLE_CACHE:
arr = _FINAL_SAMPLE_CACHE[env_hash] # (num_points,3) in root frame
points[i] = torch.from_numpy(arr).to(device) * base_scale.unsqueeze(0)
continue
# otherwise build per-prim samples (with per-prim cache)
all_samples_np: list[np.ndarray] = []
for prim, ph in zip(prims, prim_hashes):
key = (ph, num_points)
if key in _PRIM_SAMPLE_CACHE:
samples = _PRIM_SAMPLE_CACHE[key]
else:
prim_type = prim.GetTypeName()
if prim_type == "Mesh":
mesh = UsdGeom.Mesh(prim)
verts = np.asarray(mesh.GetPointsAttr().Get(), dtype=np.float32)
faces = _triangulate_faces(prim)
mesh_tm = trimesh.Trimesh(vertices=verts, faces=faces, process=False)
else:
mesh_tm = create_primitive_mesh(prim)
face_weights = mesh_tm.area_faces
samples_np, _ = sample_surface(mesh_tm, num_points * 2, face_weight=face_weights)
# FPS to num_points on chosen device
tensor_pts = torch.from_numpy(samples_np.astype(np.float32)).to(device)
prim_idxs = farthest_point_sampling(tensor_pts, num_points)
local_pts = tensor_pts[prim_idxs]
# prim -> root transform
rel = xform_cache.GetLocalToWorldTransform(prim) * world_root.GetInverse()
mat_np = np.array([[rel[r][c] for c in range(4)] for r in range(4)], dtype=np.float32)
mat_t = torch.from_numpy(mat_np).to(device)
ones = torch.ones((num_points, 1), device=device)
pts_h = torch.cat([local_pts, ones], dim=1)
root_h = pts_h @ mat_t
samples = root_h[:, :3].detach().cpu().numpy()
if prim_type == "Cone":
samples[:, 2] -= UsdGeom.Cone(prim).GetHeightAttr().Get() / 2
_PRIM_SAMPLE_CACHE[key] = samples # cache in root frame @ num_points
all_samples_np.append(samples)
# combine & env-level FPS (if needed)
if len(all_samples_np) == 1:
samples_final = torch.from_numpy(all_samples_np[0]).to(device)
else:
combined = torch.from_numpy(np.concatenate(all_samples_np, axis=0)).to(device)
idxs = farthest_point_sampling(combined, num_points)
samples_final = combined[idxs]
# store env-level cache in root frame (CPU)
_FINAL_SAMPLE_CACHE[env_hash] = samples_final.detach().cpu().numpy()
# apply root scale and write out
points[i] = samples_final * base_scale.unsqueeze(0)
return points
API reference for mesh-raycaster
mesh-raycaster
@Milotrince
What feature or enhancement are you proposing?
IsaacLab supports mesh ray-caster, there are also some rl workflows in which points sampled from the surface of the mesh(using some USD specific properties) are used as the observation for the RL policy.
I guess this would look something like the grid_raycaster, but only the ray intersections with the collision geometry of the attached entity should be returned by the sensor read.
Motivation
This sensor is somewhat common in RL, it would be great if this could be supported. Particularly helpful for manipulation tasks and less memory intensive than a grid_raycaster which will help with scaling the number of parallel envs
Potential Benefit
Faster simulation, Faster training helpful for non end-to-end RL training
What is the expected outcome of the implementation work?
OR
Additional information
an example of object only point sampling from IsaacLab
API reference for mesh-raycaster
mesh-raycaster
@Milotrince