diff --git a/docs/source/_static/demos/multi_asset.jpg b/docs/source/_static/demos/multi_asset.jpg new file mode 100644 index 0000000000..c7124d20e6 Binary files /dev/null and b/docs/source/_static/demos/multi_asset.jpg differ diff --git a/docs/source/api/lab/omni.isaac.lab.sim.spawners.rst b/docs/source/api/lab/omni.isaac.lab.sim.spawners.rst index 6e3ca9aa9e..a1c073d4c2 100644 --- a/docs/source/api/lab/omni.isaac.lab.sim.spawners.rst +++ b/docs/source/api/lab/omni.isaac.lab.sim.spawners.rst @@ -13,6 +13,7 @@ sensors from_files materials + wrappers .. rubric:: Classes @@ -302,3 +303,27 @@ Physical Materials .. autoclass:: DeformableBodyMaterialCfg :members: :exclude-members: __init__, func + +Wrappers +-------- + +.. automodule:: omni.isaac.lab.sim.spawners.wrappers + + .. rubric:: Classes + + .. autosummary:: + + MultiAssetSpawnerCfg + MultiUsdFileCfg + +.. autofunction:: spawn_multi_asset + +.. autoclass:: MultiAssetSpawnerCfg + :members: + :exclude-members: __init__, func + +.. autofunction:: spawn_multi_usd_file + +.. autoclass:: MultiUsdFileCfg + :members: + :exclude-members: __init__, func diff --git a/docs/source/how-to/index.rst b/docs/source/how-to/index.rst index 893b3a69b3..4b5c426d82 100644 --- a/docs/source/how-to/index.rst +++ b/docs/source/how-to/index.rst @@ -36,6 +36,17 @@ a fixed base robot. This guide goes over the various considerations and steps to make_fixed_prim +Spawning Multiple Assets +------------------------ + +This guide explains how to import and configure different assets in each environment. This is +useful when you want to create diverse environments with different objects. + +.. toctree:: + :maxdepth: 1 + + multi_asset_spawning + Saving Camera Output -------------------- diff --git a/docs/source/how-to/multi_asset_spawning.rst b/docs/source/how-to/multi_asset_spawning.rst new file mode 100644 index 0000000000..9f74e39f6b --- /dev/null +++ b/docs/source/how-to/multi_asset_spawning.rst @@ -0,0 +1,101 @@ +Spawning Multiple Assets +======================== + +.. currentmodule:: omni.isaac.lab + +Typical, spawning configurations (introduced in the :ref:`tutorial-spawn-prims` tutorial) copy the same +asset (or USD primitive) across the different resolved prim paths from the expressions. +For instance, if the user specifies to spawn the asset at "/World/Table\_.*/Object", the same +asset is created at the paths "/World/Table_0/Object", "/World/Table_1/Object" and so on. + +However, at times, it might be desirable to spawn different assets under the prim paths to +ensure a diversity in the simulation. This guide describes how to create different assets under +each prim path using the spawning functionality. + +The sample script ``multi_asset.py`` is used as a reference, located in the +``IsaacLab/source/standalone/demos`` directory. + +.. dropdown:: Code for multi_asset.py + :icon: code + + .. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :emphasize-lines: 101-123, 130-149 + :linenos: + +This script creates multiple environments, where each environment has a rigid object that is either a cone, +a cube, or a sphere, and an articulation that is either the ANYmal-C or ANYmal-D robot. + +.. image:: ../_static/demos/multi_asset.jpg + :width: 100% + :alt: result of multi_asset.py + +Using Multi-Asset Spawning Functions +------------------------------------ + +It is possible to spawn different assets and USDs in each environment using the spawners +:class:`~sim.spawners.wrappers.MultiAssetSpawnerCfg` and :class:`~sim.spawners.wrappers.MultiUsdFileCfg`: + +* We set the spawn configuration in :class:`~assets.RigidObjectCfg` to be + :class:`~sim.spawners.wrappers.MultiAssetSpawnerCfg`: + + .. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :lines: 99-125 + :dedent: + + This function allows you to define a list of different assets that can be spawned as rigid objects. + When :attr:`~sim.spawners.wrappers.MultiAssetSpawnerCfg.random_choice` is set to True, one asset from the list + is randomly selected and spawned at the specified prim path. + +* Similarly, we set the spawn configuration in :class:`~assets.ArticulationCfg` to be + :class:`~sim.spawners.wrappers.MultiUsdFileCfg`: + + .. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :lines: 128-161 + :dedent: + + Similar to before, this configuration allows the selection of different USD files representing articulated assets. + + +Things to Note +-------------- + +Similar asset structuring +~~~~~~~~~~~~~~~~~~~~~~~~~ + +While spawning and handling multiple assets using the same physics interface (the rigid object or articulation classes), +it is essential to have the assets at all the prim locations follow a similar structure. In case of an articulation, +this means that they all must have the same number of links and joints, the same number of collision bodies and +the same names for them. If that is not the case, the physics parsing of the prims can get affected and fail. + +The main purpose of this functionality is to enable the user to create randomized versions of the same asset, +for example robots with different link lengths, or rigid objects with different collider shapes. + +Disabling physics replication in interactive scene +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the flag :attr:`scene.InteractiveScene.replicate_physics` is set to True. This flag informs the physics +engine that the simulation environments are copies of one another so it just needs to parse the first environment +to understand the entire simulation scene. This helps speed up the simulation scene parsing. + +However, in the case of spawning different assets in different environments, this assumption does not hold +anymore. Hence the flag :attr:`scene.InteractiveScene.replicate_physics` must be disabled. + +.. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :lines: 221-224 + :dedent: + +The Code Execution +------------------ + +To execute the script with multiple environments and randomized assets, use the following command: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/demos/multi_asset.py --num_envs 2048 + +This command runs the simulation with 2048 environments, each with randomly selected assets. +To stop the simulation, you can close the window, or press ``Ctrl+C`` in the terminal. diff --git a/docs/source/overview/showroom.rst b/docs/source/overview/showroom.rst index d8ff7a933d..d3d86fd777 100644 --- a/docs/source/overview/showroom.rst +++ b/docs/source/overview/showroom.rst @@ -77,7 +77,7 @@ A few quick showroom scripts to run and checkout: :width: 100% :alt: Dexterous hands in Isaac Lab -- Spawn procedurally generated terrains with different configurations: +- Spawn different deformable (soft) bodies and let them fall from a height: .. tab-set:: :sync-group: os @@ -87,20 +87,20 @@ A few quick showroom scripts to run and checkout: .. code:: bash - ./isaaclab.sh -p source/standalone/demos/procedural_terrain.py + ./isaaclab.sh -p source/standalone/demos/deformables.py .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows .. code:: batch - isaaclab.bat -p source\standalone\demos\procedural_terrain.py + isaaclab.bat -p source\standalone\demos\deformables.py - .. image:: ../_static/demos/procedural_terrain.jpg + .. image:: ../_static/demos/deformables.jpg :width: 100% - :alt: Procedural Terrains in Isaac Lab + :alt: Deformable primitive-shaped objects in Isaac Lab -- Spawn different deformable (soft) bodies and let them fall from a height: +- Use the interactive scene and spawn varying assets in individual environments: .. tab-set:: :sync-group: os @@ -110,20 +110,43 @@ A few quick showroom scripts to run and checkout: .. code:: bash - ./isaaclab.sh -p source/standalone/demos/deformables.py + ./isaaclab.sh -p source/standalone/demos/multi_asset.py .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows .. code:: batch - isaaclab.bat -p source\standalone\demos\deformables.py + isaaclab.bat -p source\standalone\demos\multi_asset.py - .. image:: ../_static/demos/deformables.jpg + .. image:: ../_static/demos/multi_asset.jpg :width: 100% - :alt: Deformable primitive-shaped objects in Isaac Lab + :alt: Multiple assets managed through the same simulation handles + +- Create and spawn procedurally generated terrains with different configurations: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/demos/procedural_terrain.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\demos\procedural_terrain.py + + .. image:: ../_static/demos/procedural_terrain.jpg + :width: 100% + :alt: Procedural Terrains in Isaac Lab -- Spawn multiple markers that are useful for visualizations: +- Define multiple markers that are useful for visualizations: .. tab-set:: :sync-group: os diff --git a/source/extensions/omni.isaac.lab/config/extension.toml b/source/extensions/omni.isaac.lab/config/extension.toml index b8b9a8c0ef..7e9225c2b3 100644 --- a/source/extensions/omni.isaac.lab/config/extension.toml +++ b/source/extensions/omni.isaac.lab/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.24.20" +version = "0.25.0" # Description title = "Isaac Lab framework for Robot Learning" diff --git a/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst b/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst index 9437a7aaf2..b0fa66f629 100644 --- a/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst +++ b/source/extensions/omni.isaac.lab/docs/CHANGELOG.rst @@ -1,6 +1,16 @@ Changelog --------- +0.25.0 (2024-10-06) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added configuration classes for spawning assets from a list of individual asset configurations randomly + at the specified prim paths. + + 0.24.20 (2024-10-07) ~~~~~~~~~~~~~~~~~~~~ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/scene/interactive_scene.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/scene/interactive_scene.py index c803f0e305..8c4b81aaff 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/scene/interactive_scene.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/scene/interactive_scene.py @@ -166,6 +166,18 @@ def clone_environments(self, copy_from_source: bool = False): If True, clones are independent copies of the source prim and won't reflect its changes (start-up time may increase). Defaults to False. """ + # check if user spawned different assets in individual environments + # this flag will be None if no multi asset is spawned + carb_settings_iface = carb.settings.get_settings() + has_multi_assets = carb_settings_iface.get("/isaaclab/spawn/multi_assets") + if has_multi_assets and self.cfg.replicate_physics: + carb.log_warn( + "Varying assets might have been spawned under different environments." + " However, the replicate physics flag is enabled in the 'InteractiveScene' configuration." + " This may adversely affect PhysX parsing. We recommend disabling this property." + ) + + # clone the environment env_origins = self.cloner.clone( source_prim_path=self.env_prim_paths[0], prim_paths=self.env_prim_paths, @@ -187,9 +199,6 @@ def filter_collisions(self, global_prim_paths: list[str] | None = None): global_prim_paths: A list of global prim paths to enable collisions with. Defaults to None, in which case no global prim paths are considered. """ - # obtain the current physics scene - physics_scene_prim_path = self.physics_scene_path - # validate paths in global prim paths if global_prim_paths is None: global_prim_paths = [] @@ -203,7 +212,7 @@ def filter_collisions(self, global_prim_paths: list[str] | None = None): # filter collisions within each environment instance self.cloner.filter_collisions( - physics_scene_prim_path, + self.physics_scene_path, "/World/collisions", self.env_prim_paths, global_paths=self._global_prim_paths, @@ -224,14 +233,16 @@ def __str__(self) -> str: """ @property - def physics_scene_path(self): - """Search the stage for the physics scene""" + def physics_scene_path(self) -> str: + """The path to the USD Physics Scene.""" if self._physics_scene_path is None: for prim in self.stage.Traverse(): if prim.HasAPI(PhysxSchema.PhysxSceneAPI): - self._physics_scene_path = prim.GetPrimPath() + self._physics_scene_path = prim.GetPrimPath().pathString carb.log_info(f"Physics scene prim path: {self._physics_scene_path}") break + if self._physics_scene_path is None: + raise RuntimeError("No physics scene found! Please make sure one exists.") return self._physics_scene_path @property diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/__init__.py index 851750f371..94b1245ab6 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/__init__.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/__init__.py @@ -61,3 +61,4 @@ class and the function call in a single line of code. from .sensors import * # noqa: F401, F403 from .shapes import * # noqa: F401, F403 from .spawner_cfg import * # noqa: F401, F403 +from .wrappers import * # noqa: F401, F403 diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/spawner_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/spawner_cfg.py index 089b38b29a..351b3cde96 100644 --- a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/spawner_cfg.py +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/spawner_cfg.py @@ -64,11 +64,6 @@ class SpawnerCfg: This parameter is only used when cloning prims. If False, then the asset will be inherited from the source prim, i.e. all USD changes to the source prim will be reflected in the cloned prims. - - .. versionadded:: 2023.1 - - This parameter is only supported from Isaac Sim 2023.1 onwards. If you are using an older - version of Isaac Sim, this parameter will be ignored. """ diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/__init__.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/__init__.py new file mode 100644 index 0000000000..f05d3e58c7 --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Sub-module for wrapping spawner configurations. + +Unlike the other spawner modules, this module provides a way to wrap multiple spawner configurations +into a single configuration. This is useful when the user wants to spawn multiple assets based on +different configurations. +""" + +from .wrappers import spawn_multi_asset, spawn_multi_usd_file +from .wrappers_cfg import MultiAssetSpawnerCfg, MultiUsdFileCfg diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/wrappers.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/wrappers.py new file mode 100644 index 0000000000..9040569e4a --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/wrappers.py @@ -0,0 +1,169 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import random +import re +from typing import TYPE_CHECKING + +import carb +import omni.isaac.core.utils.prims as prim_utils +import omni.isaac.core.utils.stage as stage_utils +from pxr import Sdf, Usd + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.sim.spawners.from_files import UsdFileCfg + +if TYPE_CHECKING: + from . import wrappers_cfg + + +def spawn_multi_asset( + prim_path: str, + cfg: wrappers_cfg.MultiAssetSpawnerCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Spawn multiple assets based on the provided configurations. + + This function spawns multiple assets based on the provided configurations. The assets are spawned + in the order they are provided in the list. If the :attr:`~MultiAssetSpawnerCfg.random_choice` parameter is + set to True, a random asset configuration is selected for each spawn. + + Args: + prim_path: The prim path to spawn the assets. + cfg: The configuration for spawning the assets. + translation: The translation of the spawned assets. Default is None. + orientation: The orientation of the spawned assets in (w, x, y, z) order. Default is None. + + Returns: + The created prim at the first prim path. + """ + # resolve: {SPAWN_NS}/AssetName + # note: this assumes that the spawn namespace already exists in the stage + root_path, asset_path = prim_path.rsplit("/", 1) + # check if input is a regex expression + # note: a valid prim path can only contain alphanumeric characters, underscores, and forward slashes + is_regex_expression = re.match(r"^[a-zA-Z0-9/_]+$", root_path) is None + + # resolve matching prims for source prim path expression + if is_regex_expression and root_path != "": + source_prim_paths = sim_utils.find_matching_prim_paths(root_path) + # if no matching prims are found, raise an error + if len(source_prim_paths) == 0: + raise RuntimeError( + f"Unable to find source prim path: '{root_path}'. Please create the prim before spawning." + ) + else: + source_prim_paths = [root_path] + + # find a free prim path to hold all the template prims + template_prim_path = stage_utils.get_next_free_path("/World/Template") + prim_utils.create_prim(template_prim_path, "Scope") + + # spawn everything first in a "Dataset" prim + proto_prim_paths = list() + for index, asset_cfg in enumerate(cfg.assets_cfg): + # append semantic tags if specified + if cfg.semantic_tags is not None: + if asset_cfg.semantic_tags is None: + asset_cfg.semantic_tags = cfg.semantic_tags + else: + asset_cfg.semantic_tags += cfg.semantic_tags + # override settings for properties + attr_names = ["mass_props", "rigid_props", "collision_props", "activate_contact_sensors", "deformable_props"] + for attr_name in attr_names: + attr_value = getattr(cfg, attr_name) + if hasattr(asset_cfg, attr_name) and attr_value is not None: + setattr(asset_cfg, attr_name, attr_value) + # spawn single instance + proto_prim_path = f"{template_prim_path}/Asset_{index:04d}" + asset_cfg.func(proto_prim_path, asset_cfg, translation=translation, orientation=orientation) + # append to proto prim paths + proto_prim_paths.append(proto_prim_path) + + # resolve prim paths for spawning and cloning + prim_paths = [f"{source_prim_path}/{asset_path}" for source_prim_path in source_prim_paths] + + # acquire stage + stage = stage_utils.get_current_stage() + + # manually clone prims if the source prim path is a regex expression + # note: unlike in the cloner API from Isaac Sim, we do not "reset" xforms on the copied prims. + # This is because the "spawn" calls during the creation of the proto prims already handles this operation. + with Sdf.ChangeBlock(): + for index, prim_path in enumerate(prim_paths): + # spawn single instance + env_spec = Sdf.CreatePrimInLayer(stage.GetRootLayer(), prim_path) + # randomly select an asset configuration + if cfg.random_choice: + proto_path = random.choice(proto_prim_paths) + else: + proto_path = proto_prim_paths[index % len(proto_prim_paths)] + # copy the proto prim + Sdf.CopySpec(env_spec.layer, Sdf.Path(proto_path), env_spec.layer, Sdf.Path(prim_path)) + + # delete the dataset prim after spawning + prim_utils.delete_prim(template_prim_path) + + # set carb setting to indicate Isaac Lab's environments that different prims have been spawned + # at varying prim paths. In this case, PhysX parser shouldn't optimize the stage parsing. + # the flag is mainly used to inform the user that they should disable `InteractiveScene.replicate_physics` + carb_settings_iface = carb.settings.get_settings() + carb_settings_iface.set_bool("/isaaclab/spawn/multi_assets", True) + + # return the prim + return prim_utils.get_prim_at_path(prim_paths[0]) + + +def spawn_multi_usd_file( + prim_path: str, + cfg: wrappers_cfg.MultiUsdFileCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Spawn multiple USD files based on the provided configurations. + + This function creates configuration instances corresponding the individual USD files and + calls the :meth:`spawn_multi_asset` method to spawn them into the scene. + + Args: + prim_path: The prim path to spawn the assets. + cfg: The configuration for spawning the assets. + translation: The translation of the spawned assets. Default is None. + orientation: The orientation of the spawned assets in (w, x, y, z) order. Default is None. + + Returns: + The created prim at the first prim path. + """ + # needed here to avoid circular imports + from .wrappers_cfg import MultiAssetSpawnerCfg + + # parse all the usd files + if isinstance(cfg.usd_path, str): + usd_paths = [cfg.usd_path] + else: + usd_paths = cfg.usd_path + + # make a template usd config + usd_template_cfg = UsdFileCfg() + for attr_name, attr_value in cfg.__dict__.items(): + # skip names we know are not present + if attr_name in ["func", "usd_path", "random_choice"]: + continue + # set the attribute into the template + setattr(usd_template_cfg, attr_name, attr_value) + + # create multi asset configuration of USD files + multi_asset_cfg = MultiAssetSpawnerCfg(assets_cfg=[]) + for usd_path in usd_paths: + usd_cfg = usd_template_cfg.replace(usd_path=usd_path) + multi_asset_cfg.assets_cfg.append(usd_cfg) + # set random choice + multi_asset_cfg.random_choice = cfg.random_choice + + # call the original function + return spawn_multi_asset(prim_path, multi_asset_cfg, translation, orientation) diff --git a/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/wrappers_cfg.py b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/wrappers_cfg.py new file mode 100644 index 0000000000..83d42cc4af --- /dev/null +++ b/source/extensions/omni.isaac.lab/omni/isaac/lab/sim/spawners/wrappers/wrappers_cfg.py @@ -0,0 +1,67 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING + +from omni.isaac.lab.sim.spawners.from_files import UsdFileCfg +from omni.isaac.lab.sim.spawners.spawner_cfg import DeformableObjectSpawnerCfg, RigidObjectSpawnerCfg, SpawnerCfg +from omni.isaac.lab.utils import configclass + +from . import wrappers + + +@configclass +class MultiAssetSpawnerCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg): + """Configuration parameters for loading multiple assets from their individual configurations. + + Specifying values for any properties at the configuration level will override the settings of + individual assets' configuration. For instance if the attribute + :attr:`MultiAssetSpawnerCfg.mass_props` is specified, its value will overwrite the values of the + mass properties in each configuration inside :attr:`assets_cfg` (wherever applicable). + This is done to simplify configuring similar properties globally. By default, all properties are set to None. + + The following is an exception to the above: + + * :attr:`visible`: This parameter is ignored. Its value for the individual assets is used. + * :attr:`semantic_tags`: If specified, it will be appended to each individual asset's semantic tags. + + """ + + func = wrappers.spawn_multi_asset + + assets_cfg: list[SpawnerCfg] = MISSING + """List of asset configurations to spawn.""" + + random_choice: bool = True + """Whether to randomly select an asset configuration. Default is True. + + If False, the asset configurations are spawned in the order they are provided in the list. + If True, a random asset configuration is selected for each spawn. + """ + + +@configclass +class MultiUsdFileCfg(UsdFileCfg): + """Configuration parameters for loading multiple USD files. + + Specifying values for any properties at the configuration level is applied to all the assets + imported from their USD files. + + .. tip:: + It is recommended that all the USD based assets follow a similar prim-hierarchy. + + """ + + func = wrappers.spawn_multi_usd_file + + usd_path: str | list[str] = MISSING + """Path or a list of paths to the USD files to spawn asset from.""" + + random_choice: bool = True + """Whether to randomly select an asset configuration. Default is True. + + If False, the asset configurations are spawned in the order they are provided in the list. + If True, a random asset configuration is selected for each spawn. + """ diff --git a/source/extensions/omni.isaac.lab/test/sim/test_spawn_wrappers.py b/source/extensions/omni.isaac.lab/test/sim/test_spawn_wrappers.py new file mode 100644 index 0000000000..1260facf58 --- /dev/null +++ b/source/extensions/omni.isaac.lab/test/sim/test_spawn_wrappers.py @@ -0,0 +1,191 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Launch Isaac Sim Simulator first.""" + +from omni.isaac.lab.app import AppLauncher, run_tests + +# launch omniverse app +simulation_app = AppLauncher(headless=True).app + +"""Rest everything follows.""" + +import unittest + +import omni.isaac.core.utils.prims as prim_utils +import omni.isaac.core.utils.stage as stage_utils +from omni.isaac.core.simulation_context import SimulationContext + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.utils.assets import ISAACLAB_NUCLEUS_DIR + + +class TestSpawningWrappers(unittest.TestCase): + """Test fixture for checking spawning of multiple assets wrappers.""" + + def setUp(self) -> None: + """Create a blank new stage for each test.""" + # Create a new stage + stage_utils.create_new_stage() + # Simulation time-step + self.dt = 0.1 + # Load kit helper + self.sim = SimulationContext(physics_dt=self.dt, rendering_dt=self.dt, backend="numpy") + # Wait for spawning + stage_utils.update_stage() + + def tearDown(self) -> None: + """Stops simulator after each test.""" + # stop simulation + self.sim.stop() + self.sim.clear() + self.sim.clear_all_callbacks() + self.sim.clear_instance() + + """ + Tests - Multiple assets. + """ + + def test_spawn_multiple_shapes_with_global_settings(self): + """Test spawning of shapes randomly with global rigid body settings.""" + # Define prim parents + num_clones = 10 + for i in range(num_clones): + prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + + # Spawn shapes + cfg = sim_utils.MultiAssetSpawnerCfg( + assets_cfg=[ + sim_utils.ConeCfg( + radius=0.3, + height=0.6, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2), + mass_props=sim_utils.MassPropertiesCfg(mass=100.0), # this one should get overridden + ), + sim_utils.CuboidCfg( + size=(0.3, 0.3, 0.3), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2), + ), + sim_utils.SphereCfg( + radius=0.3, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0), metallic=0.2), + ), + ], + random_choice=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, solver_velocity_iteration_count=0 + ), + mass_props=sim_utils.MassPropertiesCfg(mass=1.0), + collision_props=sim_utils.CollisionPropertiesCfg(), + ) + prim = cfg.func("/World/env_.*/Cone", cfg) + + # Check validity + self.assertTrue(prim.IsValid()) + self.assertEqual(prim_utils.get_prim_path(prim), "/World/env_0/Cone") + # Find matching prims + prim_paths = prim_utils.find_matching_prim_paths("/World/env_*/Cone") + self.assertEqual(len(prim_paths), num_clones) + + # Check all prims have correct settings + for prim_path in prim_paths: + prim = prim_utils.get_prim_at_path(prim_path) + self.assertEqual(prim.GetAttribute("physics:mass").Get(), cfg.mass_props.mass) + + def test_spawn_multiple_shapes_with_individual_settings(self): + """Test spawning of shapes randomly with individual rigid object settings""" + # Define prim parents + num_clones = 10 + for i in range(num_clones): + prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + + # Make a list of masses + mass_variations = [2.0, 3.0, 4.0] + # Spawn shapes + cfg = sim_utils.MultiAssetSpawnerCfg( + assets_cfg=[ + sim_utils.ConeCfg( + radius=0.3, + height=0.6, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=mass_variations[0]), + collision_props=sim_utils.CollisionPropertiesCfg(), + ), + sim_utils.CuboidCfg( + size=(0.3, 0.3, 0.3), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=mass_variations[1]), + collision_props=sim_utils.CollisionPropertiesCfg(), + ), + sim_utils.SphereCfg( + radius=0.3, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0), metallic=0.2), + rigid_props=sim_utils.RigidBodyPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=mass_variations[2]), + collision_props=sim_utils.CollisionPropertiesCfg(), + ), + ], + random_choice=True, + ) + prim = cfg.func("/World/env_.*/Cone", cfg) + + # Check validity + self.assertTrue(prim.IsValid()) + self.assertEqual(prim_utils.get_prim_path(prim), "/World/env_0/Cone") + # Find matching prims + prim_paths = prim_utils.find_matching_prim_paths("/World/env_*/Cone") + self.assertEqual(len(prim_paths), num_clones) + + # Check all prims have correct settings + for prim_path in prim_paths: + prim = prim_utils.get_prim_at_path(prim_path) + self.assertTrue(prim.GetAttribute("physics:mass").Get() in mass_variations) + + """ + Tests - Multiple USDs. + """ + + def test_spawn_multiple_files_with_global_settings(self): + """Test spawning of files randomly with global articulation settings.""" + # Define prim parents + num_clones = 10 + for i in range(num_clones): + prim_utils.create_prim(f"/World/env_{i}", "Xform", translation=(i, i, 0)) + + # Spawn shapes + cfg = sim_utils.MultiUsdFileCfg( + usd_path=[ + f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-C/anymal_c.usd", + f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-D/anymal_d.usd", + ], + random_choice=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + retain_accelerations=False, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=1000.0, + max_depenetration_velocity=1.0, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=True, solver_position_iteration_count=4, solver_velocity_iteration_count=0 + ), + activate_contact_sensors=True, + ) + prim = cfg.func("/World/env_.*/Robot", cfg) + + # Check validity + self.assertTrue(prim.IsValid()) + self.assertEqual(prim_utils.get_prim_path(prim), "/World/env_0/Robot") + # Find matching prims + prim_paths = prim_utils.find_matching_prim_paths("/World/env_*/Robot") + self.assertEqual(len(prim_paths), num_clones) + + +if __name__ == "__main__": + run_tests() diff --git a/source/standalone/demos/multi_asset.py b/source/standalone/demos/multi_asset.py new file mode 100644 index 0000000000..6363999949 --- /dev/null +++ b/source/standalone/demos/multi_asset.py @@ -0,0 +1,244 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""This script demonstrates how to spawn multiple objects in multiple environments. + +.. code-block:: bash + + # Usage + ./isaaclab.sh -p source/standalone/demos/multi_asset.py --num_envs 2048 + +""" + +from __future__ import annotations + +"""Launch Isaac Sim Simulator first.""" + + +import argparse + +from omni.isaac.lab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Demo on spawning different objects in multiple environments.") +parser.add_argument("--num_envs", type=int, default=1024, help="Number of environments to spawn.") +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import random + +import omni.usd +from pxr import Gf, Sdf + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg +from omni.isaac.lab.scene import InteractiveScene, InteractiveSceneCfg +from omni.isaac.lab.sim import SimulationContext +from omni.isaac.lab.utils import Timer, configclass +from omni.isaac.lab.utils.assets import ISAACLAB_NUCLEUS_DIR + +## +# Pre-defined Configuration +## + +from omni.isaac.lab_assets.anymal import ANYDRIVE_3_LSTM_ACTUATOR_CFG # isort: skip + + +## +# Randomization events. +## + + +def randomize_shape_color(prim_path_expr: str): + """Randomize the color of the geometry.""" + # acquire stage + stage = omni.usd.get_context().get_stage() + # resolve prim paths for spawning and cloning + prim_paths = sim_utils.find_matching_prim_paths(prim_path_expr) + # manually clone prims if the source prim path is a regex expression + with Sdf.ChangeBlock(): + for prim_path in prim_paths: + # spawn single instance + prim_spec = Sdf.CreatePrimInLayer(stage.GetRootLayer(), prim_path) + + # DO YOUR OWN OTHER KIND OF RANDOMIZATION HERE! + # Note: Just need to acquire the right attribute about the property you want to set + # Here is an example on setting color randomly + color_spec = prim_spec.GetAttributeAtPath(prim_path + "/geometry/material/Shader.inputs:diffuseColor") + color_spec.default = Gf.Vec3f(random.random(), random.random(), random.random()) + + +## +# Scene Configuration +## + + +@configclass +class MultiObjectSceneCfg(InteractiveSceneCfg): + """Configuration for a multi-object scene.""" + + # ground plane + ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg()) + + # lights + dome_light = AssetBaseCfg( + prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) + ) + + # rigid object + object: RigidObjectCfg = RigidObjectCfg( + prim_path="/World/envs/env_.*/Object", + spawn=sim_utils.MultiAssetSpawnerCfg( + assets_cfg=[ + sim_utils.ConeCfg( + radius=0.3, + height=0.6, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2), + ), + sim_utils.CuboidCfg( + size=(0.3, 0.3, 0.3), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2), + ), + sim_utils.SphereCfg( + radius=0.3, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0), metallic=0.2), + ), + ], + random_choice=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=4, solver_velocity_iteration_count=0 + ), + mass_props=sim_utils.MassPropertiesCfg(mass=1.0), + collision_props=sim_utils.CollisionPropertiesCfg(), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 2.0)), + ) + + # articulation + robot: ArticulationCfg = ArticulationCfg( + prim_path="/World/envs/env_.*/Robot", + spawn=sim_utils.MultiUsdFileCfg( + usd_path=[ + f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-C/anymal_c.usd", + f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-D/anymal_d.usd", + ], + random_choice=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + retain_accelerations=False, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=1000.0, + max_depenetration_velocity=1.0, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=True, solver_position_iteration_count=4, solver_velocity_iteration_count=0 + ), + activate_contact_sensors=True, + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.0, 0.6), + joint_pos={ + ".*HAA": 0.0, # all HAA + ".*F_HFE": 0.4, # both front HFE + ".*H_HFE": -0.4, # both hind HFE + ".*F_KFE": -0.8, # both front KFE + ".*H_KFE": 0.8, # both hind KFE + }, + ), + actuators={"legs": ANYDRIVE_3_LSTM_ACTUATOR_CFG}, + ) + + +## +# Simulation Loop +## + + +def run_simulator(sim: SimulationContext, scene: InteractiveScene): + """Runs the simulation loop.""" + # Extract scene entities + # note: we only do this here for readability. + rigid_object = scene["object"] + robot = scene["robot"] + # Define simulation stepping + sim_dt = sim.get_physics_dt() + count = 0 + # Simulation loop + while simulation_app.is_running(): + # Reset + if count % 500 == 0: + # reset counter + count = 0 + # reset the scene entities + # object + root_state = rigid_object.data.default_root_state.clone() + root_state[:, :3] += scene.env_origins + rigid_object.write_root_state_to_sim(root_state) + # robot + # -- root state + root_state = robot.data.default_root_state.clone() + root_state[:, :3] += scene.env_origins + robot.write_root_state_to_sim(root_state) + # -- joint state + joint_pos, joint_vel = robot.data.default_joint_pos.clone(), robot.data.default_joint_vel.clone() + robot.write_joint_state_to_sim(joint_pos, joint_vel) + # clear internal buffers + scene.reset() + print("[INFO]: Resetting scene state...") + + # Apply action to robot + robot.set_joint_position_target(robot.data.default_joint_pos) + # Write data to sim + scene.write_data_to_sim() + # Perform step + sim.step() + # Increment counter + count += 1 + # Update buffers + scene.update(sim_dt) + + +def main(): + """Main function.""" + # Load kit helper + sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device) + sim = SimulationContext(sim_cfg) + # Set main camera + sim.set_camera_view([2.5, 0.0, 4.0], [0.0, 0.0, 2.0]) + + # Design scene + scene_cfg = MultiObjectSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0, replicate_physics=False) + with Timer("[INFO] Time to create scene: "): + scene = InteractiveScene(scene_cfg) + + with Timer("[INFO] Time to randomize scene: "): + # DO YOUR OWN OTHER KIND OF RANDOMIZATION HERE! + # Note: Just need to acquire the right attribute about the property you want to set + # Here is an example on setting color randomly + randomize_shape_color(scene_cfg.object.prim_path) + + # Play the simulator + sim.reset() + # Now we are ready! + print("[INFO]: Setup complete...") + # Run the simulator + run_simulator(sim, scene) + + +if __name__ == "__main__": + # run the main execution + main() + # close sim app + simulation_app.close()