射线投射器

射线投射器#

描述坐标变换基本几何关系的示意图

射线投射器传感器(以及射线投射相机)与基于 RTX 的渲染类似,都涉及射线投射。不同之处在于,射线投射器传感器仅返回沿射线方向的碰撞信息,并且可以指定每条射线的方向。射线不会发生反射,也不会受到材质或透明度等因素的影响。对于传感器指定的每条射线,都会沿其路径进行跟踪,并返回与指定网格的首次碰撞位置。我们的一些四足机器人示例使用此方法来测量局部高度场。

射线投射的实现取决于物理后端。在 PhysX 和 OV PhysX 上,配置的网格会在传感器初始化时加载到 Warp ,且必须保持静止。在 Newton 上,传感器查询实时场景的 BVH,因此能够感知所在环境中的所有碰撞形状、全局地形和动态刚体。Newton 会忽略 mesh_prim_paths ;可使用 isaaclab_newton.sensors.NewtonRaycastSensorCfg 明确指定这一行为,并访问 Newton 特有的选项。

使用射线投射器传感器需要指定 模式(pattern) 并附加到一个父级变换(parent xform)。模式定义了射线的投射方式,而主元(prim)属性定义了传感器的方向和位置(可以通过额外偏移实现更精确的放置)。Isaac Lab 支持多种射线投射模式配置,包括通用激光雷达(LIDAR)模式和网格模式。

@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),
    )

请注意,模式配置的单位是 度(degrees) !此外,我们在此启用了可视化,以便在渲染时直观展示模式,但这并非必需项,实际应用中应禁用此功能以优化性能。

激光雷达模式可视化

与其他传感器类似,可以在仿真运行时查询射线投射器传感器的数据。

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

这里可以看到传感器返回的数据。首先,注意数据开头和结尾各有 3 个闭括号,这表明数据按照传感器的数量进行了批处理。射线投射模式也被展平,因此数组的维度为 [N, B, 3] ,其中 N 是传感器数量, B 是模式中投射的射线数量,3 代表投射空间的维度。最后,注意到投射模式的前几个值是相同的,这是因为激光雷达模式是球形的,而我们指定的视场(FOV)为半球形,因此包含极点。在这种配置下, "展平模式" 就很明显: 前 180 个条目是相同的,因为它们对应于该半球的底部极点,并且由于我们的水平视场为 180 度、分辨率为 1 度,所以共有 180 个条目。

您可以使用此脚本实验不同的模式配置,并通过更改第 81 行的 triggered 变量,直观理解数据的存储方式。

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