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:
A rigid object collection batches several rigid objects in every environment behind one data and command API.
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
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.