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 setting SimulationManager.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#