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

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