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 packaged multi-asset example provides the reference script, multi_asset.py.

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

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 example 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=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 and dispatches each prototype only to its assigned environments. Physics assets use their declared clone contexts, including native physics replication by default. Declare per-environment stage differences as clone_combinations prototypes.

The example relies on physics replication. 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)
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 example#

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

uv run --extra isaacsim isaaclab example multi-asset \
    --physics isaacsim_physx --visualizer kit --num_envs 2048
uv run --extra isaacsim isaaclab example multi-asset \
    --physics newton_mjwarp --visualizer kit --num_envs 2048
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 before running the kitless command.

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