地形#

gs.morphs.Terrain 添加由高度场定义的静态刚性地面:一个二维高程网格。它是运动控制任务的标准地面:机器人行走在斜坡、台阶和障碍物上,而不是平坦的 Plane 上。构建地形有两种方式:让 Genesis World 程序化生成子地形网格,或提供你自己的高度场。

本页引用的三个可运行示例随 Genesis World 一起提供:

最小示例#

用最少的代码行把机器人尺度的地形放到地面上:2×2 的台阶子地形网格。

import genesis as gs

gs.init(backend=gs.gpu)  # use gs.cpu to run anywhere

scene = gs.Scene(show_viewer=True)

terrain = scene.add_entity(
    gs.morphs.Terrain(
        n_subterrains=(2, 2),
        subterrain_size=(6.0, 6.0),  # meters, per tile
        subterrain_types="pyramid_stairs_terrain",
    ),
)

scene.build()
for _ in range(1000):
    scene.step()

subterrain_types 传入单个字符串时,会用同一个生成器填充每个瓦片。

地形的表示方式#

地形是由高度场支撑的静态刚性实体:在规则网格上以整数高度采样构成的二维数组 height_field[i, j]。有两个比例尺把该网格换算成场景右手系、Z 轴向上的米制坐标:

  • horizontal_scale:相邻网格点之间的米数(单元格尺寸)。默认 0.25。

  • vertical_scale:每个高度场单位对应的米数。默认 0.005。

因此网格单元 (i, j) 位于世界坐标 (i * horizontal_scale, j * horizontal_scale, height_field[i, j] * vertical_scale),并加上 morph 的 pos 偏移。Genesis World 在构建时把该网格转换为两种表示:用于碰撞查询的高度图和 SDF,以及用于渲染的封闭三角网格。

Note

Genesis World 会自动计算地形碰撞 SDF 的分辨率,忽略在 gs.materials.Rigid() 上设置的任何分辨率。

程序化子地形#

运动控制任务很少需要手工编写高度场。取而代之的是用子地形铺满地面(这一做法由 Isaac Gym 推广),由指定的生成器填充每个瓦片。三个参数控制该网格:

  • n_subterrains=(nx, ny):x 和 y 方向上的瓦片数量。默认 (3, 3)。

  • subterrain_size=(sx, sy):每个瓦片的尺寸(米)。默认 (12.0, 12.0)。

  • subterrain_types:应用到所有瓦片的单个生成器名称,或与 n_subterrains 匹配的二维列表。

terrain = scene.add_entity(
    gs.morphs.Terrain(
        n_subterrains=(2, 2),
        subterrain_size=(6.0, 6.0),
        horizontal_scale=0.25,  # meters per grid cell
        vertical_scale=0.005,  # meters per height unit
        subterrain_types=[
            ["flat_terrain", "random_uniform_terrain"],
            ["pyramid_sloped_terrain", "discrete_obstacles_terrain"],
        ],
    ),
)

可用的生成器:

生成器

生成内容

flat_terrain

平坦瓦片。

random_uniform_terrain

均匀随机起伏。

sloped_terrain

单一恒定坡度。

pyramid_sloped_terrain

向中心峰顶升高的坡面。

discrete_obstacles_terrain

散布的凸起/凹陷方块。

wave_terrain

正弦波。

stairs_terrain

平行的台阶。

pyramid_stairs_terrain

向中心平台升高的台阶。

stepping_stones_terrain

彼此间隔的孤立石块。

fractal_terrain

分形噪声高度景观。

设置 randomize=True 可让涉及随机性的生成器在每次构建时使用新参数;保持默认的 False 时,它们使用固定种子,因此地形可复现。各生成器的设置可通过 subterrain_parameters 覆盖。

自定义高度场#

传入 height_field 数组即可用自己的数据构建地形,例如数字高程模型,或你生成的 NumPy 数组。height_field 会覆盖上面的子地形参数。

import numpy as np

horizontal_scale = 0.25
vertical_scale = 0.005
height_field = np.zeros([40, 40])
heights_range = np.arange(-10, 20, 10)
height_field[5:35, 5:35] = 200 + np.random.choice(heights_range, (30, 30))

