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