Creating a simulation scene#
This tutorial shows how to create a basic simulation scene using SimulationManager. It covers the setup of the simulation context, adding rigid objects, running the simulation loop, and exporting a video automatically when the example runs in headless mode.
The Code#
The tutorial corresponds to the create_scene.py script in the scripts/tutorials/sim directory.
Code for create_scene.py
1# ----------------------------------------------------------------------------
2# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15# ----------------------------------------------------------------------------
16
17from __future__ import annotations
18
19"""
20This script demonstrates how to create a simulation scene using SimulationManager.
21It shows the basic setup of simulation context, adding objects, and sensors.
22"""
23
24import argparse
25import time
26
27from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
28from embodichain.lab.sim.cfg import RigidBodyAttributesCfg, RenderCfg
29from embodichain.lab.sim.shapes import CubeCfg, MeshCfg
30from embodichain.lab.sim.objects import RigidObject, RigidObjectCfg
31from embodichain.lab.gym.utils.gym_utils import add_env_launcher_args_to_parser
32from embodichain.data import get_data_path
33
34
35def main():
36 """Main function to create and run the simulation scene."""
37
38 # Parse command line arguments
39 parser = argparse.ArgumentParser(
40 description="Create a simulation scene with SimulationManager"
41 )
42 add_env_launcher_args_to_parser(parser)
43 parser.add_argument(
44 "--record-steps",
45 type=int,
46 default=1000,
47 help="Number of simulation steps to record before exiting in headless mode.",
48 )
49 parser.add_argument(
50 "--record-fps",
51 type=int,
52 default=20,
53 help="Output video FPS for headless recording.",
54 )
55 parser.add_argument(
56 "--record-save-path",
57 type=str,
58 default=None,
59 help="Optional mp4 output path for headless recording.",
60 )
61 args = parser.parse_args()
62
63 # Configure the simulation
64 sim_cfg = SimulationManagerCfg(
65 width=1920,
66 height=1080,
67 headless=True,
68 physics_dt=1.0 / 100.0, # Physics timestep (100 Hz)
69 sim_device=args.device,
70 render_cfg=RenderCfg(
71 renderer=args.renderer,
72 ),
73 num_envs=args.num_envs,
74 arena_space=3.0,
75 )
76
77 # Create the simulation instance
78 sim = SimulationManager(sim_cfg)
79
80 # Add cube object to the scene
81 cube: RigidObject = sim.add_rigid_object(
82 cfg=RigidObjectCfg(
83 uid="cube",
84 shape=CubeCfg(size=[0.1, 0.1, 0.1]),
85 body_type="dynamic",
86 attrs=RigidBodyAttributesCfg(
87 mass=1.0,
88 dynamic_friction=0.5,
89 static_friction=0.5,
90 restitution=0.1,
91 ),
92 init_pos=[0, 0.0, 1.0],
93 )
94 )
95
96 # Add chair object to the scene
97 path = get_data_path("Chair/chair.glb")
98 chair: RigidObject = sim.add_rigid_object(
99 cfg=RigidObjectCfg(
100 uid="chair",
101 shape=MeshCfg(fpath=path),
102 body_type="dynamic",
103 attrs=RigidBodyAttributesCfg(
104 mass=3.0,
105 ),
106 body_scale=[0.5, 0.5, 0.5],
107 init_pos=[0.0, 0.0, 0.2],
108 init_rot=[90.0, 0.0, 0.0],
109 )
110 )
111
112 print("[INFO]: Scene setup complete!")
113 print(f"[INFO]: Running simulation with {args.num_envs} environment(s)")
114 print("[INFO]: Press Ctrl+C to stop the simulation")
115
116 # Open window when the scene has been set up
117 if not args.headless:
118 sim.open_window()
119
120 if args.headless:
121 if not sim.start_window_record(
122 save_path=args.record_save_path,
123 fps=args.record_fps,
124 max_memory=2048,
125 video_prefix="create_scene_headless",
126 look_at=((2.6, -2.2, 1.6), (0.0, 0.0, 0.45), (0.0, 0.0, 1.0)),
127 ):
128 raise RuntimeError("Failed to start headless recording")
129 print("[INFO]: Headless recording enabled.")
130 print(
131 "[INFO]: The output path is reported by `SimulationManager.start_window_record()`."
132 )
133 print(f"[INFO]: Running {args.record_steps} steps before exporting the video")
134
135 # Run the simulation
136 run_simulation(
137 sim,
138 max_steps=args.record_steps if args.headless else None,
139 )
140
141
142def run_simulation(
143 sim: SimulationManager,
144 max_steps: int | None = None,
145):
146 """Run the simulation loop.
147
148 Args:
149 sim: The SimulationManager instance to run.
150 max_steps: Optional maximum number of simulation steps to execute.
151 """
152
153 # Initialize GPU physics if using CUDA
154 if sim.is_use_gpu_physics:
155 sim.init_gpu_physics()
156
157 step_count = 0
158
159 try:
160 last_time = time.time()
161 last_step = 0
162 while True:
163 # Update physics simulation
164 sim.update(step=1)
165 step_count += 1
166
167 # Print FPS every second
168 if step_count % 100 == 0:
169 current_time = time.time()
170 elapsed = current_time - last_time
171 fps = (
172 sim.num_envs * (step_count - last_step) / elapsed
173 if elapsed > 0
174 else 0
175 )
176 print(f"[INFO]: Simulation step: {step_count}, FPS: {fps:.2f}")
177 last_time = current_time
178 last_step = step_count
179
180 if max_steps is not None and step_count >= max_steps:
181 print(
182 f"[INFO]: Reached {max_steps} steps. Exporting headless recording..."
183 )
184 break
185
186 except KeyboardInterrupt:
187 print("\n[INFO]: Stopping simulation...")
188 finally:
189 if sim.is_window_recording():
190 sim.stop_window_record()
191 sim.wait_window_record_saves()
192
193 # Clean up resources
194 sim.destroy()
195
196
197if __name__ == "__main__":
198 main()
The Code Explained#
Configuring the simulation#
The first step is to configure the simulation environment. This is done using the SimulationManagerCfg data class, which allows you to specify various parameters like window dimensions, headless mode, physics timestep, simulation device (CPU/GPU), and rendering options like ray tracing.
Command-line arguments are parsed using argparse to allow for easy customization of the simulation from the terminal. In addition to the common launcher flags, this tutorial adds --record-steps, --record-fps, and --record-save-path for headless recording.
# Parse command line arguments
parser = argparse.ArgumentParser(
description="Create a simulation scene with SimulationManager"
)
add_env_launcher_args_to_parser(parser)
parser.add_argument(
"--record-steps",
type=int,
default=1000,
help="Number of simulation steps to record before exiting in headless mode.",
)
parser.add_argument(
"--record-fps",
type=int,
default=20,
help="Output video FPS for headless recording.",
)
parser.add_argument(
"--record-save-path",
type=str,
default=None,
help="Optional mp4 output path for headless recording.",
)
args = parser.parse_args()
# Configure the simulation
sim_cfg = SimulationManagerCfg(
width=1920,
height=1080,
headless=True,
physics_dt=1.0 / 100.0, # Physics timestep (100 Hz)
sim_device=args.device,
render_cfg=RenderCfg(
renderer=args.renderer,
),
num_envs=args.num_envs,
arena_space=3.0,
)
# Create the simulation instance
sim = SimulationManager(sim_cfg)
There are two kinds of physics mode in SimulationManager:
manual: The physics updates only when the user calls the
SimulationManager.update()function. This mode is used for robot learning tasks where precise control over simulation steps is required. Enabled by settingSimulationManager.set_manual_update()to True.auto: The physics updates in a standalone thread, which enable asynchronous rendering and physics stepping. This mode is suitable for visualizations and demos for digital twins applications. This is the default mode.
Adding objects to the scene#
With the simulation context created, we can add objects. This tutorial demonstrates adding a dynamic rigid cube and a chair mesh to the scene using the SimulationManager.add_rigid_object() method. Their properties, such as shape, initial pose, and physics attributes (mass, friction, restitution), are defined through cfg.RigidObjectCfg.
# Add cube object to the scene
cube: RigidObject = sim.add_rigid_object(
cfg=RigidObjectCfg(
uid="cube",
shape=CubeCfg(size=[0.1, 0.1, 0.1]),
body_type="dynamic",
attrs=RigidBodyAttributesCfg(
mass=1.0,
dynamic_friction=0.5,
static_friction=0.5,
restitution=0.1,
),
init_pos=[0, 0.0, 1.0],
)
)
# Add chair object to the scene
path = get_data_path("Chair/chair.glb")
chair: RigidObject = sim.add_rigid_object(
cfg=RigidObjectCfg(
uid="chair",
shape=MeshCfg(fpath=path),
body_type="dynamic",
attrs=RigidBodyAttributesCfg(
mass=3.0,
),
body_scale=[0.5, 0.5, 0.5],
init_pos=[0.0, 0.0, 0.2],
init_rot=[90.0, 0.0, 0.0],
)
)
Headless recording#
When the script runs with --headless, it uses SimulationManager.start_window_record() with a fixed look_at camera pose. This is the same public recorder API used for viewer recording, but it now also supports headless execution without depending on a live window.
The example starts recording before the simulation loop, runs for --record-steps physics steps, then stops the recorder and waits for the video export to finish before destroying the simulation.
if args.headless:
if not sim.start_window_record(
save_path=args.record_save_path,
fps=args.record_fps,
max_memory=2048,
video_prefix="create_scene_headless",
look_at=((2.6, -2.2, 1.6), (0.0, 0.0, 0.45), (0.0, 0.0, 1.0)),
):
raise RuntimeError("Failed to start headless recording")
print("[INFO]: Headless recording enabled.")
print(
"[INFO]: The output path is reported by `SimulationManager.start_window_record()`."
)
print(f"[INFO]: Running {args.record_steps} steps before exporting the video")
Running the simulation#
The simulation is advanced through a loop in the run_simulation function. Before starting the loop, GPU physics is initialized if a CUDA device is used.
Inside the loop, SimulationManager.update() is called to step the physics simulation forward. The script also includes logic to calculate and print the Frames Per Second (FPS) to monitor performance. In GUI mode the simulation runs until it is manually stopped with Ctrl+C. In headless mode the loop exits automatically after the configured number of recording steps.
def run_simulation(
sim: SimulationManager,
max_steps: int | None = None,
):
"""Run the simulation loop.
Args:
sim: The SimulationManager instance to run.
max_steps: Optional maximum number of simulation steps to execute.
"""
# Initialize GPU physics if using CUDA
if sim.is_use_gpu_physics:
sim.init_gpu_physics()
step_count = 0
try:
last_time = time.time()
last_step = 0
while True:
# Update physics simulation
sim.update(step=1)
step_count += 1
# Print FPS every second
if step_count % 100 == 0:
current_time = time.time()
elapsed = current_time - last_time
fps = (
sim.num_envs * (step_count - last_step) / elapsed
if elapsed > 0
else 0
)
print(f"[INFO]: Simulation step: {step_count}, FPS: {fps:.2f}")
last_time = current_time
last_step = step_count
if max_steps is not None and step_count >= max_steps:
print(
f"[INFO]: Reached {max_steps} steps. Exporting headless recording..."
)
break
Exiting the simulation#
Upon exiting the simulation loop (e.g., by a KeyboardInterrupt), it’s important to clean up resources. The example stops any active recording, waits for the background video export to finish, then calls SimulationManager.destroy() in a finally block to ensure that the simulation is properly terminated and all allocated resources are released.
The Code Execution#
To run the script and see the result, execute the following command:
python scripts/tutorials/sim/create_scene.py
A window should appear showing a cube dropping onto a flat plane. To stop the simulation, you can either close the window or press Ctrl+C in the terminal.
You can also pass arguments to customize the simulation. For example, to run in headless mode with n parallel environments using the specified device:
python scripts/tutorials/sim/create_scene.py --headless --num_envs <n> --device <cuda/cpu>
In headless mode, the script records a video and saves it under outputs/videos by default. You can control the exported clip length and destination:
python scripts/tutorials/sim/create_scene.py \
--headless \
--record-steps 1000 \
--record-fps 20 \
--record-save-path outputs/videos/my_scene.mp4
Now that we have a basic understanding of how to create a scene, let’s move on to more advanced topics.
Next Steps#
Creating a soft-body simulation — Add deformable bodies to your scene
Simulating a Robot — Load and control a robot
Simulating a Camera Sensor — Add cameras and capture sensor data
Creating a Basic Environment — Create your first Gymnasium environment
Simulation Manager — Full SimulationManager API reference