光线投射传感器#

光线投射器(raycaster)以几何方式测量场景:它从传感器坐标系投射一组固定的光线,找出每条光线首次命中场景几何体的位置,并返回命中点及其距离。两种具体传感器共用这套机制,区别只在于如何解读结果:

  • gs.sensors.Lidar 返回原始命中集合(点云与逐条光线的距离),用于测距、建图和避障。

  • gs.sensors.DepthCamera 投射针孔相机的光线网格,并把距离重塑为深度图像。

光线数量和方向由模式(gs.sensors.RaycastPattern)决定。本页以两个可运行示例为准:一个安装在 Go2 上的遥操作传感器,见 examples/sensors/lidar_teleop.py;以及作用在刚体和可变形几何体上的深度相机,见 examples/sensors/depth_camera_custom_vverts.py。

最小示例#

在机器人上安装一个旋转式激光雷达,给光线添加一个可供命中的物体,然后每一步读取点云:

import genesis as gs

gs.init(backend=gs.gpu)

scene = gs.Scene(show_viewer=True)
scene.add_entity(gs.morphs.Plane())
robot = scene.add_entity(gs.morphs.URDF(file="urdf/go2/urdf/go2.urdf", fixed=True))
scene.add_entity(gs.morphs.Box(size=(0.5, 0.5, 1.0), pos=(2.0, 0.0, 0.5), fixed=True))

lidar = scene.add_sensor(
    gs.sensors.Lidar(
        pattern=gs.sensors.SphericalPattern(),  # 360deg x 60deg fov, 128 x 64 rays
        entity_idx=robot.idx,                   # mount on the robot's base link
        pos_offset=(0.3, 0.0, 0.1),             # meters, in the link frame
        max_range=20.0,                         # meters
        return_world_frame=True,
        draw_debug=True,                        # draw hit points in the viewer
    )
)

scene.build()
for _ in range(1000):
    scene.step()
    result = lidar.read()          # RaycasterReturnType(points, distances)
    points = result.points         # shape (128, 64, 3), meters, Z-up world frame
    distances = result.distances   # shape (128, 64), meters

激光雷达需要有几何体可供投射:如果刚体求解器未启用,且没有任何实体选择启用视觉光线投射,光线投射器会在构建阶段报错。

光线投射的工作原理#

模式是对光线的纯局部描述:它在传感器自身坐标系中为每条光线固定一个起点和一个单位方向,与传感器最终位于世界何处无关。模式的 return_shape(例如球面扫描的 (n_horizontal, n_vertical))决定每次读取都保持的布局。

在每次 read() 时,Genesis World 通过把所连接连杆的位姿与传感器的 pos_offset、euler_offset 组合,将模式放置到世界中,然后向基于场景几何体构建的加速结构投射每一条光线。每条光线报告它命中的第一个表面。若光线在 max_range 内未命中任何物体,或命中距离小于 min_range,则报告 no_hit_value(默认为 max_range),从而让返回的张量保持固定形状。

通过设置 entity_idx 和 link_idx_local 把传感器连接到连杆;光线随后随该连杆一起移动。若将 entity_idx 留空(或设为 None),传感器固定在世界坐标系中,此时 pos_offset 和 euler_offset 在世界坐标系下生效。

读取数据#

read() 返回 RaycasterReturnType,即由 points 和 distances 组成的 NamedTuple:

result = lidar.read()
points = result.points        # shape ([n_envs,] *return_shape, 3), meters
distances = result.distances  # shape ([n_envs,] *return_shape),    meters
  • distances 是沿光线方向的命中距离,单位为米,未命中处由 no_hit_value 填充。

  • points 是每条光线的命中位置。当 return_world_frame=True 时位于世界坐标系(Z 轴向上,单位米);默认为 return_world_frame=False,此时位于传感器局部坐标系。

  • return_points=False 只测量命中距离:read().points 为 None,传感器的内存占用和每步开销降到约四分之一。仅需距离的传感(例如深度图像)可以用它;需要点云时保持默认值 True。

末尾的轴与模式自身的布局一致:默认球面扫描为 (128, 64),深度相机为 (height, width),网格为 (n_x, n_y)。只有当场景在构建时包含多个环境,才会出现前导的 [n_envs,] 轴(参见多环境)。

光线模式#

模式决定了传感器的形态。在 gs.sensors 下构造以下三者中的任意一个,并作为 pattern 参数传入。

模式

光线布局

典型硬件

SphericalPattern

方位角 × 仰角扫描线

3D 激光雷达(Velodyne、Ouster)

DepthCameraPattern

针孔图像网格

深度相机(RealSense、Kinect)

GridPattern

平面上的平行光线

高度图、平面传感

SphericalPattern#

光线在水平(方位角)和垂直(仰角)视场角内展开。可以按光线数量、按角度分辨率或按显式角度数组来指定模式:

gs.sensors.SphericalPattern(
    fov=(360.0, 60.0),   # (horizontal, vertical) degrees; a scalar is centered on 0deg,
                         # a (min, max) tuple gives an asymmetric range
    n_points=(128, 64),  # (horizontal, vertical) ray counts -> return_shape (128, 64)
    # angular_resolution=(0.25, 0.5),  # degrees per ray; overrides n_points
    # angles=(h_angles, v_angles),     # explicit angle arrays; overrides the rest
)

要建模真实设备,可根据其数据手册设置 fov 和光线数量。例如 Velodyne VLP-16 为 fov=(360.0, 30.0), n_points=(1800, 16)。

DepthCameraPattern#

DepthCameraPattern 是一个针孔相机,其光轴为传感器的局部 +X 轴。可通过视场角或显式内参来配置:

gs.sensors.DepthCameraPattern(
    res=(128, 96),        # (width, height) in pixels -> return_shape (96, 128)
    fov_horizontal=90.0,  # degrees; fov_vertical is derived from the aspect ratio
    # fov_vertical=None,  # set instead to derive fov_horizontal, or set both
    # fx=None, fy=None,   # focal lengths in pixels, override the fov
    # cx=None, cy=None,   # principal point in pixels, defaults to the image center
)

GridPattern#

GridPattern 从平面上沿单一方向投射平行光线,例如传感器下方的高度图:

gs.sensors.GridPattern(
    resolution=0.1,              # grid spacing, meters (>= 1 mm)
    size=(2.0, 2.0),             # (length, width) of the grid, meters
    direction=(0.0, 0.0, -1.0),  # ray direction in the sensor frame (here: straight down)
)

自定义模式#

通过继承 gs.sensors.RaycastPattern 来投射任意光线集合:在 _get_return_shape() 中返回布局形状,并在 compute_ray_dirs() 中把传感器坐标系下的单位方向填入 _ray_dirs。

import torch

class CrossPattern(gs.sensors.RaycastPattern):
    def _get_return_shape(self):
        return (4,)

    def compute_ray_dirs(self):
        self._ray_dirs[:] = torch.tensor(
            [
                [1.0, 0.0, 0.0],   # forward
                [0.0, 1.0, 0.0],   # left
                [-1.0, 0.0, 0.0],  # back
                [0.0, -1.0, 0.0],  # right
            ],
            dtype=gs.tc_float,
            device=gs.device,
        )

sensor = scene.add_sensor(gs.sensors.Lidar(pattern=CrossPattern(), entity_idx=robot.idx))

深度相机#

gs.sensors.DepthCamera 是使用 DepthCameraPattern 的光线投射器。它提供激光雷达的全部能力,另外还有 read_image(),用于把逐条光线的距离重塑为深度图像:

depth_cam = scene.add_sensor(
    gs.sensors.DepthCamera(
        pattern=gs.sensors.DepthCameraPattern(
            res=(96, 72),         # (width, height) in pixels
            fov_horizontal=90.0,  # degrees
        ),
        entity_idx=go2.idx,
        link_idx_local=0,
        pos_offset=(0.3, 0.0, 0.1),  # meters, in the link frame
        max_range=5.0,               # meters
        return_world_frame=True,
    ),
)

scene.build()
scene.step()
depth = depth_cam.read_image()  # shape ([n_envs,] 72, 96), meters

read_image() 只返回 distances 字段,并重塑为 ([n_envs,] height, width);未命中处携带 no_hit_value。点云仍可通过 read().points 获取,除非创建相机时设置了 return_points=False——在只需要深度时,这会跳过点云以节省内存。由于深度相机与场景中的任何激光雷达共享同一套光线投射后端,二者针对相同的几何体投射。

运行 python examples/sensors/lidar_teleop.py --pattern depth 可在 Go2 上遥操作一台深度相机(--pattern 还接受 spherical 和 grid):

通用选项#

Lidar 和 DepthCamera 共用下列光线投射器选项(挂载字段对所有挂载式传感器通用):

gs.sensors.Lidar(
    pattern=pattern,
    entity_idx=robot.idx,        # attach to this entity; omit for a world-fixed sensor
    link_idx_local=0,            # which link of the entity to attach to
    pos_offset=(0.0, 0.0, 0.15), # meters, in the link frame
    euler_offset=(0.0, 0.0, 0.0),# extrinsic x-y-z, degrees
    min_range=0.0,               # meters; hits closer than this are dropped
    max_range=20.0,              # meters
    no_hit_value=None,           # value for a miss; defaults to max_range
    return_world_frame=False,    # world frame if True, else the sensor's local frame
    return_points=True,          # False measures distances only; read().points is then None
    draw_debug=False,            # draw ray starts and hit points in the viewer
)

多环境#

使用 n_envs > 0 构建后,每次读取都会多出一个位于最前面的批量轴;模式布局保持不变:

scene.build(n_envs=4)
scene.step()
result = lidar.read()
result.points.shape     # (4, 128, 64, 3)
result.distances.shape  # (4, 128, 64)

另见#

  • 传感器总览:传感器流水线、噪声、延迟和批处理读取。

  • 相机传感器:通过相机获取渲染的 RGB、深度和分割结果。

  • 记录数据:在仿真过程中一并保存深度图像和点云。

  • 约定:返回点所遵循的 Z 轴向上坐标系与单位约定。