Spawning Multiple Assets#

Typical spawning configurations (introduced in the Spawning prims into the scene 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, we also support multi-asset spawning with two mechanisms:

  1. Rigid object collections. This allows the user to spawn multiple rigid objects in each environment and access/modify them with a unified API, improving performance.

  2. Spawning different assets under the same prim path. This allows the user to create diverse simulations, where each environment has a different asset.

This guide describes how to use these two mechanisms.

The sample script multi_asset.py is used as a reference, located in the IsaacLab/scripts/demos directory.

Code for multi_asset.py
  1# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
  2# All rights reserved.
  3#
  4# SPDX-License-Identifier: BSD-3-Clause
  5
  6"""This script demonstrates how to spawn multiple objects in multiple environments.
  7
  8.. code-block:: bash
  9
 10    # Usage with default PhysX physics and default kit visualizer.
 11    uv run python scripts/demos/multi_asset.py --num_envs 1024
 12
 13    # Usage with Newton visualizer and default PhysX physics.
 14    uv run python scripts/demos/multi_asset.py --visualizer newton --num_envs 1024
 15
 16    # Usage with Newton (MJWarp) physics and default kit visualizer.
 17    uv run python scripts/demos/multi_asset.py --physics newton_mjwarp --num_envs 1024
 18
 19    # Usage with Newton visualizer and Newton (MJWarp) physics.
 20    uv run python scripts/demos/multi_asset.py --visualizer newton --physics newton_mjwarp --num_envs 1024
 21
 22"""
 23
 24from __future__ import annotations
 25
 26"""Parse CLI first so we can decide whether to launch Isaac Sim Kit."""
 27
 28import argparse
 29from typing import TYPE_CHECKING
 30
 31from isaaclab.app import add_launcher_args, launch_simulation
 32
 33# add argparse arguments
 34parser = argparse.ArgumentParser(
 35    description="Demo on spawning different objects in multiple environments.",
 36    conflict_handler="resolve",
 37)
 38parser.add_argument("--num_envs", type=int, default=512, help="Number of environments to spawn.")
 39parser.add_argument(
 40    "--physics", default="isaacsim_physx", choices=["isaacsim_physx", "newton_mjwarp"], help="Physics backend."
 41)
 42add_launcher_args(parser)
 43# demos should open Kit visualizer by default
 44parser.set_defaults(visualizer=["kit"])
 45# parse the arguments
 46args_cli = parser.parse_args()
 47
 48import isaaclab.sim as sim_utils
 49
 50##
 51# Pre-defined configs
 52##
 53from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg, RigidObjectCollectionCfg
 54from isaaclab.physics import PhysicsCfg
 55from isaaclab.scene import InteractiveSceneCfg
 56
 57from isaaclab_assets.robots.anymal import ANYDRIVE_3_LSTM_ACTUATOR_CFG  # isort: skip
 58
 59from isaaclab.utils import Timer
 60from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR
 61from isaaclab.utils.configclass import configclass
 62
 63if TYPE_CHECKING:
 64    from isaaclab.assets import Articulation, RigidObject, RigidObjectCollection
 65    from isaaclab.scene import InteractiveScene
 66
 67
 68# Visual material presets for the multi-asset variants.
 69GREEN_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2)}
 70RED_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2)}
 71BLUE_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0), metallic=0.2)}
 72GOLD_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.75, 0.0), metallic=0.2)}
 73PURPLE_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(0.5, 0.0, 1.0), metallic=0.2)}
 74OBJECT_PHYSICS = {
 75    "rigid_props": sim_utils.RigidBodyPropertiesCfg(
 76        solver_position_iteration_count=4, solver_velocity_iteration_count=0
 77    ),
 78    "mass_props": sim_utils.MassPropertiesCfg(mass=1.0),
 79    "collision_props": sim_utils.CollisionPropertiesCfg(),
 80}
 81
 82##
 83# Scene Configuration
 84##
 85
 86
 87@configclass
 88class MultiObjectSceneCfg(InteractiveSceneCfg):
 89    """Configuration for a multi-object scene."""
 90
 91    # ground plane
 92    ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg())
 93
 94    # lights
 95    dome_light = AssetBaseCfg(
 96        prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
 97    )
 98
 99    # rigid object
