Skip to content

Commit 089cc7a

Browse files
renezurbrueggpascal-rothjtigue-bdaiMayankm96Copilot
authored andcommitted
Adds Raycaster with tracking for dynamic meshes (isaac-sim#3298)
# Description This PR introduces `MultiMeshRayCaster` and `MultiMeshRayCasterCamera`, an extension of the default `RayCaster` with the following enhancements: 1. **Raycasting against multiple target types** : Supports primitive shapes (spheres, cubes, …) as well as arbitrary meshes. 2. **Dynamic mesh tracking** : Keeps track of specified meshes, enabling raycasting against moving parts (e.g., robot links, articulated bodies, or dynamic obstacles). 3. **Memory-efficient caching** : Avoids redundant memory usage by caching and reusing duplicate meshes. This is joint work with @pascal-roth and @Mayankm96. The default `RayCaster` was limited to static environments and required manual handling of moving meshes, which restricted its use for robotics scenarios where robots or obstacles move dynamically. `MultiMeshRayCaster` addresses these limitations by and now supports **raycasting against robot parts and other moving entities**. --- ## Usage For a quick demo, run: ```bash python scripts/demos/sensors/multi_mesh_raycaster.py --num_envs 16 --asset_type <allegro_hand|anymal_d|multi> ``` <img width="2882" height="804" alt="demo image" src="https://github.com/user-attachments/assets/a019e9d4-e991-4ca2-a94e-8cdba790f26d" /> ### Drop-in replacement Example change to migrate from `RayCasterCfg` to `MultiMeshRayCasterCfg`: ```diff - ray_caster_cfg = RayCasterCfg( + ray_caster_cfg = MultiMeshRayCasterCfg( prim_path="{ENV_REGEX_NS}/Robot", mesh_prim_paths=[ "/World/Ground", + MultiMeshRayCasterCfg.RaycastTargetCfg(target_prim_expr="{ENV_REGEX_NS}/Robot/LF_.*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(target_prim_expr="{ENV_REGEX_NS}/Robot/RF_.*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(target_prim_expr="{ENV_REGEX_NS}/Robot/LH_.*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(target_prim_expr="{ENV_REGEX_NS}/Robot/RH_.*/visuals"), + MultiMeshRayCasterCfg.RaycastTargetCfg(target_prim_expr="{ENV_REGEX_NS}/Robot/base/visuals"), ], pattern_cfg=patterns.GridPatternCfg(resolution=resolution, size=(5.0, 5.0)), ) ``` --- ## Benchmarking & Validation To benchmark the new raycaster, run: ```bash python scripts/benchmarks/benchmark_ray_caster.py ``` Then plot the results with: ```bash python scripts/benchmarks/plot_raycast_results.py ``` This will generate outputs under: `outputs/benchmarks/raycast_benchmark...` ### Example plots <table style="border-collapse:collapse; width:100%;"> <!-- Row 1: big image across both columns --> <tr> <td colspan="2" style="text-align:center; padding-bottom:8px;"> <img width="1000" height="500" alt="big image" src="https://github.com/user-attachments/assets/ff69253c-0329-4ab6-a944-7fcaac233923" /> </td> </tr> <!-- Row 2: two images side by side --> <tr> <td style="text-align:center; padding-right:8px;"> <img width="500" height="500" alt="left image" src="https://github.com/user-attachments/assets/5375ed6c-f419-448d-ba25-759c1b16bcdd" /> </td> <td style="text-align:center;"> <img width="500" height="500" alt="right image" src="https://github.com/user-attachments/assets/bc36a0a6-aedd-4cdb-9909-9a05b1f2be0e" /> </td> </tr> <!-- Row 3: last image centered across both columns --> <tr> <td colspan="2" style="text-align:center; padding-top:8px;"> <img width="500" height="500" alt="bottom image" src="https://github.com/user-attachments/assets/67b75944-f64e-4c0b-b51f-aa20da3cf9b1" /> </td> </tr> </table> --- ## Type of Change * [x] New feature (non-breaking change which adds functionality) * [ ] This change requires a documentation update ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there <!-- As you go through the checklist above, you can mark something as done by putting an x character in it For example, - [x] I have done this task - [ ] I have not done this task --> --------- Signed-off-by: renezurbruegg <zrene@ethz.ch> Signed-off-by: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Signed-off-by: James Tigue <166445701+jtigue-bdai@users.noreply.github.com> Signed-off-by: Pascal Roth <57946385+pascal-roth@users.noreply.github.com> Signed-off-by: Kelly Guo <kellyg@nvidia.com> Co-authored-by: Pascal Roth <57946385+pascal-roth@users.noreply.github.com> Co-authored-by: Pascal Roth <roth.pascal@outlook.de> Co-authored-by: James Tigue <166445701+jtigue-bdai@users.noreply.github.com> Co-authored-by: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Co-authored-by: Mayank Mittal <mittalma@leggedrobotics.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Kelly Guo <kellyg@nvidia.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent 6e5c627 commit 089cc7a

22 files changed

+3790
-88
lines changed

docs/source/api/lab/isaaclab.utils.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
dict
1515
interpolation
1616
math
17+
mesh
1718
modifiers
1819
noise
1920
string
@@ -89,6 +90,14 @@ Math operations
8990
:inherited-members:
9091
:show-inheritance:
9192

93+
Mesh operations
94+
~~~~~~~~~~~~~~~
95+
96+
.. automodule:: isaaclab.utils.mesh
97+
:members:
98+
:imported-members:
99+
:show-inheritance:
100+
92101
Modifier operations
93102
~~~~~~~~~~~~~~~~~~~
94103

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
7+
"""Example on using the MultiMesh Raycaster sensor.
8+
9+
Usage:
10+
`python scripts/demos/sensors/multi_mesh_raycaster.py --num_envs 16 --asset_type <allegro_hand|anymal_d|multi>`
11+
"""
12+
13+
import argparse
14+
15+
from isaaclab.app import AppLauncher
16+
17+
# add argparse arguments
18+
parser = argparse.ArgumentParser(description="Example on using the raycaster sensor.")
19+
parser.add_argument("--num_envs", type=int, default=16, help="Number of environments to spawn.")
20+
parser.add_argument(
21+
"--asset_type",
22+
type=str,
23+
default="allegro_hand",
24+
help="Asset type to use.",
25+
choices=["allegro_hand", "anymal_d", "multi"],
26+
)
27+
# append AppLauncher cli args
28+
AppLauncher.add_app_launcher_args(parser)
29+
# parse the arguments
30+
args_cli = parser.parse_args()
31+
32+
# launch omniverse app
33+
app_launcher = AppLauncher(args_cli)
34+
simulation_app = app_launcher.app
35+
36+
"""Rest everything follows."""
37+
38+
import random
39+
import torch
40+
41+
import omni.usd
42+
from pxr import Gf, Sdf
43+
44+
##
45+
# Pre-defined configs
46+
##
47+
from isaaclab_assets.robots.allegro import ALLEGRO_HAND_CFG
48+
from isaaclab_assets.robots.anymal import ANYMAL_D_CFG
49+
50+
import isaaclab.sim as sim_utils
51+
from isaaclab.assets import Articulation, AssetBaseCfg, RigidObjectCfg
52+
from isaaclab.markers.config import VisualizationMarkersCfg
53+
from isaaclab.scene import InteractiveScene, InteractiveSceneCfg
54+
from isaaclab.sensors.ray_caster import MultiMeshRayCasterCfg, patterns
55+
from isaaclab.utils import configclass
56+
from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR
57+
58+
RAY_CASTER_MARKER_CFG = VisualizationMarkersCfg(
59+
markers={
60+
"hit": sim_utils.SphereCfg(
61+
radius=0.01,
62+
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0)),
63+
),
64+
},
65+
)
66+
67+
68+
if args_cli.asset_type == "allegro_hand":
69+
asset_cfg = ALLEGRO_HAND_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
70+
ray_caster_cfg = MultiMeshRayCasterCfg(
71+
prim_path="{ENV_REGEX_NS}/Robot",
72+
update_period=1 / 60,
73+
offset=MultiMeshRayCasterCfg.OffsetCfg(pos=(0, -0.1, 0.3)),
74+
mesh_prim_paths=[
75+
"/World/Ground",
76+
MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/thumb_link_.*/visuals_xform"),
77+
MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/index_link.*/visuals_xform"),
78+
MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/middle_link_.*/visuals_xform"),
79+
MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/ring_link_.*/visuals_xform"),
80+
MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/palm_link/visuals_xform"),
81+
MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/allegro_mount/visuals_xform"),
82+
],
83+
ray_alignment="world",
84+
pattern_cfg=patterns.GridPatternCfg(resolution=0.005, size=(0.4, 0.4), direction=(0, 0, -1)),
85+
debug_vis=not args_cli.headless,
86+
visualizer_cfg=RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/RayCaster"),
87+
)
88+
89+
elif args_cli.asset_type == "anymal_d":
90+
asset_cfg = ANYMAL_D_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
91+
ray_caster_cfg = MultiMeshRayCasterCfg(
92+
prim_path="{ENV_REGEX_NS}/Robot",
93+
update_period=1 / 60,
94+
offset=MultiMeshRayCasterCfg.OffsetCfg(pos=(0, -0.1, 0.3)),
95+
mesh_prim_paths=[
96+
"/World/Ground",
97+
MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/LF_.*/visuals"),
98+
MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/RF_.*/visuals"),
99+
MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/LH_.*/visuals"),
100+
MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/RH_.*/visuals"),
101+
MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Robot/base/visuals"),
102+
],
103+
ray_alignment="world",
104+
pattern_cfg=patterns.GridPatternCfg(resolution=0.02, size=(2.5, 2.5), direction=(0, 0, -1)),
105+
debug_vis=not args_cli.headless,
106+
visualizer_cfg=RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/RayCaster"),
107+
)
108+
109+
elif args_cli.asset_type == "multi":
110+
asset_cfg = RigidObjectCfg(
111+
prim_path="{ENV_REGEX_NS}/Object",
112+
spawn=sim_utils.MultiAssetSpawnerCfg(
113+
assets_cfg=[
114+
sim_utils.CuboidCfg(
115+
size=(0.3, 0.3, 0.3),
116+
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2),
117+
),
118+
sim_utils.SphereCfg(
119+
radius=0.3,
120+
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0), metallic=0.2),
121+
),
122+
sim_utils.CylinderCfg(
123+
radius=0.2,
124+
height=0.5,
125+
axis="Y",
126+
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2),
127+
),
128+
sim_utils.CapsuleCfg(
129+
radius=0.15,
130+
height=0.5,
131+
axis="Z",
132+
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 1.0, 0.0), metallic=0.2),
133+
),
134+
sim_utils.ConeCfg(
135+
radius=0.2,
136+
height=0.5,
137+
axis="Z",
138+
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 1.0), metallic=0.2),
139+
),
140+
],
141+
random_choice=True,
142+
rigid_props=sim_utils.RigidBodyPropertiesCfg(
143+
solver_position_iteration_count=4, solver_velocity_iteration_count=0
144+
),
145+
mass_props=sim_utils.MassPropertiesCfg(mass=1.0),
146+
collision_props=sim_utils.CollisionPropertiesCfg(),
147+
),
148+
init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 2.0)),
149+
)
150+
ray_caster_cfg = MultiMeshRayCasterCfg(
151+
prim_path="{ENV_REGEX_NS}/Object",
152+
update_period=1 / 60,
153+
offset=MultiMeshRayCasterCfg.OffsetCfg(pos=(0, 0.0, 0.6)),
154+
mesh_prim_paths=[
155+
"/World/Ground",
156+
MultiMeshRayCasterCfg.RaycastTargetCfg(prim_expr="{ENV_REGEX_NS}/Object"),
157+
],
158+
ray_alignment="world",
159+
pattern_cfg=patterns.GridPatternCfg(resolution=0.01, size=(0.6, 0.6), direction=(0, 0, -1)),
160+
debug_vis=not args_cli.headless,
161+
visualizer_cfg=RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/RayCaster"),
162+
)
163+
else:
164+
raise ValueError(f"Unknown asset type: {args_cli.asset_type}")
165+
166+
167+
@configclass
168+
class RaycasterSensorSceneCfg(InteractiveSceneCfg):
169+
"""Design the scene with sensors on the asset."""
170+
171+
# ground plane
172+
ground = AssetBaseCfg(
173+
prim_path="/World/Ground",
174+
spawn=sim_utils.UsdFileCfg(
175+
usd_path=f"{ISAAC_NUCLEUS_DIR}/Environments/Terrains/rough_plane.usd",
176+
scale=(1, 1, 1),
177+
),
178+
)
179+
180+
# lights
181+
dome_light = AssetBaseCfg(
182+
prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
183+
)
184+
185+
# asset
186+
asset = asset_cfg
187+
# ray caster
188+
ray_caster = ray_caster_cfg
189+
190+
191+
def randomize_shape_color(prim_path_expr: str):
192+
"""Randomize the color of the geometry."""
193+
194+
# acquire stage
195+
stage = omni.usd.get_context().get_stage()
196+
# resolve prim paths for spawning and cloning
197+
prim_paths = sim_utils.find_matching_prim_paths(prim_path_expr)
198+
# manually clone prims if the source prim path is a regex expression
199+
200+
with Sdf.ChangeBlock():
201+
for prim_path in prim_paths:
202+
print("Applying prim scale to:", prim_path)
203+
# spawn single instance
204+
prim_spec = Sdf.CreatePrimInLayer(stage.GetRootLayer(), prim_path)
205+
206+
# DO YOUR OWN OTHER KIND OF RANDOMIZATION HERE!
207+
# Note: Just need to acquire the right attribute about the property you want to set
208+
# Here is an example on setting color randomly
209+
color_spec = prim_spec.GetAttributeAtPath(prim_path + "/geometry/material/Shader.inputs:diffuseColor")
210+
color_spec.default = Gf.Vec3f(random.random(), random.random(), random.random())
211+
212+
# randomize scale
213+
scale_spec = prim_spec.GetAttributeAtPath(prim_path + ".xformOp:scale")
214+
scale_spec.default = Gf.Vec3f(random.uniform(0.5, 1.5), random.uniform(0.5, 1.5), random.uniform(0.5, 1.5))
215+
216+
217+
def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
218+
"""Run the simulator."""
219+
# Define simulation stepping
220+
sim_dt = sim.get_physics_dt()
221+
sim_time = 0.0
222+
count = 0
223+
224+
# Simulate physics
225+
while simulation_app.is_running():
226+
227+
if count % 500 == 0:
228+
# reset counter
229+
count = 0
230+
# reset the scene entities
231+
# root state
232+
root_state = scene["asset"].data.default_root_state.clone()
233+
root_state[:, :3] += scene.env_origins
234+
scene["asset"].write_root_pose_to_sim(root_state[:, :7])
235+
scene["asset"].write_root_velocity_to_sim(root_state[:, 7:])
236+
237+
if isinstance(scene["asset"], Articulation):
238+
# set joint positions with some noise
239+
joint_pos, joint_vel = (
240+
scene["asset"].data.default_joint_pos.clone(),
241+
scene["asset"].data.default_joint_vel.clone(),
242+
)
243+
joint_pos += torch.rand_like(joint_pos) * 0.1
244+
scene["asset"].write_joint_state_to_sim(joint_pos, joint_vel)
245+
# clear internal buffers
246+
scene.reset()
247+
print("[INFO]: Resetting Asset state...")
248+
249+
if isinstance(scene["asset"], Articulation):
250+
# -- generate actions/commands
251+
targets = scene["asset"].data.default_joint_pos + 5 * (
252+
torch.rand_like(scene["asset"].data.default_joint_pos) - 0.5
253+
)
254+
# -- apply action to the asset
255+
scene["asset"].set_joint_position_target(targets)
256+
# -- write data to sim
257+
scene.write_data_to_sim()
258+
# perform step
259+
sim.step()
260+
# update sim-time
261+
sim_time += sim_dt
262+
count += 1
263+
# update buffers
264+
scene.update(sim_dt)
265+
266+
267+
def main():
268+
"""Main function."""
269+
270+
# Initialize the simulation context
271+
sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device)
272+
sim = sim_utils.SimulationContext(sim_cfg)
273+
# Set main camera
274+
sim.set_camera_view(eye=[3.5, 3.5, 3.5], target=[0.0, 0.0, 0.0])
275+
# design scene
276+
scene_cfg = RaycasterSensorSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0, replicate_physics=False)
277+
scene = InteractiveScene(scene_cfg)
278+
279+
if args_cli.asset_type == "multi":
280+
randomize_shape_color(scene_cfg.asset.prim_path.format(ENV_REGEX_NS="/World/envs/env_.*"))
281+
282+
# Play the simulator
283+
sim.reset()
284+
# Now we are ready!
285+
print("[INFO]: Setup complete...")
286+
# Run the simulator
287+
run_simulator(sim, scene)
288+
289+
290+
if __name__ == "__main__":
291+
# run the main function
292+
main()
293+
# close sim app
294+
simulation_app.close()

0 commit comments

Comments
 (0)