terrain = scene.add_entity(
    gs.morphs.Terrain(
        horizontal_scale=horizontal_scale,
        vertical_scale=vertical_scale,
        height_field=height_field,
    ),
)

数值的单位是高度场单位而非米:vertical_scale=0.005 时,值为 200 的条目位于 1.0 m 高度。

场景构建完成后,实际使用的高度场可通过地形几何体上的 terrain.geoms[0].metadata["height_field"] 获取。这有助于验证几何形状,例如在每个采样点绘制一个调试球体:

height_field = terrain.geoms[0].metadata["height_field"]
rows = horizontal_scale * torch.arange(0, height_field.shape[0], device=gs.device)
cols = horizontal_scale * torch.arange(0, height_field.shape[1], device=gs.device)
rows = rows.unsqueeze(1).repeat((1, height_field.shape[1]))
cols = cols.unsqueeze(0).repeat((height_field.shape[0], 1))
heights = vertical_scale * torch.tensor(height_field, device=gs.device)
poss = torch.stack([rows, cols, heights], dim=-1).reshape((-1, 3))  # shape (n_cells, 3), meters
scene.draw_debug_spheres(poss=poss, radius=0.05, color=(0, 0, 1, 0.7))

从网格生成高度场#

如果你已经有一个三角网格形式的地形,genesis.utils.terrain.mesh_to_heightfield 会用垂直射线对其采样,并返回一个可传给 Terrain 的高度场。这以网格的精确几何换取高度场快速的碰撞查询能力。

from genesis.utils.terrain import mesh_to_heightfield

horizontal_scale = 2.0  # target grid spacing, mesh units
# heights: (nx, ny); xs: (nx,); ys: (ny,)
hf_terrain, xs, ys = mesh_to_heightfield(path_terrain, spacing=horizontal_scale, oversample=1)

# The height field starts at the origin; shift it to sit under the mesh.
translation = np.array([np.nanmin(xs), np.nanmin(ys), 0.0])

terrain = scene.add_entity(
    morph=gs.morphs.Terrain(
        horizontal_scale=horizontal_scale,
        vertical_scale=1.0,  # heights are already in mesh units (meters)
        height_field=hf_terrain,
        pos=translation,
    ),
)

spacing 是网格自身单位下的网格步长,oversample 会在每个单元投射更多射线,以免遗漏单元内部的峰值(内存随 oversample² 增长)。对于以 Y 轴向上创作的网格(例如 glTF),传入 up_axis="y";该函数会先将其旋转到 Z 轴向上再采样。没有射线命中的单元返回 NaN。

查询表面高度#

scene.add_entity(gs.morphs.Terrain(...)) 返回 TerrainEntity,其 get_terrain_height 报告世界坐标系 x-y 位置处地形表面的高程。高度来自物体实际碰撞的同一分段平面表面,因此运动控制策略可以读取每只脚下方的地面高度。

positions = torch.tensor([[1.0, 1.0], [2.0, 3.5]], device=gs.device)  # x-y, meters
heights = terrain.get_terrain_height(positions)  # shape ([n_envs,] n_points), meters

该查询会应用地形的平移和偏航,因此请提供世界坐标系下的位置,而不是网格坐标。positions 的形状决定查询如何批处理:

  • (2,): 单个点,结果会去掉点维度。

  • (n_points, 2): 各环境共享的点。

  • (n_envs, n_points, 2): 每个环境各自的位置。

在上述任何一种形状下同时传入 envs_idx,即可把查询限制到部分环境。

超出地形最多一个网格单元的位置会被钳制到边缘,再远的位置返回 NaN。包含 NaN 或无穷大的位置同样返回 NaN,相对世界竖直方向倾斜超过 0.001 rad 的地形上的所有位置也返回 NaN。

缓存生成的地形#

每次构建场景时都会生成地形(高度场、碰撞网格和可视网格)。传入 name="my_terrain" 可让给定一组选项只生成一次,并在后续构建时从缓存加载。即便 randomize=True 也是如此,因此为地形命名是在多次运行中精确复现随机地形的方法。

另请参阅#