100    object: RigidObjectCfg = RigidObjectCfg(
101        prim_path="/World/envs/env_.*/Object",
102        spawn=sim_utils.MultiAssetSpawnerCfg(
103            assets_cfg=[
104                sim_utils.CylinderCfg(radius=0.3, height=0.6, **GREEN_MATERIAL),
105                sim_utils.CuboidCfg(size=(0.3, 0.3, 0.3), **RED_MATERIAL),
106                sim_utils.SphereCfg(radius=0.3, **BLUE_MATERIAL),
107                sim_utils.CylinderCfg(radius=0.3, height=0.6, **GOLD_MATERIAL),
108                sim_utils.CuboidCfg(size=(0.3, 0.3, 0.3), **GOLD_MATERIAL),
109                sim_utils.SphereCfg(radius=0.3, **GOLD_MATERIAL),
110                sim_utils.CylinderCfg(radius=0.3, height=0.6, **PURPLE_MATERIAL),
111                sim_utils.CuboidCfg(size=(0.3, 0.3, 0.3), **PURPLE_MATERIAL),
112                sim_utils.SphereCfg(radius=0.3, **PURPLE_MATERIAL),
113            ],
114            random_choice=False,
115            **OBJECT_PHYSICS,
116        ),
117        init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 2.0)),
118    )
119
120    # object collection
121    object_collection: RigidObjectCollectionCfg = RigidObjectCollectionCfg(
122        rigid_objects={
123            "object_A": RigidObjectCfg(
124                prim_path="/World/envs/env_.*/Object_A",
125                spawn=sim_utils.SphereCfg(radius=0.1, **RED_MATERIAL, **OBJECT_PHYSICS),
126                init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.5, 2.0)),
127            ),
128            "object_B": RigidObjectCfg(
129                prim_path="/World/envs/env_.*/Object_B",
130                spawn=sim_utils.CuboidCfg(size=(0.1, 0.1, 0.1), **RED_MATERIAL, **OBJECT_PHYSICS),
131                init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.5, 2.0)),
132            ),
133            "object_C": RigidObjectCfg(
134                prim_path="/World/envs/env_.*/Object_C",
135                spawn=sim_utils.CylinderCfg(radius=0.1, height=0.3, **RED_MATERIAL, **OBJECT_PHYSICS),
136                init_state=RigidObjectCfg.InitialStateCfg(pos=(0.5, 0.0, 2.0)),
137            ),
138        }
139    )
140
141    # articulation
142    robot: ArticulationCfg = ArticulationCfg(
143        prim_path="/World/envs/env_.*/Robot",
144        spawn=sim_utils.MultiUsdFileCfg(
145            usd_path=[
146                f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-C/anymal_c.usd",
147                f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-D/anymal_d.usd",
148            ],
149            random_choice=False,
150            rigid_props=sim_utils.RigidBodyPropertiesCfg(
151                disable_gravity=False,
152                retain_accelerations=False,
153                linear_damping=0.0,
154                angular_damping=0.0,
155                max_linear_velocity=1000.0,
156                max_angular_velocity=1000.0,
157                max_depenetration_velocity=1.0,
158            ),
159            articulation_props=sim_utils.ArticulationRootPropertiesCfg(
160                enabled_self_collisions=True, solver_position_iteration_count=4, solver_velocity_iteration_count=0
161            ),
162            activate_contact_sensors=True,
163        ),
164        init_state=ArticulationCfg.InitialStateCfg(
165            pos=(0.0, 0.0, 0.6),
166            joint_pos={
167                ".*HAA": 0.0,  # all HAA
168                ".*F_HFE": 0.4,  # both front HFE
169                ".*H_HFE": -0.4,  # both hind HFE
170                ".*F_KFE": -0.8,  # both front KFE
171                ".*H_KFE": 0.8,  # both hind KFE
172            },
173        ),
174        actuators={"legs": ANYDRIVE_3_LSTM_ACTUATOR_CFG},
175    )
176
177
178##
179# Simulation Loop
180##
181
182
183def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
184    """Runs the simulation loop."""
185    # Extract scene entities
186    # note: we only do this here for readability.
187    rigid_object: RigidObject = scene["object"]
188    rigid_object_collection: RigidObjectCollection = scene["object_collection"]
189    robot: Articulation = scene["robot"]
190    # Define simulation stepping
191    sim_dt = sim.get_physics_dt()
192    count = 0
193    # Step while a visualizer window is still open (or none exist, e.g. headless); works for kit and newton.
194    while sim.is_headless_or_exist_active_visualizer():
195        # Reset
196        if count % 250 == 0:
197            # reset counter
198            count = 0
199            # reset the scene entities
200            # object
201            root_pose = rigid_object.data.default_root_pose.torch.clone()
202            root_pose[:, :3] += scene.env_origins
203            rigid_object.write_root_pose_to_sim_index(root_pose=root_pose)
204            root_vel = rigid_object.data.default_root_vel.torch.clone()
205            rigid_object.write_root_velocity_to_sim_index(root_velocity=root_vel)
206            # object collection
207            default_pose_w = rigid_object_collection.data.default_body_pose.torch.clone()
208            default_pose_w[..., :3] += scene.env_origins.unsqueeze(1)
209            rigid_object_collection.write_body_pose_to_sim_index(body_poses=default_pose_w)
210            default_vel_w = rigid_object_collection.data.default_body_vel.torch.clone()
211            rigid_object_collection.write_body_com_velocity_to_sim_index(body_velocities=default_vel_w)
212            # robot
213            # -- root state
214            root_pose = robot.data.default_root_pose.torch.clone()
215            root_pose[:, :3] += scene.env_origins
216            robot.write_root_pose_to_sim_index(root_pose=root_pose)
217            root_vel = robot.data.default_root_vel.torch
218            robot.write_root_velocity_to_sim_index(root_velocity=root_vel)
219            # -- joint state
220            joint_pos = robot.data.default_joint_pos.torch
221            joint_vel = robot.data.default_joint_vel.torch
222            robot.write_joint_position_to_sim_index(position=joint_pos)
223            robot.write_joint_velocity_to_sim_index(velocity=joint_vel)
224            # clear internal buffers
225            scene.reset()
226            print("[INFO]: Resetting scene state...")
227
228        # Apply action to robot
229        robot.set_joint_position_target_index(target=robot.data.default_joint_pos.torch)
230        # Write data to sim
231        scene.write_data_to_sim()
232        # Perform step
233        sim.step()
234        # Increment counter
235        count += 1
236        # Update buffers
237        scene.update(sim_dt)
238
239
240def main():
241    """Main function."""
242    with launch_simulation(cfg=PhysicsCfg(), launcher_args=args_cli) as physics_cfg:
243        sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device, physics=physics_cfg)
244        sim = sim_utils.SimulationContext(sim_cfg)
245        # Set main camera
246        sim.set_camera_view([2.5, 0.0, 4.0], [0.0, 0.0, 2.0])
247
248        # Design scene
249        scene_cfg = MultiObjectSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0, replicate_physics=True)
250        if args_cli.physics == "newton_mjwarp":
251            # Newton views currently require a uniform body layout across worlds.
252            scene_cfg.object.spawn.assets_cfg = scene_cfg.object.spawn.assets_cfg[1:2]
253            scene_cfg.robot.spawn.usd_path = scene_cfg.robot.spawn.usd_path[0]
254        with Timer("[INFO] Time to create scene: "):
255            scene = scene_cfg.class_type(scene_cfg)
256
257        # Play the simulator
258        sim.reset()
259        # Now we are ready!
260        print("[INFO]: Setup complete...")
261        # Run the simulator
262        run_simulator(sim, scene)
263
264
265if __name__ == "__main__":
266    # run the main execution
267    main()

