Ray Caster

Ray Caster#

Ray patterns supported by the ray caster sensor

A ray caster reports the first collision along each ray in a chosen pattern. Rays do not bounce or respond to materials and opacity. Quadruped tasks use this sensor to measure terrain height around the robot.

The ray-casting implementation depends on the physics backend. With PhysX and OV PhysX, configured meshes are loaded into Warp when the sensor initializes and must remain static. With Newton, the sensor queries the live scene BVH and therefore sees every collision shape in its environment, global terrain, and dynamic bodies. Newton ignores mesh_prim_paths; use isaaclab_newton.sensors.NewtonRaycastSensorCfg to make that behavior explicit and to access Newton-specific options.

Choose a ray pattern and a parent frame. The frame and sensor offset set the pose; the pattern sets the ray origins and directions. Built-in patterns include lidar and grids.

@configclass
class RaycasterSensorSceneCfg(InteractiveSceneCfg):
    """Design the scene with sensors on the robot."""

    # ground plane
    ground = AssetBaseCfg(
        prim_path="/World/Ground",
        spawn=sim_utils.UsdFileCfg(
            usd_path=f"{ISAAC_NUCLEUS_DIR}/Environments/Terrains/rough_plane.usd",
            scale=(1, 1, 1),
        ),
    )

    # lights
    dome_light = AssetBaseCfg(
        prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
    )

    # robot
    robot = replace(ANYMAL_C_CFG, prim_path="{ENV_REGEX_NS}/Robot")

    ray_caster = RayCasterCfg(
        prim_path="{ENV_REGEX_NS}/Robot/base",
        update_period=1 / 60,
        offset=RayCasterCfg.OffsetCfg(pos=(0, 0, 0.5)),
        mesh_prim_paths=["/World/Ground"],
        ray_alignment="yaw",
        pattern_cfg=patterns.LidarPatternCfg(
            channels=100, vertical_fov_range=[-90, 90], horizontal_fov_range=[-90, 90], horizontal_res=1.0
        ),
        debug_vis=bool(args_cli.visualizer),
    )

Pattern angles use degrees. Enable debug visualization to inspect the pattern, then disable it when measuring performance.

Lidar Pattern visualized

Read hit positions from data.ray_hits_w during simulation:

def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
  .
  .
  .
  # Simulate physics
  while sim.is_running():
    .
    .
    .
    # print information from the sensors
      print("-------------------------------")
      print(scene["ray_caster"])
      print("Ray cast hit results: ", scene["ray_caster"].data.ray_hits_w)
-------------------------------
Ray-caster @ '/World/envs/env_.*/Robot/base/lidar_cage':
        view type            : <class 'isaacsim.core.experimental.prims.xform_prim.XformPrim'>
        update period (s)    : 0.016666666666666666
        number of meshes     : 1
        number of sensors    : 1
        number of rays/sensor: 18000
        total number of rays : 18000
Ray cast hit results:  tensor([[[-0.3698,  0.0357,  0.0000],
        [-0.3698,  0.0357,  0.0000],
        [-0.3698,  0.0357,  0.0000],
        ...,
        [    inf,     inf,     inf],
        [    inf,     inf,     inf],
        [    inf,     inf,     inf]]], device='cuda:0')
-------------------------------

Hit positions have shape (N, B, 3), where N is the number of sensors and B is the number of rays. Missed rays return inf. The repeated points in this example come from rays at the bottom pole of the hemispherical lidar pattern: all 180 horizontal samples meet there.

Try different patterns in the example below to inspect their hit positions.

