# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Functions to generate different terrains using the ``trimesh`` library."""
from __future__ import annotations
import numpy as np
import scipy.spatial.transform as tf
import torch
import trimesh
from typing import TYPE_CHECKING
from .utils import * # noqa: F401, F403
from .utils import make_border, make_plane
if TYPE_CHECKING:
from . import mesh_terrains_cfg
[docs]def flat_terrain(
difficulty: float, cfg: mesh_terrains_cfg.MeshPlaneTerrainCfg
) -> tuple[list[trimesh.Trimesh], np.ndarray]:
"""Generate a flat terrain as a plane.
.. image:: ../../_static/terrains/trimesh/flat_terrain.jpg
:width: 45%
:align: center
Note:
The :obj:`difficulty` parameter is ignored for this terrain.
Args:
difficulty: The difficulty of the terrain. This is a value between 0 and 1.
cfg: The configuration for the terrain.
Returns:
A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m).
"""
# compute the position of the terrain
origin = (cfg.size[0] / 2.0, cfg.size[1] / 2.0, 0.0)
# compute the vertices of the terrain
plane_mesh = make_plane(cfg.size, 0.0, center_zero=False)
# return the tri-mesh and the position
return [plane_mesh], np.array(origin)
[docs]def pyramid_stairs_terrain(
difficulty: float, cfg: mesh_terrains_cfg.MeshPyramidStairsTerrainCfg
) -> tuple[list[trimesh.Trimesh], np.ndarray]:
"""Generate a terrain with a pyramid stair pattern.
The terrain is a pyramid stair pattern which trims to a flat platform at the center of the terrain.
If :obj:`cfg.holes` is True, the terrain will have pyramid stairs of length or width
:obj:`cfg.platform_width` (depending on the direction) with no steps in the remaining area. Additionally,
no border will be added.
.. image:: ../../_static/terrains/trimesh/pyramid_stairs_terrain.jpg
:width: 45%
.. image:: ../../_static/terrains/trimesh/pyramid_stairs_terrain_with_holes.jpg
:width: 45%
Args:
difficulty: The difficulty of the terrain. This is a value between 0 and 1.
cfg: The configuration for the terrain.
Returns:
A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m).
"""
# resolve the terrain configuration
step_height = cfg.step_height_range[0] + difficulty * (cfg.step_height_range[1] - cfg.step_height_range[0])
# compute number of steps in x and y direction
num_steps_x = (cfg.size[0] - 2 * cfg.border_width - cfg.platform_width) // (2 * cfg.step_width) + 1
num_steps_y = (cfg.size[1] - 2 * cfg.border_width - cfg.platform_width) // (2 * cfg.step_width) + 1
# we take the minimum number of steps in x and y direction
num_steps = int(min(num_steps_x, num_steps_y))
# initialize list of meshes
meshes_list = list()
# generate the border if needed
if cfg.border_width > 0.0 and not cfg.holes:
# obtain a list of meshes for the border
border_center = [0.5 * cfg.size[0], 0.5 * cfg.size[1], -step_height / 2]
border_inner_size = (cfg.size[0] - 2 * cfg.border_width, cfg.size[1] - 2 * cfg.border_width)
make_borders = make_border(cfg.size, border_inner_size, step_height, border_center)
# add the border meshes to the list of meshes
meshes_list += make_borders
# generate the terrain
# -- compute the position of the center of the terrain
terrain_center = [0.5 * cfg.size[0], 0.5 * cfg.size[1], 0.0]
terrain_size = (cfg.size[0] - 2 * cfg.border_width, cfg.size[1] - 2 * cfg.border_width)
# -- generate the stair pattern
for k in range(num_steps):
# check if we need to add holes around the steps
if cfg.holes:
box_size = (cfg.platform_width, cfg.platform_width)
else:
box_size = (terrain_size[0] - 2 * k * cfg.step_width, terrain_size[1] - 2 * k * cfg.step_width)
# compute the quantities of the box
# -- location
box_z = terrain_center[2] + k * step_height / 2.0
box_offset = (k + 0.5) * cfg.step_width
# -- dimensions
box_height = (k + 2) * step_height
# generate the boxes
# top/bottom
box_dims = (box_size[0], cfg.step_width, box_height)
# -- top
box_pos = (terrain_center[0], terrain_center[1] + terrain_size[1] / 2.0 - box_offset, box_z)
box_top = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos))
# -- bottom
box_pos = (terrain_center[0], terrain_center[1] - terrain_size[1] / 2.0 + box_offset, box_z)
box_bottom = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos))
# right/left
if cfg.holes:
box_dims = (cfg.step_width, box_size[1], box_height)
else:
box_dims = (cfg.step_width, box_size[1] - 2 * cfg.step_width, box_height)
# -- right
box_pos = (terrain_center[0] + terrain_size[0] / 2.0 - box_offset, terrain_center[1], box_z)
box_right = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos))
# -- left
box_pos = (terrain_center[0] - terrain_size[0] / 2.0 + box_offset, terrain_center[1], box_z)
box_left = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos))
# add the boxes to the list of meshes
meshes_list += [box_top, box_bottom, box_right, box_left]
# generate final box for the middle of the terrain
box_dims = (
terrain_size[0] - 2 * num_steps * cfg.step_width,
terrain_size[1] - 2 * num_steps * cfg.step_width,
(num_steps + 2) * step_height,
)
box_pos = (terrain_center[0], terrain_center[1], terrain_center[2] + num_steps * step_height / 2)
box_middle = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos))
meshes_list.append(box_middle)
# origin of the terrain
origin = np.array([terrain_center[0], terrain_center[1], (num_steps + 1) * step_height])
return meshes_list, origin
[docs]def inverted_pyramid_stairs_terrain(
difficulty: float, cfg: mesh_terrains_cfg.MeshInvertedPyramidStairsTerrainCfg
) -> tuple[list[trimesh.Trimesh], np.ndarray]:
"""Generate a terrain with a inverted pyramid stair pattern.
The terrain is an inverted pyramid stair pattern which trims to a flat platform at the center of the terrain.
If :obj:`cfg.holes` is True, the terrain will have pyramid stairs of length or width
:obj:`cfg.platform_width` (depending on the direction) with no steps in the remaining area. Additionally,
no border will be added.
.. image:: ../../_static/terrains/trimesh/inverted_pyramid_stairs_terrain.jpg
:width: 45%
.. image:: ../../_static/terrains/trimesh/inverted_pyramid_stairs_terrain_with_holes.jpg
:width: 45%
Args:
difficulty: The difficulty of the terrain. This is a value between 0 and 1.
cfg: The configuration for the terrain.
Returns:
A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m).
"""
# resolve the terrain configuration
step_height = cfg.step_height_range[0] + difficulty * (cfg.step_height_range[1] - cfg.step_height_range[0])
# compute number of steps in x and y direction
num_steps_x = (cfg.size[0] - 2 * cfg.border_width - cfg.platform_width) // (2 * cfg.step_width) + 1
num_steps_y = (cfg.size[1] - 2 * cfg.border_width - cfg.platform_width) // (2 * cfg.step_width) + 1
# we take the minimum number of steps in x and y direction
num_steps = int(min(num_steps_x, num_steps_y))
# total height of the terrain
total_height = (num_steps + 1) * step_height
# initialize list of meshes
meshes_list = list()
# generate the border if needed
if cfg.border_width > 0.0 and not cfg.holes:
# obtain a list of meshes for the border
border_center = [0.5 * cfg.size[0], 0.5 * cfg.size[1], -0.5 * step_height]
border_inner_size = (cfg.size[0] - 2 * cfg.border_width, cfg.size[1] - 2 * cfg.border_width)
make_borders = make_border(cfg.size, border_inner_size, step_height, border_center)
# add the border meshes to the list of meshes
meshes_list += make_borders
# generate the terrain
# -- compute the position of the center of the terrain
terrain_center = [0.5 * cfg.size[0], 0.5 * cfg.size[1], 0.0]
terrain_size = (cfg.size[0] - 2 * cfg.border_width, cfg.size[1] - 2 * cfg.border_width)
# -- generate the stair pattern
for k in range(num_steps):
# check if we need to add holes around the steps
if cfg.holes:
box_size = (cfg.platform_width, cfg.platform_width)
else:
box_size = (terrain_size[0] - 2 * k * cfg.step_width, terrain_size[1] - 2 * k * cfg.step_width)
# compute the quantities of the box
# -- location
box_z = terrain_center[2] - total_height / 2 - (k + 1) * step_height / 2.0
box_offset = (k + 0.5) * cfg.step_width
# -- dimensions
box_height = total_height - (k + 1) * step_height
# generate the boxes
# top/bottom
box_dims = (box_size[0], cfg.step_width, box_height)
# -- top
box_pos = (terrain_center[0], terrain_center[1] + terrain_size[1] / 2.0 - box_offset, box_z)
box_top = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos))
# -- bottom
box_pos = (terrain_center[0], terrain_center[1] - terrain_size[1] / 2.0 + box_offset, box_z)
box_bottom = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos))
# right/left
if cfg.holes:
box_dims = (cfg.step_width, box_size[1], box_height)
else:
box_dims = (cfg.step_width, box_size[1] - 2 * cfg.step_width, box_height)
# -- right
box_pos = (terrain_center[0] + terrain_size[0] / 2.0 - box_offset, terrain_center[1], box_z)
box_right = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos))
# -- left
box_pos = (terrain_center[0] - terrain_size[0] / 2.0 + box_offset, terrain_center[1], box_z)
box_left = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos))
# add the boxes to the list of meshes
meshes_list += [box_top, box_bottom, box_right, box_left]
# generate final box for the middle of the terrain
box_dims = (
terrain_size[0] - 2 * num_steps * cfg.step_width,
terrain_size[1] - 2 * num_steps * cfg.step_width,
step_height,
)
box_pos = (terrain_center[0], terrain_center[1], terrain_center[2] - total_height - step_height / 2)
box_middle = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos))
meshes_list.append(box_middle)
# origin of the terrain
origin = np.array([terrain_center[0], terrain_center[1], -(num_steps + 1) * step_height])
return meshes_list, origin
[docs]def random_grid_terrain(
difficulty: float, cfg: mesh_terrains_cfg.MeshRandomGridTerrainCfg
) -> tuple[list[trimesh.Trimesh], np.ndarray]:
"""Generate a terrain with cells of random heights and fixed width.
The terrain is generated in the x-y plane and has a height of 1.0. It is then divided into a grid of the
specified size :obj:`cfg.grid_width`. Each grid cell is then randomly shifted in the z-direction by a value uniformly
sampled between :obj:`cfg.grid_height_range`. At the center of the terrain, a platform of the specified width
:obj:`cfg.platform_width` is generated.
If :obj:`cfg.holes` is True, the terrain will have randomized grid cells only along the plane extending
from the platform (like a plus sign). The remaining area remains empty and no border will be added.
.. image:: ../../_static/terrains/trimesh/random_grid_terrain.jpg
:width: 45%
.. image:: ../../_static/terrains/trimesh/random_grid_terrain_with_holes.jpg
:width: 45%
Args:
difficulty: The difficulty of the terrain. This is a value between 0 and 1.
cfg: The configuration for the terrain.
Returns:
A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m).
Raises:
ValueError: If the terrain is not square. This method only supports square terrains.
RuntimeError: If the grid width is large such that the border width is negative.
"""
# check to ensure square terrain
if cfg.size[0] != cfg.size[1]:
raise ValueError(f"The terrain must be square. Received size: {cfg.size}.")
# resolve the terrain configuration
grid_height = cfg.grid_height_range[0] + difficulty * (cfg.grid_height_range[1] - cfg.grid_height_range[0])
# initialize list of meshes
meshes_list = list()
# compute the number of boxes in each direction
num_boxes_x = int(cfg.size[0] / cfg.grid_width)
num_boxes_y = int(cfg.size[1] / cfg.grid_width)
# constant parameters
terrain_height = 1.0
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
# generate the border
border_width = cfg.size[0] - min(num_boxes_x, num_boxes_y) * cfg.grid_width
if border_width > 0:
# compute parameters for the border
border_center = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -terrain_height / 2)
border_inner_size = (cfg.size[0] - border_width, cfg.size[1] - border_width)
# create border meshes
make_borders = make_border(cfg.size, border_inner_size, terrain_height, border_center)
meshes_list += make_borders
else:
raise RuntimeError("Border width must be greater than 0! Adjust the parameter 'cfg.grid_width'.")
# create a template grid of terrain height
grid_dim = [cfg.grid_width, cfg.grid_width, terrain_height]
grid_position = [0.5 * cfg.grid_width, 0.5 * cfg.grid_width, -terrain_height / 2]
template_box = trimesh.creation.box(grid_dim, trimesh.transformations.translation_matrix(grid_position))
# extract vertices and faces of the box to create a template
template_vertices = template_box.vertices # (8, 3)
template_faces = template_box.faces
# repeat the template box vertices to span the terrain (num_boxes_x * num_boxes_y, 8, 3)
vertices = torch.tensor(template_vertices, device=device).repeat(num_boxes_x * num_boxes_y, 1, 1)
# create a meshgrid to offset the vertices
x = torch.arange(0, num_boxes_x, device=device)
y = torch.arange(0, num_boxes_y, device=device)
xx, yy = torch.meshgrid(x, y, indexing="ij")
xx = xx.flatten().view(-1, 1)
yy = yy.flatten().view(-1, 1)
xx_yy = torch.cat((xx, yy), dim=1)
# offset the vertices
offsets = cfg.grid_width * xx_yy + border_width / 2
vertices[:, :, :2] += offsets.unsqueeze(1)
# mask the vertices to create holes, s.t. only grids along the x and y axis are present
if cfg.holes:
# -- x-axis
mask_x = torch.logical_and(
(vertices[:, :, 0] > (cfg.size[0] - border_width - cfg.platform_width) / 2).all(dim=1),
(vertices[:, :, 0] < (cfg.size[0] + border_width + cfg.platform_width) / 2).all(dim=1),
)
vertices_x = vertices[mask_x]
# -- y-axis
mask_y = torch.logical_and(
(vertices[:, :, 1] > (cfg.size[1] - border_width - cfg.platform_width) / 2).all(dim=1),
(vertices[:, :, 1] < (cfg.size[1] + border_width + cfg.platform_width) / 2).all(dim=1),
)
vertices_y = vertices[mask_y]
# -- combine these vertices
vertices = torch.cat((vertices_x, vertices_y))
# add noise to the vertices to have a random height over each grid cell
num_boxes = len(vertices)
# create noise for the z-axis
h_noise = torch.zeros((num_boxes, 3), device=device)
h_noise[:, 2].uniform_(-grid_height, grid_height)
# reshape noise to match the vertices (num_boxes, 4, 3)
# only the top vertices of the box are affected
vertices_noise = torch.zeros((num_boxes, 4, 3), device=device)
vertices_noise += h_noise.unsqueeze(1)
# add height only to the top vertices of the box
vertices[vertices[:, :, 2] == 0] += vertices_noise.view(-1, 3)
# move to numpy
vertices = vertices.reshape(-1, 3).cpu().numpy()
# create faces for boxes (num_boxes, 12, 3). Each box has 6 faces, each face has 2 triangles.
faces = torch.tensor(template_faces, device=device).repeat(num_boxes, 1, 1)
face_offsets = torch.arange(0, num_boxes, device=device).unsqueeze(1).repeat(1, 12) * 8
faces += face_offsets.unsqueeze(2)
# move to numpy
faces = faces.view(-1, 3).cpu().numpy()
# convert to trimesh
grid_mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
meshes_list.append(grid_mesh)
# add a platform in the center of the terrain that is accessible from all sides
dim = (cfg.platform_width, cfg.platform_width, terrain_height + grid_height)
pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -terrain_height / 2 + grid_height / 2)
box_platform = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos))
meshes_list.append(box_platform)
# specify the origin of the terrain
origin = np.array([0.5 * cfg.size[0], 0.5 * cfg.size[1], grid_height])
return meshes_list, origin
[docs]def rails_terrain(
difficulty: float, cfg: mesh_terrains_cfg.MeshRailsTerrainCfg
) -> tuple[list[trimesh.Trimesh], np.ndarray]:
"""Generate a terrain with box rails as extrusions.
The terrain contains two sets of box rails created as extrusions. The first set (inner rails) is extruded from
the platform at the center of the terrain, and the second set is extruded between the first set of rails
and the terrain border. Each set of rails is extruded to the same height.
.. image:: ../../_static/terrains/trimesh/rails_terrain.jpg
:width: 40%
:align: center
Args:
difficulty: The difficulty of the terrain. this is a value between 0 and 1.
cfg: The configuration for the terrain.
Returns:
A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m).
"""
# resolve the terrain configuration
rail_height = cfg.rail_height_range[1] - difficulty * (cfg.rail_height_range[1] - cfg.rail_height_range[0])
# initialize list of meshes
meshes_list = list()
# extract quantities
rail_1_thickness, rail_2_thickness = cfg.rail_thickness_range
rail_center = (0.5 * cfg.size[0], 0.5 * cfg.size[1], rail_height * 0.5)
# constants for terrain generation
terrain_height = 1.0
rail_2_ratio = 0.6
# generate first set of rails
rail_1_inner_size = (cfg.platform_width, cfg.platform_width)
rail_1_outer_size = (cfg.platform_width + 2.0 * rail_1_thickness, cfg.platform_width + 2.0 * rail_1_thickness)
meshes_list += make_border(rail_1_outer_size, rail_1_inner_size, rail_height, rail_center)
# generate second set of rails
rail_2_inner_x = cfg.platform_width + (cfg.size[0] - cfg.platform_width) * rail_2_ratio
rail_2_inner_y = cfg.platform_width + (cfg.size[1] - cfg.platform_width) * rail_2_ratio
rail_2_inner_size = (rail_2_inner_x, rail_2_inner_y)
rail_2_outer_size = (rail_2_inner_x + 2.0 * rail_2_thickness, rail_2_inner_y + 2.0 * rail_2_thickness)
meshes_list += make_border(rail_2_outer_size, rail_2_inner_size, rail_height, rail_center)
# generate the ground
dim = (cfg.size[0], cfg.size[1], terrain_height)
pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -terrain_height / 2)
ground_meshes = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos))
meshes_list.append(ground_meshes)
# specify the origin of the terrain
origin = np.array([pos[0], pos[1], 0.0])
return meshes_list, origin
[docs]def pit_terrain(
difficulty: float, cfg: mesh_terrains_cfg.MeshPitTerrainCfg
) -> tuple[list[trimesh.Trimesh], np.ndarray]:
"""Generate a terrain with a pit with levels (stairs) leading out of the pit.
The terrain contains a platform at the center and a staircase leading out of the pit.
The staircase is a series of steps that are aligned along the x- and y- axis. The steps are
created by extruding a ring along the x- and y- axis. If :obj:`is_double_pit` is True, the pit
contains two levels.
.. image:: ../../_static/terrains/trimesh/pit_terrain.jpg
:width: 40%
.. image:: ../../_static/terrains/trimesh/pit_terrain_with_two_levels.jpg
:width: 40%
Args:
difficulty: The difficulty of the terrain. This is a value between 0 and 1.
cfg: The configuration for the terrain.
Returns:
A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m).
"""
# resolve the terrain configuration
pit_depth = cfg.pit_depth_range[0] + difficulty * (cfg.pit_depth_range[1] - cfg.pit_depth_range[0])
# initialize list of meshes
meshes_list = list()
# extract quantities
inner_pit_size = (cfg.platform_width, cfg.platform_width)
total_depth = pit_depth
# constants for terrain generation
terrain_height = 1.0
ring_2_ratio = 0.6
# if the pit is double, the inner ring is smaller to fit the second level
if cfg.double_pit:
# increase the total height of the pit
total_depth *= 2.0
# reduce the size of the inner ring
inner_pit_x = cfg.platform_width + (cfg.size[0] - cfg.platform_width) * ring_2_ratio
inner_pit_y = cfg.platform_width + (cfg.size[1] - cfg.platform_width) * ring_2_ratio
inner_pit_size = (inner_pit_x, inner_pit_y)
# generate the pit (outer ring)
pit_center = [0.5 * cfg.size[0], 0.5 * cfg.size[1], -total_depth * 0.5]
meshes_list += make_border(cfg.size, inner_pit_size, total_depth, pit_center)
# generate the second level of the pit (inner ring)
if cfg.double_pit:
pit_center[2] = -total_depth
meshes_list += make_border(inner_pit_size, (cfg.platform_width, cfg.platform_width), total_depth, pit_center)
# generate the ground
dim = (cfg.size[0], cfg.size[1], terrain_height)
pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -total_depth - terrain_height / 2)
ground_meshes = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos))
meshes_list.append(ground_meshes)
# specify the origin of the terrain
origin = np.array([pos[0], pos[1], -total_depth])
return meshes_list, origin
[docs]def box_terrain(
difficulty: float, cfg: mesh_terrains_cfg.MeshBoxTerrainCfg
) -> tuple[list[trimesh.Trimesh], np.ndarray]:
"""Generate a terrain with boxes (similar to a pyramid).
The terrain has a ground with boxes on top of it that are stacked on top of each other.
The boxes are created by extruding a rectangle along the z-axis. If :obj:`double_box` is True,
then two boxes of height :obj:`box_height` are stacked on top of each other.
.. image:: ../../_static/terrains/trimesh/box_terrain.jpg
:width: 40%
.. image:: ../../_static/terrains/trimesh/box_terrain_with_two_boxes.jpg
:width: 40%
Args:
difficulty: The difficulty of the terrain. This is a value between 0 and 1.
cfg: The configuration for the terrain.
Returns:
A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m).
"""
# resolve the terrain configuration
box_height = cfg.box_height_range[0] + difficulty * (cfg.box_height_range[1] - cfg.box_height_range[0])
# initialize list of meshes
meshes_list = list()
# extract quantities
total_height = box_height
if cfg.double_box:
total_height *= 2.0
# constants for terrain generation
terrain_height = 1.0
box_2_ratio = 0.6
# Generate the top box
dim = (cfg.platform_width, cfg.platform_width, terrain_height + total_height)
pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], (total_height - terrain_height) / 2)
box_mesh = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos))
meshes_list.append(box_mesh)
# Generate the lower box
if cfg.double_box:
# calculate the size of the lower box
outer_box_x = cfg.platform_width + (cfg.size[0] - cfg.platform_width) * box_2_ratio
outer_box_y = cfg.platform_width + (cfg.size[1] - cfg.platform_width) * box_2_ratio
# create the lower box
dim = (outer_box_x, outer_box_y, terrain_height + total_height / 2)
pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], (total_height - terrain_height) / 2 - total_height / 4)
box_mesh = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos))
meshes_list.append(box_mesh)
# Generate the ground
pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -terrain_height / 2)
dim = (cfg.size[0], cfg.size[1], terrain_height)
ground_mesh = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos))
meshes_list.append(ground_mesh)
# specify the origin of the terrain
origin = np.array([pos[0], pos[1], total_height])
return meshes_list, origin
[docs]def gap_terrain(
difficulty: float, cfg: mesh_terrains_cfg.MeshGapTerrainCfg
) -> tuple[list[trimesh.Trimesh], np.ndarray]:
"""Generate a terrain with a gap around the platform.
The terrain has a ground with a platform in the middle. The platform is surrounded by a gap
of width :obj:`gap_width` on all sides.
.. image:: ../../_static/terrains/trimesh/gap_terrain.jpg
:width: 40%
:align: center
Args:
difficulty: The difficulty of the terrain. This is a value between 0 and 1.
cfg: The configuration for the terrain.
Returns:
A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m).
"""
# resolve the terrain configuration
gap_width = cfg.gap_width_range[0] + difficulty * (cfg.gap_width_range[1] - cfg.gap_width_range[0])
# initialize list of meshes
meshes_list = list()
# constants for terrain generation
terrain_height = 1.0
terrain_center = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -terrain_height / 2)
# Generate the outer ring
inner_size = (cfg.platform_width + 2 * gap_width, cfg.platform_width + 2 * gap_width)
meshes_list += make_border(cfg.size, inner_size, terrain_height, terrain_center)
# Generate the inner box
box_dim = (cfg.platform_width, cfg.platform_width, terrain_height)
box = trimesh.creation.box(box_dim, trimesh.transformations.translation_matrix(terrain_center))
meshes_list.append(box)
# specify the origin of the terrain
origin = np.array([terrain_center[0], terrain_center[1], 0.0])
return meshes_list, origin
[docs]def floating_ring_terrain(
difficulty: float, cfg: mesh_terrains_cfg.MeshFloatingRingTerrainCfg
) -> tuple[list[trimesh.Trimesh], np.ndarray]:
"""Generate a terrain with a floating square ring.
The terrain has a ground with a floating ring in the middle. The ring extends from the center from
:obj:`platform_width` to :obj:`platform_width` + :obj:`ring_width` in the x and y directions.
The thickness of the ring is :obj:`ring_thickness` and the height of the ring from the terrain
is :obj:`ring_height`.
.. image:: ../../_static/terrains/trimesh/floating_ring_terrain.jpg
:width: 40%
:align: center
Args:
difficulty: The difficulty of the terrain. This is a value between 0 and 1.
cfg: The configuration for the terrain.
Returns:
A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m).
"""
# resolve the terrain configuration
ring_height = cfg.ring_height_range[1] - difficulty * (cfg.ring_height_range[1] - cfg.ring_height_range[0])
ring_width = cfg.ring_width_range[0] + difficulty * (cfg.ring_width_range[1] - cfg.ring_width_range[0])
# initialize list of meshes
meshes_list = list()
# constants for terrain generation
terrain_height = 1.0
# Generate the floating ring
ring_center = (0.5 * cfg.size[0], 0.5 * cfg.size[1], ring_height + 0.5 * cfg.ring_thickness)
ring_outer_size = (cfg.platform_width + 2 * ring_width, cfg.platform_width + 2 * ring_width)
ring_inner_size = (cfg.platform_width, cfg.platform_width)
meshes_list += make_border(ring_outer_size, ring_inner_size, cfg.ring_thickness, ring_center)
# Generate the ground
dim = (cfg.size[0], cfg.size[1], terrain_height)
pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -terrain_height / 2)
ground = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos))
meshes_list.append(ground)
# specify the origin of the terrain
origin = np.asarray([pos[0], pos[1], 0.0])
return meshes_list, origin
[docs]def star_terrain(
difficulty: float, cfg: mesh_terrains_cfg.MeshStarTerrainCfg
) -> tuple[list[trimesh.Trimesh], np.ndarray]:
"""Generate a terrain with a star.
The terrain has a ground with a cylinder in the middle. The star is made of :obj:`num_bars` bars
with a width of :obj:`bar_width` and a height of :obj:`bar_height`. The bars are evenly
spaced around the cylinder and connect to the peripheral of the terrain.
.. image:: ../../_static/terrains/trimesh/star_terrain.jpg
:width: 40%
:align: center
Args:
difficulty: The difficulty of the terrain. This is a value between 0 and 1.
cfg: The configuration for the terrain.
Returns:
A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m).
Raises:
ValueError: If :obj:`num_bars` is less than 2.
"""
# check the number of bars
if cfg.num_bars < 2:
raise ValueError(f"The number of bars in the star must be greater than 2. Received: {cfg.num_bars}")
# resolve the terrain configuration
bar_height = cfg.bar_height_range[0] + difficulty * (cfg.bar_height_range[1] - cfg.bar_height_range[0])
bar_width = cfg.bar_width_range[1] - difficulty * (cfg.bar_width_range[1] - cfg.bar_width_range[0])
# initialize list of meshes
meshes_list = list()
# Generate a platform in the middle
platform_center = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -bar_height / 2)
platform_transform = trimesh.transformations.translation_matrix(platform_center)
platform = trimesh.creation.cylinder(
cfg.platform_width * 0.5, bar_height, sections=2 * cfg.num_bars, transform=platform_transform
)
meshes_list.append(platform)
# Generate bars to connect the platform to the terrain
transform = np.eye(4)
transform[:3, -1] = np.asarray(platform_center)
yaw = 0.0
for _ in range(cfg.num_bars):
# compute the length of the bar based on the yaw
# length changes since the bar is connected to a square border
bar_length = cfg.size[0]
if yaw < 0.25 * np.pi:
bar_length /= np.math.cos(yaw)
elif yaw < 0.75 * np.pi:
bar_length /= np.math.sin(yaw)
else:
bar_length /= np.math.cos(np.pi - yaw)
# compute the transform of the bar
transform[0:3, 0:3] = tf.Rotation.from_euler("z", yaw).as_matrix()
# add the bar to the mesh
dim = [bar_length - bar_width, bar_width, bar_height]
bar = trimesh.creation.box(dim, transform)
meshes_list.append(bar)
# increment the yaw
yaw += np.pi / cfg.num_bars
# Generate the exterior border
inner_size = (cfg.size[0] - 2 * bar_width, cfg.size[1] - 2 * bar_width)
meshes_list += make_border(cfg.size, inner_size, bar_height, platform_center)
# Generate the ground
ground = make_plane(cfg.size, -bar_height, center_zero=False)
meshes_list.append(ground)
# specify the origin of the terrain
origin = np.asarray([0.5 * cfg.size[0], 0.5 * cfg.size[1], 0.0])
return meshes_list, origin
[docs]def repeated_objects_terrain(
difficulty: float, cfg: mesh_terrains_cfg.MeshRepeatedObjectsTerrainCfg
) -> tuple[list[trimesh.Trimesh], np.ndarray]:
"""Generate a terrain with a set of repeated objects.
The terrain has a ground with a platform in the middle. The objects are randomly placed on the
terrain s.t. they do not overlap with the platform.
Depending on the object type, the objects are generated with different parameters. The objects
The types of objects that can be generated are: ``"cylinder"``, ``"box"``, ``"cone"``.
The object parameters are specified in the configuration as curriculum parameters. The difficulty
is used to linearly interpolate between the minimum and maximum values of the parameters.
.. image:: ../../_static/terrains/trimesh/repeated_objects_cylinder_terrain.jpg
:width: 30%
.. image:: ../../_static/terrains/trimesh/repeated_objects_box_terrain.jpg
:width: 30%
.. image:: ../../_static/terrains/trimesh/repeated_objects_pyramid_terrain.jpg
:width: 30%
Args:
difficulty: The difficulty of the terrain. This is a value between 0 and 1.
cfg: The configuration for the terrain.
Returns:
A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m).
Raises:
ValueError: If the object type is not supported. It must be either a string or a callable.
"""
# import the object functions -- this is done here to avoid circular imports
from .mesh_terrains_cfg import (
MeshRepeatedBoxesTerrainCfg,
MeshRepeatedCylindersTerrainCfg,
MeshRepeatedPyramidsTerrainCfg,
)
# if object type is a string, get the function: make_{object_type}
if isinstance(cfg.object_type, str):
object_func = globals().get(f"make_{cfg.object_type}")
else:
object_func = cfg.object_type
if not callable(object_func):
raise ValueError(f"The attribute 'object_type' must be a string or a callable. Received: {object_func}")
# Resolve the terrain configuration
# -- pass parameters to make calling simpler
cp_0 = cfg.object_params_start
cp_1 = cfg.object_params_end
# -- common parameters
num_objects = cp_0.num_objects + int(difficulty * (cp_1.num_objects - cp_0.num_objects))
height = cp_0.height + difficulty * (cp_1.height - cp_0.height)
# -- object specific parameters
# note: SIM114 requires duplicated logical blocks under a single body.
if isinstance(cfg, MeshRepeatedBoxesTerrainCfg):
cp_0: MeshRepeatedBoxesTerrainCfg.ObjectCfg
cp_1: MeshRepeatedBoxesTerrainCfg.ObjectCfg
object_kwargs = {
"length": cp_0.size[0] + difficulty * (cp_1.size[0] - cp_0.size[0]),
"width": cp_0.size[1] + difficulty * (cp_1.size[1] - cp_0.size[1]),
"max_yx_angle": cp_0.max_yx_angle + difficulty * (cp_1.max_yx_angle - cp_0.max_yx_angle),
"degrees": cp_0.degrees,
}
elif isinstance(cfg, MeshRepeatedPyramidsTerrainCfg): # noqa: SIM114
cp_0: MeshRepeatedPyramidsTerrainCfg.ObjectCfg
cp_1: MeshRepeatedPyramidsTerrainCfg.ObjectCfg
object_kwargs = {
"radius": cp_0.radius + difficulty * (cp_1.radius - cp_0.radius),
"max_yx_angle": cp_0.max_yx_angle + difficulty * (cp_1.max_yx_angle - cp_0.max_yx_angle),
"degrees": cp_0.degrees,
}
elif isinstance(cfg, MeshRepeatedCylindersTerrainCfg): # noqa: SIM114
cp_0: MeshRepeatedCylindersTerrainCfg.ObjectCfg
cp_1: MeshRepeatedCylindersTerrainCfg.ObjectCfg
object_kwargs = {
"radius": cp_0.radius + difficulty * (cp_1.radius - cp_0.radius),
"max_yx_angle": cp_0.max_yx_angle + difficulty * (cp_1.max_yx_angle - cp_0.max_yx_angle),
"degrees": cp_0.degrees,
}
else:
raise ValueError(f"Unknown terrain configuration: {cfg}")
# constants for the terrain
platform_clearance = 0.1
# initialize list of meshes
meshes_list = list()
# compute quantities
origin = np.asarray((0.5 * cfg.size[0], 0.5 * cfg.size[1], 0.5 * height))
platform_corners = np.asarray([
[origin[0] - cfg.platform_width / 2, origin[1] - cfg.platform_width / 2],
[origin[0] + cfg.platform_width / 2, origin[1] + cfg.platform_width / 2],
])
platform_corners[0, :] *= 1 - platform_clearance
platform_corners[1, :] *= 1 + platform_clearance
# sample center for objects
while True:
object_centers = np.zeros((num_objects, 3))
object_centers[:, 0] = np.random.uniform(0, cfg.size[0], num_objects)
object_centers[:, 1] = np.random.uniform(0, cfg.size[1], num_objects)
# filter out the centers that are on the platform
is_within_platform_x = np.logical_and(
object_centers[:, 0] >= platform_corners[0, 0], object_centers[:, 0] <= platform_corners[1, 0]
)
is_within_platform_y = np.logical_and(
object_centers[:, 1] >= platform_corners[0, 1], object_centers[:, 1] <= platform_corners[1, 1]
)
masks = np.logical_and(is_within_platform_x, is_within_platform_y)
# if there are no objects on the platform, break
if not np.any(masks):
break
# generate obstacles (but keep platform clean)
for index in range(len(object_centers)):
# randomize the height of the object
ob_height = height + np.random.uniform(-cfg.max_height_noise, cfg.max_height_noise)
if ob_height > 0.0:
object_mesh = object_func(center=object_centers[index], height=ob_height, **object_kwargs)
meshes_list.append(object_mesh)
# generate a ground plane for the terrain
ground_plane = make_plane(cfg.size, height=0.0, center_zero=False)
meshes_list.append(ground_plane)
# generate a platform in the middle
dim = (cfg.platform_width, cfg.platform_width, 0.5 * height)
pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], 0.25 * height)
platform = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos))
meshes_list.append(platform)
return meshes_list, origin