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
 26from embodichain.cli.sim import add_sim_args_to_parser
 27
 28
 29def build_parser() -> argparse.ArgumentParser:
 30    """Build CLI options without initializing simulation resources."""
 31    parser = argparse.ArgumentParser(
 32        description="Create a simulation scene with SimulationManager"
 33    )
 34    add_sim_args_to_parser(parser)
 35    parser.add_argument(
 36        "--max_steps",
 37        type=int,
 38        default=1000,
 39        help=(
 40            "Number of simulation steps before exiting in headless recording " "mode."
 41        ),
 42    )
 43    parser.add_argument(
 44        "--record-fps",
 45        type=int,
 46        default=20,
 47        help="Output video FPS for headless recording.",
 48    )
 49    parser.add_argument(
 50        "--record-save-path",
 51        type=str,
 52        default=None,
 53        help="Optional mp4 output path for headless recording.",
 54    )
 55    return parser
 56
 57
 58if __name__ == "__main__":
 59    # Parse before importing optional simulation/planning dependencies.
 60    _cli_args = build_parser().parse_args()
 61
 62
 63from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
 64from embodichain.lab.sim.cfg import (
 65    MassPropertiesCfg,
 66    RenderCfg,
 67    RigidBodyMaterialCfg,
 68    RigidBodyPhysicsCfg,
 69    physics_cfg_for_backend,
 70)
 71from embodichain.lab.sim.shapes import CubeCfg, MeshCfg, MeshCollisionCfg
 72from embodichain.lab.sim.objects import RigidObject, RigidObjectCfg
 73from embodichain.lab.visualization import visualization_cfg_from_args
 74from embodichain.data import get_data_path
 75
 76
 77def main(args: argparse.Namespace | None = None) -> None:
 78    """Main function to create and run the simulation scene."""
 79
 80    # Parse command line arguments
 81    parser = build_parser()
 82    if args is None:
 83        args = parser.parse_args()
 84    # Configure the simulation
 85    sim_cfg = SimulationManagerCfg(
 86        width=1920,
 87        height=1080,
 88        headless=True,
 89        physics_dt=1.0 / 100.0,  # Physics timestep (100 Hz)
 90        device=args.device,
 91        physics_cfg=physics_cfg_for_backend(args.physics),
 92        render_cfg=RenderCfg(
 93            renderer=args.renderer,
 94        ),
 95        num_envs=args.num_envs,
 96        arena_space=3.0,
 97        visualization=visualization_cfg_from_args(args),
 98    )
 99
