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
17"""
18This script demonstrates how to create a simulation scene using SimulationManager.
19It shows the basic setup of simulation context, adding objects, and sensors.
20"""
21
22from __future__ import annotations
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.lab.visualization import visualization_cfg_from_args
33from embodichain.data import get_data_path
34
35
36def main() -> None:
37 """Main function to create and run the simulation scene."""
38
39 # Parse command line arguments
40 parser = argparse.ArgumentParser(
41 description="Create a simulation scene with SimulationManager"
42 )
43 add_env_launcher_args_to_parser(parser)
44 parser.add_argument(
45 "--record-steps",
46 type=int,
47 default=1000,
48 help=(
49 "Number of simulation steps before exiting in headless recording " "mode."
50 ),
51 )
52 parser.add_argument(
53 "--record-fps",
54 type=int,
55 default=20,
56 help="Output video FPS for headless recording.",
57 )
58 parser.add_argument(
59 "--record-save-path",
60 type=str,
61 default=None,
62 help="Optional mp4 output path for headless recording.",
63 )
64 args = parser.parse_args()
65 # Configure the simulation
66 sim_cfg = SimulationManagerCfg(
67 width=1920,
68 height=1080,
69 headless=True,
70 physics_dt=1.0 / 100.0, # Physics timestep (100 Hz)
71 sim_device=args.device,
72 render_cfg=RenderCfg(
73 renderer=args.renderer,
74 ),
75 num_envs=args.num_envs,
76 arena_space=3.0,
77 visualization=visualization_cfg_from_args(args),
78 )
79
80 # Create the simulation instance
81 sim = SimulationManager(sim_cfg)
82
83 # Add cube object to the scene
84 cube: RigidObject = sim.add_rigid_object(
85 cfg=RigidObjectCfg(
86 uid="cube",
87 shape=CubeCfg(size=[0.1, 0.1, 0.1]),
88 body_type="dynamic",
89 attrs=RigidBodyAttributesCfg(
90 mass=1.0,
91 dynamic_friction=0.5,
92 static_friction=0.5,
93 restitution=0.1,
94 ),
95 init_pos=[0, 0.0, 1.0],
96 )
97 )
98
99 # Add chair object to the scene
100 path = get_data_path("Chair/chair.glb")
101 chair: RigidObject = sim.add_rigid_object(
102 cfg=RigidObjectCfg(
103 uid="chair",
104 shape=MeshCfg(fpath=path),
105 body_type="dynamic",
106 attrs=RigidBodyAttributesCfg(
107 mass=3.0,
108 ),
109 body_scale=[0.5, 0.5, 0.5],
110 init_pos=[0.0, 0.0, 0.2],
111 init_rot=[90.0, 0.0, 0.0],
112 )
113 )
114
115 print("[INFO]: Scene setup complete!")
116 print(f"[INFO]: Running simulation with {args.num_envs} environment(s)")
117 print("[INFO]: Press Ctrl+C to stop the simulation")
118
119 # Open window when the scene has been set up
120 if not args.headless:
121 sim.open_window()
122
123 if args.headless and not args.viser:
124 if not sim.start_window_record(
125 save_path=args.record_save_path,
126 fps=args.record_fps,
127 max_memory=2048,
128 video_prefix="create_scene_headless",
129 look_at=((2.6, -2.2, 1.6), (0.0, 0.0, 0.45), (0.0, 0.0, 1.0)),
130 ):
131 raise RuntimeError("Failed to start headless recording")
132 print("[INFO]: Headless recording enabled.")
133 print(
134 "[INFO]: The output path is reported by `SimulationManager.start_window_record()`."
135 )
136 print(f"[INFO]: Running {args.record_steps} steps before exporting the video")
137
138 # Run the simulation
139 run_simulation(
140 sim,
141 max_steps=args.record_steps if args.headless else None,
142 )
143
144
145def run_simulation(
146 sim: SimulationManager,
147 max_steps: int | None = None,
148) -> None:
149 """Run the simulation loop.
150
151 Args:
152 sim: The SimulationManager instance to run.
153 max_steps: Optional maximum number of simulation steps to execute.
154 """
155
156 # Initialize GPU physics if using CUDA
157 if sim.is_use_gpu_physics:
158 sim.init_gpu_physics()
159
160 step_count = 0
161
162 try:
163 last_time = time.time()
164 last_step = 0
165 while True:
166 # Update physics simulation
167 sim.update(step=1)
168 step_count += 1
169
170 # Print FPS every second
171 if step_count % 100 == 0:
172 current_time = time.time()
173 elapsed = current_time - last_time
174 fps = (
175 sim.num_envs * (step_count - last_step) / elapsed
176 if elapsed > 0
177 else 0
178 )
179 print(f"[INFO]: Simulation step: {step_count}, FPS: {fps:.2f}")
180 last_time = current_time
181 last_step = step_count
182
183 if max_steps is not None and step_count >= max_steps:
184 print(f"[INFO]: Reached {max_steps} steps. Stopping simulation...")
185 break
186
187 except KeyboardInterrupt:
188 print("\n[INFO]: Stopping simulation...")
189 finally:
190 if sim.is_window_recording():
191 sim.stop_window_record()
192 sim.wait_window_record_saves()
193
194 # Clean up resources
195 sim.destroy()
196
197
198if __name__ == "__main__":
199 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, including --viser and its update-rate/server options, 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 before exiting in headless recording " "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,
visualization=visualization_cfg_from_args(args),
)
# Create the simulation instance
sim = SimulationManager(sim_cfg)
Physics advances only when the caller invokes SimulationManager.update().
Each call executes the requested number of physics steps; sleeping, waiting for
input, and refreshing visualization do not advance simulation time. Interactive
applications use an explicit update loop with optional wall-clock pacing.
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 without --viser, 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 and not args.viser:
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")
Browser visualization#
Pass --viser to run headlessly and inspect the live scene in a browser:
python scripts/tutorials/sim/create_scene.py --viser
The endpoint is printed in the terminal and defaults to
http://127.0.0.1:8080. The browser contains the rigid cube, the chair’s
complete multi-segment mesh, and a ground grid with 1 m cells. Assets added or
removed after the server starts are refreshed on the next simulation update.
Use --viser-env-ids for selected parallel environments and
--viser-fps to limit pose updates:
python scripts/tutorials/sim/create_scene.py \
--num_envs 4 \
--viser \
--viser-env-ids 0 2 \
--viser-fps 15
See Browser visualization with Viser for the complete object support matrix, server settings, telemetry, and remote-access guidance.
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,
) -> 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. Stopping simulation...")
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.
except KeyboardInterrupt:
print("\n[INFO]: Stopping simulation...")
finally:
if sim.is_window_recording():
sim.stop_window_record()
sim.wait_window_record_saves()
# Clean up resources
sim.destroy()
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
Browser visualization with Viser — Configure browser visualization
Creating a Basic Environment — Create your first Gymnasium environment
Simulation Manager — Full SimulationManager API reference