Spawning Multiple Assets#

Typical spawning configurations (introduced in the Spawning prims into the scene tutorial) copy one asset across all prim paths resolved from an expression. Multi-asset workflows cover two related composition needs:

  1. A rigid object collection batches several rigid objects in every environment behind one data and command API.

  2. A multi-asset spawner declares several variants for one scene asset binding, allowing environments to contain different geometry or robot variants.

This guide demonstrates both mechanisms and explains how their execution differs between PhysX and Newton.

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 --extra isaacsim python scripts/demos/multi_asset.py --num_envs 1024
 12
 13    # Usage with Newton GL visualizer and default PhysX physics.
 14    uv run --extra isaacsim python scripts/demos/multi_asset.py --visualizer newton_gl --num_envs 1024
 15
 16    # Usage with Newton (MJWarp) physics and default kit visualizer.
 17    uv run --extra isaacsim python scripts/demos/multi_asset.py --physics newton_mjwarp --num_envs 1024
 18
 19    # Usage with Newton GL visualizer and Newton (MJWarp) physics.
 20    uv run python scripts/demos/multi_asset.py --visualizer newton_gl --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, configclass
 60from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR
 61
 62if TYPE_CHECKING:
 63    from isaaclab.assets import Articulation, RigidObject, RigidObjectCollection
 64    from isaaclab.scene import InteractiveScene
 65
 66
 67# Visual material presets for the multi-asset variants.
 68GREEN_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2)}
 69RED_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2)}
 70BLUE_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0), metallic=0.2)}
 71GOLD_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.75, 0.0), metallic=0.2)}
 72PURPLE_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(0.5, 0.0, 1.0), metallic=0.2)}
 73OBJECT_PHYSICS = {
 74    "rigid_props": sim_utils.RigidBodyPropertiesCfg(
 75        solver_position_iteration_count=4, solver_velocity_iteration_count=0
 76    ),
 77    "mass_props": sim_utils.MassPropertiesCfg(mass=1.0),
 78    "collision_props": sim_utils.CollisionPropertiesCfg(),
 79}
 80
 81##
 82# Scene Configuration
 83##
 84
 85
 86@configclass
 87class MultiObjectSceneCfg(InteractiveSceneCfg):
 88    """Configuration for a multi-object scene."""
 89
 90    # ground plane
 91    ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg())
 92
 93    # lights
 94    dome_light = AssetBaseCfg(
 95        prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75))
 96    )
 97
 98    # rigid object
 99    object: RigidObjectCfg = RigidObjectCfg(
100        prim_path="/World/envs/env_.*/Object",
101        spawn=sim_utils.MultiAssetSpawnerCfg(
102            assets_cfg=[
103                sim_utils.CylinderCfg(radius=0.3, height=0.6, **GREEN_MATERIAL),
104                sim_utils.CuboidCfg(size=(0.3, 0.3, 0.3), **RED_MATERIAL),
105                sim_utils.SphereCfg(radius=0.3, **BLUE_MATERIAL),
106                sim_utils.CylinderCfg(radius=0.3, height=0.6, **GOLD_MATERIAL),
107                sim_utils.CuboidCfg(size=(0.3, 0.3, 0.3), **GOLD_MATERIAL),
108                sim_utils.SphereCfg(radius=0.3, **GOLD_MATERIAL),
109                sim_utils.CylinderCfg(radius=0.3, height=0.6, **PURPLE_MATERIAL),
110                sim_utils.CuboidCfg(size=(0.3, 0.3, 0.3), **PURPLE_MATERIAL),
111                sim_utils.SphereCfg(radius=0.3, **PURPLE_MATERIAL),
112            ],
113            random_choice=False,
114            **OBJECT_PHYSICS,
115        ),
116        init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 2.0)),
117    )
118
119    # object collection
120    object_collection: RigidObjectCollectionCfg = RigidObjectCollectionCfg(
121        rigid_objects={
122            "object_A": RigidObjectCfg(
123                prim_path="/World/envs/env_.*/Object_A",
124                spawn=sim_utils.SphereCfg(radius=0.1, **RED_MATERIAL, **OBJECT_PHYSICS),
125                init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.5, 2.0)),
126            ),
127            "object_B": RigidObjectCfg(
128                prim_path="/World/envs/env_.*/Object_B",
129                spawn=sim_utils.CuboidCfg(size=(0.1, 0.1, 0.1), **RED_MATERIAL, **OBJECT_PHYSICS),
130                init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.5, 2.0)),
131            ),
132            "object_C": RigidObjectCfg(
133                prim_path="/World/envs/env_.*/Object_C",
134                spawn=sim_utils.CylinderCfg(radius=0.1, height=0.3, **RED_MATERIAL, **OBJECT_PHYSICS),
135                init_state=RigidObjectCfg.InitialStateCfg(pos=(0.5, 0.0, 2.0)),
136            ),
137        }
138    )
139
140    # articulation
141    robot: ArticulationCfg = ArticulationCfg(
142        prim_path="/World/envs/env_.*/Robot",
143        spawn=sim_utils.MultiUsdFileCfg(
144            usd_path=[
145                f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-C/anymal_c.usd",
146                f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-D/anymal_d.usd",
147            ],
148            random_choice=False,
149            rigid_props=sim_utils.RigidBodyPropertiesCfg(
150                disable_gravity=False,
151                retain_accelerations=False,
152                linear_damping=0.0,
153                angular_damping=0.0,
154                max_linear_velocity=1000.0,
155                max_angular_velocity=1000.0,
156                max_depenetration_velocity=1.0,
157            ),
158            articulation_props=sim_utils.ArticulationRootPropertiesCfg(
159                enabled_self_collisions=True, solver_position_iteration_count=4, solver_velocity_iteration_count=0
160            ),
161            activate_contact_sensors=True,
162        ),
163        init_state=ArticulationCfg.InitialStateCfg(
164            pos=(0.0, 0.0, 0.6),
165            joint_pos={
166                ".*HAA": 0.0,  # all HAA
167                ".*F_HFE": 0.4,  # both front HFE
168                ".*H_HFE": -0.4,  # both hind HFE
169                ".*F_KFE": -0.8,  # both front KFE
170                ".*H_KFE": 0.8,  # both hind KFE
171            },
172        ),
173        actuators={"legs": ANYDRIVE_3_LSTM_ACTUATOR_CFG},
174    )
175
176
177##
178# Simulation Loop
179##
180
181
182def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
183    """Runs the simulation loop."""
184    # Extract scene entities
185    # note: we only do this here for readability.
186    rigid_object: RigidObject = scene["object"]
187    rigid_object_collection: RigidObjectCollection = scene["object_collection"]
188    robot: Articulation = scene["robot"]
189    # Define simulation stepping
190    sim_dt = sim.get_physics_dt()
191    count = 0
192    # Step while a visualizer window is still open (or none exist, e.g. headless); works for kit and newton.
193    while sim.is_headless_or_exist_active_visualizer():
194        # Reset
195        if count % 250 == 0:
196            # reset counter
197            count = 0
198            # reset the scene entities
199            # object
200            root_pose = rigid_object.data.default_root_pose.torch.clone()
201            root_pose[:, :3] += scene.env_origins
202            rigid_object.write_root_pose_to_sim_index(root_pose=root_pose)
203            root_vel = rigid_object.data.default_root_vel.torch.clone()
204            rigid_object.write_root_velocity_to_sim_index(root_velocity=root_vel)
205            # object collection
206            default_pose_w = rigid_object_collection.data.default_body_pose.torch.clone()
207            default_pose_w[..., :3] += scene.env_origins.unsqueeze(1)
208            rigid_object_collection.write_body_pose_to_sim_index(body_poses=default_pose_w)
209            default_vel_w = rigid_object_collection.data.default_body_vel.torch.clone()
210            rigid_object_collection.write_body_com_velocity_to_sim_index(body_velocities=default_vel_w)
211            # robot
212            # -- root state
213            root_pose = robot.data.default_root_pose.torch.clone()
214            root_pose[:, :3] += scene.env_origins
215            robot.write_root_pose_to_sim_index(root_pose=root_pose)
216            root_vel = robot.data.default_root_vel.torch
217            robot.write_root_velocity_to_sim_index(root_velocity=root_vel)
218            # -- joint state
219            joint_pos = robot.data.default_joint_pos.torch
220            joint_vel = robot.data.default_joint_vel.torch
221            robot.write_joint_position_to_sim_index(position=joint_pos)
222            robot.write_joint_velocity_to_sim_index(velocity=joint_vel)
223            # clear internal buffers
224            scene.reset()
225            print("[INFO]: Resetting scene state...")
226
227        # Apply action to robot
228        robot.set_joint_position_target_index(target=robot.data.default_joint_pos.torch)
229        # Write data to sim
230        scene.write_data_to_sim()
231        # Perform step
232        sim.step()
233        # Increment counter
234        count += 1
235        # Update buffers
236        scene.update(sim_dt)
237
238
239def main():
240    """Main function."""
241    with launch_simulation(cfg=PhysicsCfg(), launcher_args=args_cli) as physics_cfg:
242        sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device, physics=physics_cfg)
243        sim = sim_utils.SimulationContext(sim_cfg)
244        # Set main camera
245        sim.set_camera_view([2.5, 0.0, 4.0], [0.0, 0.0, 2.0])
246
247        # Design scene
248        scene_cfg = MultiObjectSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0, replicate_physics=True)
249        if args_cli.physics == "newton_mjwarp":
250            # Newton views currently require a uniform body layout across worlds.
251            scene_cfg.object.spawn.assets_cfg = scene_cfg.object.spawn.assets_cfg[1:2]
252            scene_cfg.robot.spawn.usd_path = scene_cfg.robot.spawn.usd_path[0]
253        with Timer("[INFO] Time to create scene: "):
254            scene = scene_cfg.class_type(scene_cfg)
255
256        # Play the simulator
257        sim.reset()
258        # Now we are ready!
259        print("[INFO]: Setup complete...")
260        # Run the simulator
261        run_simulator(sim, scene)
262
263
264if __name__ == "__main__":
265    # run the main execution
266    main()

With the default PhysX configuration, this script creates multiple environments containing:

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

  • a rigid object selected from nine geometry and material variants by the clone plan

  • an articulation selected from the ANYmal-C and ANYmal-D variants by the clone plan

result of multi_asset.py

Rigid object collections#

Use a rigid object collection when every environment contains the same set of independently moving rigid bodies and you want to access them as one batch. The collection exposes data with an (env, object, ...) layout and accepts (env_ids, obj_ids) selections for commands. Compared with managing each object separately, the collection uses one batched physics view.

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(
            prim_path="/World/envs/env_.*/Object_C",
            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)),
        ),
    }
)

The RigidObjectCollectionCfg configuration owns a dictionary of RigidObjectCfg instances. Each dictionary key is the object’s stable identifier within the collection.

The demo resets all collection members through the same API used by both physics backends:

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)

Spawning variants for one scene asset#

Use MultiAssetSpawnerCfg and MultiUsdFileCfg to declare the available variants for one scene asset binding. InteractiveScene includes these variants in its clone plan and assigns one valid prototype combination to each environment.

For configuration-based assets, assign MultiAssetSpawnerCfg to the RigidObjectCfg spawn configuration:

object: RigidObjectCfg = RigidObjectCfg(
    prim_path="/World/envs/env_.*/Object",
    spawn=sim_utils.MultiAssetSpawnerCfg(
        assets_cfg=[
            sim_utils.CylinderCfg(radius=0.3, height=0.6, **GREEN_MATERIAL),
            sim_utils.CuboidCfg(size=(0.3, 0.3, 0.3), **RED_MATERIAL),
            sim_utils.SphereCfg(radius=0.3, **BLUE_MATERIAL),
            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)),
)

The assets_cfg list defines the prototypes available to the clone plan. Variant assignment is controlled by clone_strategy; the default sequential() strategy assigns combinations in round-robin order. To sample combinations randomly instead, set the strategy before constructing the scene:

from isaaclab import cloner

scene_cfg.clone_cfg.clone_strategy = cloner.random

For USD assets, assign MultiUsdFileCfg to the ArticulationCfg spawn configuration:

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},
)


Variant compatibility#

All variants behind one batched asset interface must have a compatible structure. Articulation variants must have the same links, joints, collision-body count, and names. Rigid object variants can differ in geometry and material while retaining a compatible rigid-body layout. Model structurally different assets as separate scene bindings.

Clone planning and physics replication#

InteractiveScene represents multi-asset variants as clone-plan prototypes. It can therefore keep replicate_physics enabled and replicate each prototype only to its assigned environments. Do not disable physics replication merely because a scene uses a multi-asset spawner. Reserve replicate_physics=False for per-environment stage differences that cannot be represented as clone variants; that mode is not supported by the Newton backend.

The demo keeps physics replication enabled. For Newton, it also narrows the standalone object and articulation to one variant because their batched Newton views currently require a uniform body layout across worlds:

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.
    scene_cfg.object.spawn.assets_cfg = scene_cfg.object.spawn.assets_cfg[1:2]
    scene_cfg.robot.spawn.usd_path = scene_cfg.robot.spawn.usd_path[0]

For more detail on prototype assignment and replication, see Cloning Environments.

Run the demo#

The physics backend and visualizer are selected independently. Run one of these commands from the repository root:

uv run --extra isaacsim python scripts/demos/multi_asset.py --num_envs 2048
uv run --extra isaacsim python scripts/demos/multi_asset.py \
    --physics newton_mjwarp --num_envs 2048
uv run python scripts/demos/multi_asset.py \
    --physics newton_mjwarp --visualizer newton_gl --num_envs 2048

The Newton commands exercise the same RigidObjectCollectionCfg and (env_ids, obj_ids) APIs as the PhysX command. They do not demonstrate per-environment object or articulation variants because of the uniform-layout restriction described above; use the PhysX command to inspect that part of the example. See the installation guide before running the kitless command.

To stop the simulation, you can close the window, or press Ctrl+C in the terminal.