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