This script creates multiple environments, where each environment has:

  • a rigid object collection containing a cone, a cube, and a sphere

  • a rigid object that is either a cone, a cube, or a sphere, chosen at random

  • an articulation that is either the ANYmal-C or ANYmal-D robot, chosen at random

result of multi_asset.py

Rigid Object Collections#

Multiple rigid objects can be spawned in each environment and accessed/modified with a unified (env_ids, obj_ids) API. While the user could also create multiple rigid objects by spawning them individually, the API is more user-friendly and more efficient since it uses a single physics view under the hood to handle all the objects.

                spawn=sim_utils.CylinderCfg(radius=0.1, height=0.3, **RED_MATERIAL, **OBJECT_PHYSICS),
                init_state=RigidObjectCfg.InitialStateCfg(pos=(0.5, 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=False,
            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

The configuration RigidObjectCollectionCfg is used to create the collection. It’s attribute rigid_objects is a dictionary containing RigidObjectCfg objects. The keys serve as unique identifiers for each rigid object in the collection.

Spawning different assets under the same prim path#

It is possible to spawn different assets and USDs under the same prim path in each environment using the spawners MultiAssetSpawnerCfg and MultiUsdFileCfg:

  • We set the spawn configuration in RigidObjectCfg to be MultiAssetSpawnerCfg:

                sim_utils.CylinderCfg(radius=0.3, height=0.6, **GOLD_MATERIAL),
                sim_utils.CuboidCfg(size=(0.3, 0.3, 0.3), **GOLD_MATERIAL),
                sim_utils.SphereCfg(radius=0.3, **GOLD_MATERIAL),
                sim_utils.CylinderCfg(radius=0.3, height=0.6, **PURPLE_MATERIAL),
                sim_utils.CuboidCfg(size=(0.3, 0.3, 0.3), **PURPLE_MATERIAL),
                sim_utils.SphereCfg(radius=0.3, **PURPLE_MATERIAL),
            ],
            random_choice=False,
            **OBJECT_PHYSICS,
        ),
        init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 2.0)),
    )
    
    # object collection
    object_collection: RigidObjectCollectionCfg = RigidObjectCollectionCfg(
        rigid_objects={
            "object_A": RigidObjectCfg(
                prim_path="/World/envs/env_.*/Object_A",
                spawn=sim_utils.SphereCfg(radius=0.1, **RED_MATERIAL, **OBJECT_PHYSICS),
                init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.5, 2.0)),
            ),
            "object_B": RigidObjectCfg(
                prim_path="/World/envs/env_.*/Object_B",
                spawn=sim_utils.CuboidCfg(size=(0.1, 0.1, 0.1), **RED_MATERIAL, **OBJECT_PHYSICS),
                init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.5, 2.0)),
            ),
            "object_C": RigidObjectCfg(
    

    This function allows you to define a list of different assets that can be spawned as rigid objects. When 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 ArticulationCfg to be MultiUsdFileCfg:

    
    def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
        """Runs the simulation loop."""
        # Extract scene entities
        # note: we only do this here for readability.
        rigid_object: RigidObject = scene["object"]
        rigid_object_collection: RigidObjectCollection = scene["object_collection"]
        robot: Articulation = scene["robot"]
        # Define simulation stepping
        sim_dt = sim.get_physics_dt()
        count = 0
        # Step while a visualizer window is still open (or none exist, e.g. headless); works for kit and newton.
        while sim.is_headless_or_exist_active_visualizer():
            # Reset
            if count % 250 == 0:
                # reset counter
                count = 0
                # reset the scene entities
                # object
                root_pose = rigid_object.data.default_root_pose.torch.clone()
                root_pose[:, :3] += scene.env_origins
                rigid_object.write_root_pose_to_sim_index(root_pose=root_pose)
                root_vel = rigid_object.data.default_root_vel.torch.clone()
                rigid_object.write_root_velocity_to_sim_index(root_velocity=root_vel)
                # object collection
                default_pose_w = rigid_object_collection.data.default_body_pose.torch.clone()
                default_pose_w[..., :3] += scene.env_origins.unsqueeze(1)
                rigid_object_collection.write_body_pose_to_sim_index(body_poses=default_pose_w)
                default_vel_w = rigid_object_collection.data.default_body_vel.torch.clone()
                rigid_object_collection.write_body_com_velocity_to_sim_index(body_velocities=default_vel_w)
                # robot
                # -- root state
                root_pose = robot.data.default_root_pose.torch.clone()
                root_pose[:, :3] += scene.env_origins
    

    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.

Physics replication in interactive scene#

By default, the flag 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 scene.InteractiveScene.replicate_physics must be disabled when the spawned assets do not share the same structure. For a full guide on the template-based cloning system including strategies and collision filtering, see Cloning Environments.


# Design scene
scene_cfg = MultiObjectSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0, replicate_physics=True)
if args_cli.physics == "newton_mjwarp":
    # Newton views currently require a uniform body layout across worlds.

The Code Execution#

To execute the script with multiple environments and randomized assets, use the following command:

python scripts/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.