100    # Create the simulation instance
101    sim = SimulationManager(sim_cfg)
102
103    # Add cube object to the scene
104    cube: RigidObject = sim.add_rigid_object(
105        cfg=RigidObjectCfg(
106            uid="cube",
107            shape=CubeCfg(size=[0.1, 0.1, 0.1]),
108            body_type="dynamic",
109            attrs=RigidBodyPhysicsCfg(
110                mass_props=MassPropertiesCfg(mass=0.1),
111                material_props=RigidBodyMaterialCfg(
112                    dynamic_friction=0.5,
113                    static_friction=0.5,
114                    restitution=0.1,
115                ),
116            ),
117            init_pos=[0, 0.0, 1.0],
118        )
119    )
120
121    # Add chair object to the scene
122    path = get_data_path("Chair/chair.glb")
123    chair: RigidObject = sim.add_rigid_object(
124        cfg=RigidObjectCfg(
125            uid="chair",
126            shape=MeshCfg(
127                fpath=path,
128                collision=MeshCollisionCfg(
129                    approximation="convex_decomposition",
130                    max_hulls=32,
131                    acd_method="coacd",
132                ),
133            ),
134            body_type="dynamic",
135            attrs=RigidBodyPhysicsCfg(
136                mass_props=MassPropertiesCfg(mass=10.0),
137            ),
138            body_scale=[0.5, 0.5, 0.5],
139            init_pos=[0.0, 0.0, 0.51],
140            init_rot=[0.0, 0.0, 0.0],
141        )
142    )
143
144    # Materialize the complete initial scene before exposing it to the viewer.
145    sim.prepare()
146
147    print("[INFO]: Scene setup complete!")
148    print(f"[INFO]: Running simulation with {args.num_envs} environment(s)")
149    print("[INFO]: Press Ctrl+C to stop the simulation")
150
151    # Open window when the scene has been set up
152    if not args.headless:
153        sim.open_window()
154
155    if args.headless and not args.viser:
156        if not sim.start_window_record(
157            save_path=args.record_save_path,
158            fps=args.record_fps,
159            max_memory=2048,
160            video_prefix="create_scene_headless",
161            look_at=((2.6, -2.2, 1.6), (0.0, 0.0, 0.45), (0.0, 0.0, 1.0)),
162        ):
163            raise RuntimeError("Failed to start headless recording")
164        print("[INFO]: Headless recording enabled.")
165        print(
166            "[INFO]: The output path is reported by `SimulationManager.start_window_record()`."
167        )
168        print(f"[INFO]: Running {args.max_steps} steps before exporting the video")
169
170    # Run the simulation
171    run_simulation(sim, max_steps=args.max_steps)
172
173
174def run_simulation(
175    sim: SimulationManager,
176    max_steps: int | None = None,
177) -> None:
178    """Run the simulation loop.
179
180    Args:
181        sim: The SimulationManager instance to run.
182        max_steps: Optional maximum number of simulation steps to execute.
183    """
184
185    step_count = 0
186
187    try:
188        last_time = time.time()
189        last_step = 0
190        while True:
191            # Update physics simulation
192            sim.update(step=1)
193            step_count += 1
194
195            if max_steps is not None and step_count >= max_steps:
196                break
197
198            # Print FPS every second
199            if step_count % 100 == 0:
200                current_time = time.time()
201                elapsed = current_time - last_time
202                fps = (
203                    sim.num_envs * (step_count - last_step) / elapsed
204                    if elapsed > 0
205                    else 0
206                )
207                print(f"[INFO]: Simulation step: {step_count}, FPS: {fps:.2f}")
208                last_time = current_time
209                last_step = step_count
210
211            if max_steps is not None and step_count >= max_steps:
212                print(f"[INFO]: Reached {max_steps} steps. Stopping simulation...")
213                break
214
215    except KeyboardInterrupt:
216        print("\n[INFO]: Stopping simulation...")
217    finally:
218        if sim.is_window_recording():
219            sim.stop_window_record()
220            sim.wait_window_record_saves()
221
222        # Clean up resources
223        sim.destroy()
224
225
226if __name__ == "__main__":
227    main(_cli_args)

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 --max_steps, --record-fps, and --record-save-path for headless recording. The script registers the underscore spelling --max_steps.

    # Parse command line arguments
    parser = build_parser()
    if args is None:
        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)
        device=args.device,
        physics_cfg=physics_cfg_for_backend(args.physics),
        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=RigidBodyPhysicsCfg(
                mass_props=MassPropertiesCfg(mass=0.1),
                material_props=RigidBodyMaterialCfg(
                    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,
                collision=MeshCollisionCfg(
                    approximation="convex_decomposition",
                    max_hulls=32,
                    acd_method="coacd",
                ),
            ),
            body_type="dynamic",
            attrs=RigidBodyPhysicsCfg(
                mass_props=MassPropertiesCfg(mass=10.0),
            ),
            body_scale=[0.5, 0.5, 0.5],
            init_pos=[0.0, 0.0, 0.51],
            init_rot=[0.0, 0.0, 0.0],
        )
    )

    # Materialize the complete initial scene before exposing it to the viewer.
    sim.prepare()

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

    step_count = 0

    try:
        last_time = time.time()
        last_step = 0
        while True:
            # Update physics simulation
            sim.update(step=1)
            step_count += 1

            if max_steps is not None and step_count >= max_steps:
                break

            # 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 \
    --max_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#