Commit a9de59fe authored by Mayank Mittal's avatar Mayank Mittal

Adds terrain type directly to the terrain importer class (#95)

# Description

* Changed the behavior of the
`omni.isaac.orbit.terrains.TerrainImporter` class. It now expects the
terrain type to be specified in the configuration object. This allows
the user to specify everything in the configuration object and not have
to do an explicit call to import a terrain.
* Fixed setting of quaternion orientations inside the
`omni.isaac.orbit.markers.Visualizationmarkers` class. Earlier, the
orientation was being set into the point instancer in the wrong order
(`wxyz` instead of `xyzw`).

## Type of change

- Bug fix (non-breaking change which fixes an issue)
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)

## Checklist

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./orbit.sh --format`
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
parent ca82e2ce
......@@ -35,6 +35,7 @@ extra_standard_library = [
"toml",
"trimesh",
"tqdm",
"typing_extensions",
]
# Imports from Isaac Sim and Omniverse
known_third_party = [
......
[package]
# Note: Semantic Versioning is used: https://semver.org/
version = "0.7.3"
version = "0.7.4"
# Description
title = "ORBIT framework for Robot Learning"
......
Changelog
---------
0.7.4 (2023-07-26)
~~~~~~~~~~~~~~~~~~
Changed
^^^^^^^
* Changed the behavior of the :class:`omni.isaac.orbit.terrains.TerrainImporter` class. It now expects the terrain
type to be specified in the configuration object. This allows the user to specify everything in the configuration
object and not have to do an explicit call to import a terrain.
Fixed
^^^^^
* Fixed setting of quaternion orientations inside the :class:`omni.isaac.orbit.markers.Visualizationmarkers` class.
Earlier, the orientation was being set into the point instancer in the wrong order (``wxyz`` instead of ``xyzw``).
0.7.3 (2023-07-25)
~~~~~~~~~~~~~~~~~~
......
......@@ -22,13 +22,13 @@ from dataclasses import MISSING
from typing import Any, Dict, List, Optional, Union
import omni.isaac.core.utils.prims as prim_utils
import omni.kit.commands
from omni.isaac.core.materials import PreviewSurface
from omni.isaac.core.prims import GeometryPrim
from pxr import Gf, UsdGeom, Vt
from omni.isaac.orbit.utils.assets import check_file_path
from omni.isaac.orbit.utils.configclass import configclass
from omni.isaac.orbit.utils.math import convert_quat
@configclass
......@@ -329,6 +329,9 @@ class VisualizationMarkers:
# check that shape is correct
if orientations.shape[1] != 4 or len(orientations.shape) != 2:
raise ValueError(f"Expected `orientations` to have shape (M, 4). Received: {orientations.shape}.")
# roll orientations from (w, x, y, z) to (x, y, z, w)
# internally USD expects (x, y, z, w)
orientations = convert_quat(orientations, to="xyzw")
# apply orientations
self._instancer_manager.GetOrientationsAttr().Set(Vt.QuathArray.FromNumpy(orientations))
# update number of markers
......@@ -406,9 +409,6 @@ class VisualizationMarkers:
scale=cfg.scale,
attributes=cfg.attributes,
)
# remove any physics parameters
omni.kit.commands.execute("RemovePhysicsComponentCommand", usd_prim=prim, component="PhysicsRigidBodyAPI")
omni.kit.commands.execute("RemovePhysicsComponentCommand", usd_prim=prim, component="PhysicsCollisionAPI")
# set visibility
prim_utils.set_prim_visibility(prim, visible=cfg.visible)
# create color attribute
......
......@@ -9,7 +9,7 @@ import omni.isaac.orbit.terrains as terrain_gen
from ..terrain_cfg import TerrainGeneratorCfg
ASSORTED_TERRAINS_CFG = TerrainGeneratorCfg(
ROUGH_TERRAINS_CFG = TerrainGeneratorCfg(
size=(8.0, 8.0),
border_width=20.0,
num_rows=10,
......@@ -50,3 +50,4 @@ ASSORTED_TERRAINS_CFG = TerrainGeneratorCfg(
),
},
)
"""Rough terrains configuration."""
......@@ -12,10 +12,13 @@ inherit from ``omni.isaac.orbit.terrains.terrains_cfg.TerrainConfig`` and define
and the configuration parameters and return a `tuple with the `trimesh`` mesh object and terrain origin.
"""
from __future__ import annotations
import numpy as np
import trimesh
from dataclasses import MISSING
from typing import Callable, Dict, List, Optional, Tuple
from typing import Callable
from typing_extensions import Literal
from omni.isaac.orbit.utils import configclass
......@@ -30,7 +33,7 @@ class SubTerrainBaseCfg:
extend from :math:`(0, 0)` to :math:`(size[0], size[1])`.
"""
function: Callable[[float, "SubTerrainBaseCfg"], Tuple[List[trimesh.Trimesh], np.ndarray]] = MISSING
function: Callable[[float, SubTerrainBaseCfg], tuple[list[trimesh.Trimesh], np.ndarray]] = MISSING
"""Function to generate the terrain.
This function must take as input the terrain difficulty and the configuration parameters and
......@@ -46,7 +49,7 @@ class SubTerrainBaseCfg:
is 0.7.
"""
size: Tuple[float, float] = MISSING
size: tuple[float, float] = MISSING
"""The width (along x) and length (along y) of the terrain (in m)."""
......@@ -54,13 +57,20 @@ class SubTerrainBaseCfg:
class TerrainGeneratorCfg:
"""Configuration for the terrain generator."""
seed: Optional[int] = None
seed: int | None = None
"""The seed for the random number generator. Defaults to :obj:`None`.
If :obj:`None`, the seed is not set.
"""
size: Tuple[float, float] = MISSING
curriculum: bool = False
"""Whether to use the curriculum mode. Defaults to False.
If True, the terrains are generated based on their difficulty parameter. Otherwise,
they are randomly generated.
"""
size: tuple[float, float] = MISSING
"""The width (along x) and length (along y) of each sub-terrain (in m).
Note:
......@@ -88,7 +98,7 @@ class TerrainGeneratorCfg:
This value is passed on to all the height field sub-terrain configurations.
"""
slope_threshold: Optional[float] = 0.75
slope_threshold: float | None = 0.75
"""The slope threshold above which surfaces are made vertical. Defaults to 0.75.
If :obj:`None` no correction is applied.
......@@ -96,10 +106,10 @@ class TerrainGeneratorCfg:
This value is passed on to all the height field sub-terrain configurations.
"""
sub_terrains: Dict[str, SubTerrainBaseCfg] = MISSING
sub_terrains: dict[str, SubTerrainBaseCfg] = MISSING
"""List of sub-terrain configurations."""
difficulty_choices: List[float] = [0.5, 0.75, 0.9]
difficulty_choices: list[float] = [0.5, 0.75, 0.9]
"""List of difficulty choices. Defaults to [0.5, 0.75, 0.9].
The difficulty choices are used to sample the difficulty of the generated terrain. The specified
......@@ -126,7 +136,25 @@ class TerrainImporterCfg:
All sub-terrains are imported relative to this prim path.
"""
color: Optional[Tuple[float, float, float]] = (0.065, 0.0725, 0.080)
terrain_type: Literal["generator", "plane", "usd"] = "generator"
"""The type of terrain to generate. Defaults to "generator".
Available options are "plane", "usd", and "generator".
"""
terrain_generator: TerrainGeneratorCfg | None = None
"""The terrain generator configuration.
Only used if ``terrain_type`` is set to "generator".
"""
usd_path: str | None = None
"""The path to the USD file containing the terrain.
Only used if ``terrain_type`` is set to "usd".
"""
color: tuple[float, float, float] | None = (0.065, 0.0725, 0.080)
"""The color of the terrain. Defaults to (0.065, 0.0725, 0.080).
If :obj:`None`, no color is applied to the prim.
......@@ -157,8 +185,8 @@ class TerrainImporterCfg:
This parameter is used only when no sub-terrain origins are defined.
"""
max_init_terrain_level: Optional[int] = 5
"""The maximum initial terrain level for defining environment origins. Defaults to 5.
max_init_terrain_level: int | None = None
"""The maximum initial terrain level for defining environment origins. Defaults to None.
The terrain levels are specified by the number of rows in the grid arrangement of
sub-terrains. If :obj:`None`, then the initial terrain level is set to the maximum
......
......@@ -11,6 +11,7 @@ from typing import List, Tuple
from omni.isaac.orbit.utils.dict import dict_to_md5_hash
from omni.isaac.orbit.utils.io import dump_yaml
from omni.isaac.orbit.utils.timer import Timer
from .height_field import HfTerrainBaseCfg
from .terrain_cfg import SubTerrainBaseCfg, TerrainGeneratorCfg
......@@ -51,20 +52,17 @@ class TerrainGenerator:
terrain_origins: np.ndarray
"""The origin of each sub-terrain. Shape is (num_rows, num_cols, 3)."""
def __init__(self, cfg: TerrainGeneratorCfg, curriculum: bool = False):
def __init__(self, cfg: TerrainGeneratorCfg):
"""Initialize the terrain generator.
Args:
cfg (TerrainMeshGeneratorCfg): Configuration for the terrain generator.
curriculum (bool, optional): If True, the terrains are generated based on their
difficulty parameter. Otherwise, they are randomly generated. Defaults to False.
"""
# check inputs
if len(cfg.sub_terrains) == 0:
raise ValueError("No sub-terrains specified! Please add at least one sub-terrain.")
# store inputs
self.cfg = cfg
self.enable_curriculum = curriculum
# set common values to all sub-terrains config
for sub_cfg in self.cfg.sub_terrains.values():
# size of all terrains
......@@ -84,10 +82,12 @@ class TerrainGenerator:
self.terrain_origins = np.zeros((self.cfg.num_rows, self.cfg.num_cols, 3))
# parse configuration and add sub-terrains
# create terrains based on curriculum or randomly
if self.enable_curriculum:
self._generate_curriculum_terrains()
if self.cfg.curriculum:
with Timer("[INFO] Generating terrains based on curriculum took"):
self._generate_curriculum_terrains()
else:
self._generate_random_terrains()
with Timer("[INFO] Generating terrains randomly took"):
self._generate_random_terrains()
# add a border around the terrains
self._add_terrain_border()
# combine all the sub-terrains into a single mesh
......
......@@ -13,11 +13,13 @@ import omni.isaac.core.utils.prims as prim_utils
import warp
from pxr import UsdGeom
from omni.isaac.orbit.compat.markers import StaticMarker
from omni.isaac.orbit.markers import VisualizationMarkers
from omni.isaac.orbit.markers.config import FRAME_MARKER_CFG
from omni.isaac.orbit.utils.kit import create_ground_plane
from omni.isaac.orbit.utils.warp import convert_to_warp_mesh
from .terrain_cfg import TerrainImporterCfg
from .terrain_generator import TerrainGenerator
from .trimesh.utils import make_plane
from .utils import create_prim_from_mesh
......@@ -51,12 +53,18 @@ class TerrainImporter:
env_origins: torch.Tensor
"""The origins of the environment instances. Shape is (num_envs, 3)."""
def __init__(self, cfg: TerrainImporterCfg, device: str = "cuda"):
def __init__(self, cfg: TerrainImporterCfg, num_envs: int, device: str):
"""Initialize the terrain importer.
Args:
cfg (TerrainImporterCfg): The configuration for the terrain importer.
device (str, optional): The device to use. Defaults to "cuda".
num_envs (int): The number of environment origins to configure.
device (str, optional): The device to use.
Raises:
ValueError: If input terrain type is not supported.
ValueError: If terrain type is 'generator' and no configuration provided for ``terrain_generator``.
ValueError: If terrain type is 'usd' and no configuration provided for ``usd_path``.
"""
# store inputs
self.cfg = cfg
......@@ -65,7 +73,34 @@ class TerrainImporter:
# create a dict of meshes
self.meshes = dict()
self.warp_meshes = dict()
self.origins = None
self.env_origins = None
# marker for visualization
self.origin_visualizer = None
if self.cfg.terrain_type == "generator":
# check config is provided
if self.cfg.terrain_generator is None:
raise ValueError("Input terrain type is 'generator' but no value provided for 'terrain_generator'.")
# generate the terrain
terrain_generator = TerrainGenerator(cfg=self.cfg.terrain_generator)
self.import_mesh(terrain_generator.terrain_mesh, key="terrain")
# configure the terrain origins based on the terrain generator
self.configure_env_origins(num_envs, terrain_generator.terrain_origins)
elif self.cfg.terrain_type == "usd":
# check if config is provided
if self.cfg.usd_path is None:
raise ValueError("Input terrain type is 'usd' but no value provided for 'usd_path'.")
# import the terrain
self.import_usd(self.cfg.usd_path, key="terrain")
# configure the origins in a grid
self.configure_env_origins(num_envs)
elif self.cfg.terrain_type == "plane":
# load the plane
self.import_ground_plane(key="terrain")
# configure the origins in a grid
self.configure_env_origins(num_envs)
else:
raise ValueError(f"Terrain type '{self.cfg.terrain_type}' not available.")
def import_ground_plane(self, size: Tuple[int, int] = (2.0e6, 2.0e6), key: str = "terrain", **kwargs):
"""Add a plane to the terrain importer.
......@@ -78,7 +113,7 @@ class TerrainImporter:
ValueError: If a terrain with the same key already exists.
"""
# create a plane
mesh = make_plane(size, height=0.0, centered=True)
mesh = make_plane(size, height=0.0, center_zero=True)
# store the mesh
self.meshes[key] = mesh
# create a warp mesh
......@@ -187,6 +222,8 @@ class TerrainImporter:
num_envs (int): The number of environment origins to define.
origins (Optional[np.ndarray]): The origins of the sub-terrains. Shape: (num_rows, num_cols, 3).
"""
# create markers for the origins
markers = VisualizationMarkers(f"{self.cfg.prim_path}/originMarkers", cfg=FRAME_MARKER_CFG)
# decide whether to compute origins in a grid or based on curriculum
if origins is not None:
# convert to numpy
......@@ -196,14 +233,14 @@ class TerrainImporter:
self.terrain_origins = origins.to(self.device, dtype=torch.float)
# compute environment origins
self.env_origins = self._compute_env_origins_curriculum(num_envs, self.terrain_origins)
# create markers for terrain origins
num_rows, num_cols = self.terrain_origins.shape[:2]
markers = StaticMarker(f"{self.cfg.prim_path}/originMarkers", count=num_rows * num_cols, scale=[0.5] * 3)
markers.set_world_poses(self.terrain_origins.reshape(-1, 3))
# put markers on the sub-terrain origins
markers.visualize(self.terrain_origins.reshape(-1, 3))
else:
self.terrain_origins = None
# compute environment origins
self.env_origins = self._compute_env_origins_grid(num_envs, self.cfg.env_spacing)
# put markers on the grid origins
markers.visualize(self.env_origins.reshape(-1, 3))
def update_env_origins(self, env_ids: torch.Tensor, move_up: torch.Tensor, move_down: torch.Tensor):
"""Update the environment origins based on the terrain levels."""
......
......@@ -8,7 +8,7 @@ import argparse
import os
import shutil
from omni.isaac.orbit.terrains.config.rough import ASSORTED_TERRAINS_CFG
from omni.isaac.orbit.terrains.config.rough import ROUGH_TERRAINS_CFG
from omni.isaac.orbit.terrains.terrain_generator import TerrainGenerator
if __name__ == "__main__":
......@@ -21,7 +21,8 @@ if __name__ == "__main__":
# create directory
os.makedirs(output_dir, exist_ok=True)
# modify the config to cache
ASSORTED_TERRAINS_CFG.use_cache = True
ASSORTED_TERRAINS_CFG.cache_dir = output_dir
ROUGH_TERRAINS_CFG.use_cache = True
ROUGH_TERRAINS_CFG.cache_dir = output_dir
ROUGH_TERRAINS_CFG.curriculum = False
# generate terrains
terrain_generator = TerrainGenerator(cfg=ASSORTED_TERRAINS_CFG, curriculum=False)
terrain_generator = TerrainGenerator(cfg=ROUGH_TERRAINS_CFG)
......@@ -39,7 +39,7 @@ parser.add_argument("--geom_sphere", action="store_true", default=False, help="W
parser.add_argument(
"--terrain_type",
type=str,
default="generated",
default="generator",
help="Type of terrain to import. Can be 'generated' or 'usd' or 'plane'.",
)
args_cli = parser.parse_args()
......@@ -64,7 +64,7 @@ from omni.isaac.core.simulation_context import SimulationContext
from omni.isaac.core.utils.viewports import set_camera_view
import omni.isaac.orbit.terrains as terrain_gen
from omni.isaac.orbit.terrains.config.rough import ASSORTED_TERRAINS_CFG
from omni.isaac.orbit.terrains.config.rough import ROUGH_TERRAINS_CFG
from omni.isaac.orbit.terrains.terrain_importer import TerrainImporter
from omni.isaac.orbit.utils.assets import ISAAC_NUCLEUS_DIR
......@@ -95,22 +95,14 @@ def main(terrain_type: str):
prim_utils.define_prim("/World/envs/env_0")
# Handler for terrains importing
terrain_importer_cfg = terrain_gen.TerrainImporterCfg(prim_path="/World/ground", max_init_terrain_level=None)
terrain_importer = TerrainImporter(terrain_importer_cfg, device=sim.device)
# Ground-plane
if terrain_type == "generated":
terrain_generator = terrain_gen.TerrainGenerator(cfg=ASSORTED_TERRAINS_CFG, curriculum=True)
terrain_importer.import_mesh(terrain_generator.terrain_mesh, key="rough")
terrain_importer.configure_env_origins(num_balls, terrain_generator.terrain_origins)
elif terrain_type == "usd":
terrain_importer.import_usd(f"{ISAAC_NUCLEUS_DIR}/Environments/Terrains/rough_plane.usd", key="usd")
terrain_importer.configure_env_origins(num_balls)
elif terrain_type == "plane":
terrain_importer.import_ground_plane(key="flat")
terrain_importer.configure_env_origins(num_balls)
else:
raise ValueError(f"Unknown terrain type: {terrain_type}. Valid options are: generated, usd, plane.")
terrain_importer_cfg = terrain_gen.TerrainImporterCfg(
prim_path="/World/ground",
max_init_terrain_level=None,
terrain_type=terrain_type,
terrain_generator=ROUGH_TERRAINS_CFG.replace(curriculum=True),
usd_path=f"{ISAAC_NUCLEUS_DIR}/Environments/Terrains/rough_plane.usd",
)
terrain_importer = TerrainImporter(terrain_importer_cfg, num_envs=num_balls, device=sim.device)
# Define the scene
# -- Light
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment