> This page is for version Release 3.0 (default).
> For other versions, use one of these documentation indexes:
> - Release 3.0 (default): https://docs.nvidia.com/isaaclab/3.0/llms.txt
> - Release 2.3: https://docs.nvidia.com/isaaclab/2.3/llms.txt
> - Release 2.2: https://docs.nvidia.com/isaaclab/2.2/llms.txt
> - Release 2.1: https://docs.nvidia.com/isaaclab/2.1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/isaaclab/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/isaaclab/_mcp/server.

# Spawning Multiple Assets

Typical spawning configurations (introduced in the [Spawning prims into the scene](/isaaclab/3.0/isaac-lab/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 packaged `multi-asset` example provides the reference script, `multi_asset.py`.

#### Code for multi_asset.py

```python
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Spawn several asset types across cloned environments.

.. code-block:: bash

    uvx isaaclab example multi-asset --num_envs 1024
"""

from __future__ import annotations

import argparse
from typing import TYPE_CHECKING

from isaaclab.app import add_launcher_args, launch_simulation

parser = argparse.ArgumentParser(
    description="Example of spawning different objects in multiple environments.",
    conflict_handler="resolve",
)
parser.add_argument("--num_envs", type=int, default=512, help="Number of environments to spawn.")
parser.add_argument(
    "--physics", default="newton_mjwarp", choices=["isaacsim_physx", "newton_mjwarp"], help="Physics backend."
)
parser.add_argument("--max_steps", type=int, default=-1, help="Stop after this many steps; negative runs forever.")
add_launcher_args(parser)
parser.set_defaults(visualizer=["newton_gl"])
args_cli = parser.parse_args()
if args_cli.max_steps == 0 or args_cli.max_steps < -1:
    parser.error("--max_steps must be positive or -1.")

from isaaclab_newton.sim.schemas import NewtonArticulationCfg
from isaaclab_physx.sim.schemas import PhysxArticulationCfg, PhysxRigidBodyCfg

import isaaclab.sim as sim_utils
from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg, RigidObjectCollectionCfg
from isaaclab.physics import PhysicsCfg
from isaaclab.scene import InteractiveSceneCfg

from isaaclab_assets.robots.anymal import ANYDRIVE_3_LSTM_ACTUATOR_CFG  # isort: skip

from isaaclab.utils import Timer, configclass, instantiate
from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR

if TYPE_CHECKING:
    from isaaclab.assets import Articulation, RigidObject, RigidObjectCollection
    from isaaclab.scene import InteractiveScene

# Visual material presets for the multi-asset variants.
GREEN_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0), metallic=0.2)}
RED_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0), metallic=0.2)}
BLUE_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 0.0, 1.0), metallic=0.2)}
GOLD_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.75, 0.0), metallic=0.2)}
PURPLE_MATERIAL = {"visual_material": sim_utils.PreviewSurfaceCfg(diffuse_color=(0.5, 0.0, 1.0), metallic=0.2)}
OBJECT_PHYSICS = {
    "rigid_props": PhysxRigidBodyCfg(solver_position_iteration_count=4, solver_velocity_iteration_count=0),
    "mass_props": sim_utils.MassCfg(mass=1.0),
    "collision_props": sim_utils.UsdPhysicsCollisionCfg(),
}

##
# 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.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)),
    )

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

    # 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=PhysxRigidBodyCfg(
                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=[
                PhysxArticulationCfg(
                    enabled_self_collisions=True, solver_position_iteration_count=4, solver_velocity_iteration_count=0
                ),
                NewtonArticulationCfg(self_collision_enabled=True),
            ],
            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: 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_count = 0
    # Step while a visualizer window is still open (or none exist, e.g. headless); works for kit and newton.
    while sim.is_running() and (args_cli.max_steps < 0 or step_count < args_cli.max_steps):
        # 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
            robot.write_root_pose_to_sim_index(root_pose=root_pose)
            root_vel = robot.data.default_root_vel.torch
            robot.write_root_velocity_to_sim_index(root_velocity=root_vel)
            # -- joint state
            joint_pos = robot.data.default_joint_pos.torch
            joint_vel = robot.data.default_joint_vel.torch
            robot.write_joint_position_to_sim_index(position=joint_pos)
            robot.write_joint_velocity_to_sim_index(velocity=joint_vel)
            # clear internal buffers
            scene.reset()
            print("[INFO]: Resetting scene state...")

        # Apply action to robot
        robot.set_joint_position_target_index(target=robot.data.default_joint_pos.torch)
        # Write data to sim
        scene.write_data_to_sim()
        # Perform step
        sim.step()
        step_count += 1
        # Increment counter
        count += 1
        # Update buffers
        scene.update(sim_dt)

def main():
    """Main function."""
    with launch_simulation(cfg=PhysicsCfg(), launcher_args=args_cli) as physics_cfg:
        sim_cfg = sim_utils.SimulationCfg(dt=0.005, device=args_cli.device, physics=physics_cfg)
        sim = sim_utils.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=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]
        with Timer("[INFO] Time to create scene: "):
            scene = instantiate(scene_cfg)

        sim.reset()
        print("[INFO]: Setup complete...")
        run_simulator(sim, scene)

if __name__ == "__main__":
    main()
```

With the default Newton 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

![](/isaaclab/_fern-img/611d7a00a494ae2959846eb7153795f44c8c23068158af5a03e98bfd1d086b6b.webp)

## 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.

```python
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 example resets all collection members through the same API used by both physics backends:

```python
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:

```python
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:

```python
from isaaclab import cloner

scene_cfg.clone_cfg.clone_strategy = cloner.random
```

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

```python
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=PhysxRigidBodyCfg(
            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=[
            PhysxArticulationCfg(
                enabled_self_collisions=True, solver_position_iteration_count=4, solver_velocity_iteration_count=0
            ),
            NewtonArticulationCfg(self_collision_enabled=True),
        ],
        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 example 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:

```python
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](/isaaclab/3.0/isaac-lab/cloning-environments).

## Run the example

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

#### PhysX with Kit

```bash
uv run --extra isaacsim isaaclab example multi-asset \
    --physics isaacsim_physx --visualizer kit --num_envs 2048
```

#### Newton MJWarp with Kit

```bash
uv run --extra isaacsim isaaclab example multi-asset \
    --physics newton_mjwarp --visualizer kit --num_envs 2048
```

#### Newton MJWarp with Newton GL

```bash
uv run isaaclab example multi-asset \
    --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 ](/isaaclab/3.0/isaac-lab/installation) before running
the kitless command.

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