Code for raycaster_sensor.py
  1# Copyright (c) 2022-2026, 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"""Cast a lidar-style ray pattern against rough terrain."""
  7
  8import argparse
  9from typing import TYPE_CHECKING
 10
 11import torch
 12
 13import isaaclab.sim as sim_utils
 14from isaaclab.app import add_launcher_args, launch_simulation
 15from isaaclab.assets import AssetBaseCfg
 16from isaaclab.scene import InteractiveSceneCfg
 17from isaaclab.sensors.ray_caster import RayCasterCfg, patterns
 18from isaaclab.utils import configclass, instantiate, replace
 19from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR
 20
 21from isaaclab_assets.robots.anymal import ANYMAL_C_CFG
 22
 23if TYPE_CHECKING:
 24    from isaaclab.scene import InteractiveScene
 25
 26parser = argparse.ArgumentParser(description="Example on using the raycaster sensor.")
 27parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to spawn.")
 28parser.add_argument("--log_interval", type=int, default=100, help="Steps between compact sensor summaries.")
 29parser.add_argument("--max_steps", type=int, default=-1, help="Stop after this many steps; negative runs forever.")
 30parser.add_argument(
 31    "--physics",
 32    default="isaacsim_physx",
 33    choices=["isaacsim_physx"],
 34    help="Physics backend.",
 35)
 36add_launcher_args(parser)
 37parser.set_defaults(visualizer=["kit"])
 38args_cli = parser.parse_args()
 39if args_cli.log_interval < 1:
 40    parser.error("--log_interval must be at least 1.")
 41if args_cli.max_steps == 0 or args_cli.max_steps < -1:
 42    parser.error("--max_steps must be positive or -1.")
 43
 44
 45@configclass
 46class RaycasterSensorSceneCfg(InteractiveSceneCfg):
 47    """Design the scene with sensors on the robot."""
 48
 49    # ground plane
 50    ground = AssetBaseCfg(
 51        prim_path="/World/Ground",
 52        spawn=sim_utils.UsdFileCfg(
 53            usd_path=f"{ISAAC_NUCLEUS_DIR}/Environments/Terrains/rough_plane.usd",
 54            scale=(1, 1, 1),
 55        ),
 56    )
 57
 58    # lights
 59    dome_light = AssetBaseCfg(
 60        prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
 61    )
 62
 63    # robot
 64    robot = replace(ANYMAL_C_CFG, prim_path="{ENV_REGEX_NS}/Robot")
 65
 66    ray_caster = RayCasterCfg(
 67        prim_path="{ENV_REGEX_NS}/Robot/base",
 68        update_period=1 / 60,
 69        offset=RayCasterCfg.OffsetCfg(pos=(0, 0, 0.5)),
 70        mesh_prim_paths=["/World/Ground"],
 71        ray_alignment="yaw",
 72        pattern_cfg=patterns.LidarPatternCfg(
 73            channels=100, vertical_fov_range=[-90, 90], horizontal_fov_range=[-90, 90], horizontal_res=1.0
 74        ),
 75        debug_vis=bool(args_cli.visualizer),
 76    )
 77
 78
 79def run_simulator(sim: sim_utils.SimulationContext, scene: "InteractiveScene") -> None:
 80    """Run the simulator."""
 81    # Define simulation stepping
 82    sim_dt = sim.get_physics_dt()
 83    count = 0
 84
 85    while sim.is_running() and (args_cli.max_steps < 0 or count < args_cli.max_steps):
 86        if count % 500 == 0:
 87            # reset the scene entities
 88            # root state
 89            # we offset the root state by the origin since the states are written in simulation world frame
 90            # if this is not done, then the robots will be spawned at the (0, 0, 0) of the simulation world
 91            root_pose = scene["robot"].data.default_root_pose.torch.clone()
 92            root_pose[:, :3] += scene.env_origins
 93            scene["robot"].write_root_pose_to_sim_index(root_pose=root_pose)
 94            root_vel = scene["robot"].data.default_root_vel.torch.clone()
 95            scene["robot"].write_root_velocity_to_sim_index(root_velocity=root_vel)
 96            # set joint positions with some noise
 97            joint_pos, joint_vel = (
 98                scene["robot"].data.default_joint_pos.torch.clone(),
 99                scene["robot"].data.default_joint_vel.torch.clone(),
100            )
101            joint_pos += torch.rand_like(joint_pos) * 0.1
102            scene["robot"].write_joint_position_to_sim_index(position=joint_pos)
103            scene["robot"].write_joint_velocity_to_sim_index(velocity=joint_vel)
104            # clear internal buffers
105            scene.reset()
106            print("[INFO]: Resetting robot state...")
107        targets = scene["robot"].data.default_joint_pos.torch
108        scene["robot"].set_joint_position_target_index(target=targets)
109        scene.write_data_to_sim()
110        sim.step()
111        count += 1
112        scene.update(sim_dt)
113
114        if count % args_cli.log_interval == 0:
115            hits = scene["ray_caster"].data.ray_hits_w.torch
116            valid = torch.isfinite(hits).all(dim=-1)
117            print(f"[INFO] step={count} ray hit rate={valid.float().mean().item():.1%}")
118
119
120def main() -> None:
121    """Run the ray-caster example."""
122    sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device)
123    with launch_simulation(sim_cfg, args_cli):
124        sim = sim_utils.SimulationContext(sim_cfg)
125        sim.set_camera_view(eye=[3.5, 3.5, 3.5], target=[0.0, 0.0, 0.0])
126        scene_cfg = RaycasterSensorSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0)
127        scene: InteractiveScene = instantiate(scene_cfg)
128        sim.reset()
129        print("[INFO]: Setup complete...")
130        run_simulator(sim, scene)
131
132
133if __name__ == "__main__":
134    main()