Creating a cloth simulation#

This tutorial shows how to create a cloth simulation using SimulationManager. It covers procedurally generating a grid mesh, configuring a deformable cloth object, adding a rigid body for interaction, and running the simulation loop.

The Code#

The tutorial corresponds to the create_cloth.py script in the scripts/tutorials/sim directory.

Code for create_cloth.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, lighting, and sensors.
 20"""
 21
 22from __future__ import annotations
 23
 24import argparse
 25import os
 26import tempfile
 27import time
 28import torch
 29import open3d as o3d
 30from dexsim.utility.path import get_resources_data_path
 31from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
 32from embodichain.lab.gym.utils.gym_utils import add_env_launcher_args_to_parser
 33from embodichain.lab.visualization import visualization_cfg_from_args
 34from embodichain.lab.sim.cfg import (
 35    RenderCfg,
 36    RigidObjectCfg,
 37    RigidBodyAttributesCfg,
 38    ClothObjectCfg,
 39    ClothPhysicalAttributesCfg,
 40)
 41from embodichain.lab.sim.shapes import MeshCfg, CubeCfg
 42from embodichain.lab.sim.objects import ClothObject
 43
 44
 45def create_2d_grid_mesh(width: float, height: float, nx: int = 1, ny: int = 1):
 46    """Create a flat rectangle in the XY plane centered at `origin`.
 47
 48    The rectangle is subdivided into an `nx` by `ny` grid (cells) and
 49    triangulated. `nx=1, ny=1` yields the simple two-triangle rectangle.
 50
 51    Returns an vertices and triangles.
 52    """
 53    w = float(width)
 54    h = float(height)
 55    if nx < 1 or ny < 1:
 56        raise ValueError("nx and ny must be >= 1")
 57
 58    # Vectorized vertex positions using PyTorch
 59    x_lin = torch.linspace(-w / 2.0, w / 2.0, steps=nx + 1, dtype=torch.float64)
 60    y_lin = torch.linspace(-h / 2.0, h / 2.0, steps=ny + 1, dtype=torch.float64)
 61    yy, xx = torch.meshgrid(y_lin, x_lin)  # shapes: (ny+1, nx+1)
 62    xx_flat = xx.reshape(-1)
 63    yy_flat = yy.reshape(-1)
 64    zz_flat = torch.full_like(xx_flat, 0, dtype=torch.float64)
 65    verts = torch.stack([xx_flat, yy_flat, zz_flat], dim=1)  # (Nverts, 3)
 66
 67    # Vectorized triangle indices
 68    idx = torch.arange((nx + 1) * (ny + 1), dtype=torch.int64).reshape(ny + 1, nx + 1)
 69    v0 = idx[:-1, :-1].reshape(-1)
 70    v1 = idx[:-1, 1:].reshape(-1)
 71    v2 = idx[1:, :-1].reshape(-1)
 72    v3 = idx[1:, 1:].reshape(-1)
 73    tri1 = torch.stack([v0, v1, v3], dim=1)
 74    tri2 = torch.stack([v0, v3, v2], dim=1)
 75    faces = torch.cat([tri1, tri2], dim=0).to(torch.int32)
 76    return verts, faces
 77
 78
 79def main():
 80    """Main function to create and run the simulation scene."""
 81
 82    # Parse command line arguments
 83    parser = argparse.ArgumentParser(
 84        description="Create a simulation scene with SimulationManager"
 85    )
 86    add_env_launcher_args_to_parser(parser)
 87    args = parser.parse_args()
 88
 89    # Configure the simulation
 90    sim_cfg = SimulationManagerCfg(
 91        width=1920,
 92        height=1080,
 93        headless=True,
 94        num_envs=args.num_envs,
 95        physics_dt=1.0 / 100.0,  # Physics timestep (100 Hz)
 96        sim_device="cuda",  # soft simulation only supports cuda device
 97        render_cfg=RenderCfg(renderer=args.renderer),
 98        visualization=visualization_cfg_from_args(args),
 99    )
100
101    # Create the simulation instance
102    sim = SimulationManager(sim_cfg)
103
104    print("[INFO]: Scene setup complete!")
105
106    cloth_verts, cloth_faces = create_2d_grid_mesh(width=0.3, height=0.3, nx=12, ny=12)
107    cloth_mesh = o3d.geometry.TriangleMesh(
108        vertices=o3d.utility.Vector3dVector(cloth_verts.to("cpu").numpy()),
109        triangles=o3d.utility.Vector3iVector(cloth_faces.to("cpu").numpy()),
110    )
111    cloth_save_path = os.path.join(tempfile.gettempdir(), "cloth_mesh.ply")
112    o3d.io.write_triangle_mesh(cloth_save_path, cloth_mesh)
113    # add cloth to the scene
114    cloth = sim.add_cloth_object(
115        cfg=ClothObjectCfg(
116            uid="cloth",
117            shape=MeshCfg(fpath=cloth_save_path),
118            init_pos=[0.5, 0.0, 0.3],
119            init_rot=[0, 0, 0],
120            physical_attr=ClothPhysicalAttributesCfg(
121                mass=0.01,
122                youngs=1e9,
123                poissons=0.4,
124                thickness=0.04,
125                bending_stiffness=0.01,
126                bending_damping=0.1,
127                dynamic_friction=0.95,
128                min_position_iters=30,
129            ),
130        )
131    )
132    padding_box_cfg = RigidObjectCfg(
133        uid="padding_box",
134        shape=CubeCfg(
135            size=[0.1, 0.1, 0.06],
136        ),
137        attrs=RigidBodyAttributesCfg(
138            mass=1.0,
139            static_friction=0.95,
140            dynamic_friction=0.9,
141            restitution=0.01,
142            min_position_iters=32,
143            min_velocity_iters=8,
144        ),
145        body_type="dynamic",
146        init_pos=[0.5, 0.0, 0.04],
147        init_rot=[0.0, 0.0, 0.0],
148    )
149    padding_box = sim.add_rigid_object(cfg=padding_box_cfg)
150    print("[INFO]: Add soft object complete!")
151
152    # Open window when the scene has been set up
153    if not args.headless:
154        sim.open_window()
155
156    print(f"[INFO]: Running simulation with {args.num_envs} environment(s)")
157    print("[INFO]: Press Ctrl+C to stop the simulation")
158
159    # Run the simulation
160    run_simulation(sim, cloth)
161
162
163def run_simulation(sim: SimulationManager, cloth: ClothObject) -> None:
164    """Run the simulation loop.
165
166    Args:
167        sim: The SimulationManager instance to run
168        soft_obj: soft object
169    """
170
171    # Initialize GPU physics
172    sim.init_gpu_physics()
173
174    step_count = 0
175
176    try:
177        last_time = time.time()
178        last_step = 0
179        while True:
180            # Update physics simulation
181            sim.update(step=1)
182            step_count += 1
183
184            # Print FPS every second
185            if step_count % 100 == 0:
186                current_time = time.time()
187                elapsed = current_time - last_time
188                fps = (
189                    sim.num_envs * (step_count - last_step) / elapsed
190                    if elapsed > 0
191                    else 0
192                )
193                print(f"[INFO]: Simulation step: {step_count}, FPS: {fps:.2f}")
194                last_time = current_time
195                last_step = step_count
196                if step_count % 500 == 0:
197                    cloth.reset()
198
199    except KeyboardInterrupt:
200        print("\n[INFO]: Stopping simulation...")
201    finally:
202        # Clean up resources
203        sim.destroy()
204        print("[INFO]: Simulation terminated successfully")
205
206
207if __name__ == "__main__":
208    main()

The Code Explained#

Generating the cloth mesh#

Unlike the soft-body tutorial where a pre-existing mesh file is loaded, cloth objects are typically defined by a flat 2-D surface. The helper function create_2d_grid_mesh generates a rectangular grid mesh procedurally using PyTorch, then saves it to a temporary .ply file via Open3D so that the simulation can load it.

Loading a mesh from file also works for cloth objects, but generating a grid in code allows for easy customization of the cloth dimensions and resolution.

The function accepts the physical dimensions (width, height) and the number of subdivisions (nx, ny). A finer grid gives more cloth-like wrinkle detail at the cost of simulation performance.

def create_2d_grid_mesh(width: float, height: float, nx: int = 1, ny: int = 1):
    """Create a flat rectangle in the XY plane centered at `origin`.

    The rectangle is subdivided into an `nx` by `ny` grid (cells) and
    triangulated. `nx=1, ny=1` yields the simple two-triangle rectangle.

    Returns an vertices and triangles.
    """
    w = float(width)
    h = float(height)
    if nx < 1 or ny < 1:
        raise ValueError("nx and ny must be >= 1")

    # Vectorized vertex positions using PyTorch
    x_lin = torch.linspace(-w / 2.0, w / 2.0, steps=nx + 1, dtype=torch.float64)
    y_lin = torch.linspace(-h / 2.0, h / 2.0, steps=ny + 1, dtype=torch.float64)
    yy, xx = torch.meshgrid(y_lin, x_lin)  # shapes: (ny+1, nx+1)
    xx_flat = xx.reshape(-1)
    yy_flat = yy.reshape(-1)
    zz_flat = torch.full_like(xx_flat, 0, dtype=torch.float64)
    verts = torch.stack([xx_flat, yy_flat, zz_flat], dim=1)  # (Nverts, 3)

    # Vectorized triangle indices
    idx = torch.arange((nx + 1) * (ny + 1), dtype=torch.int64).reshape(ny + 1, nx + 1)
    v0 = idx[:-1, :-1].reshape(-1)
    v1 = idx[:-1, 1:].reshape(-1)
    v2 = idx[1:, :-1].reshape(-1)
    v3 = idx[1:, 1:].reshape(-1)
    tri1 = torch.stack([v0, v1, v3], dim=1)
    tri2 = torch.stack([v0, v3, v2], dim=1)
    faces = torch.cat([tri1, tri2], dim=0).to(torch.int32)
    return verts, faces

Configuring the simulation#

The simulation environment is configured with SimulationManagerCfg. For cloth simulation the device must be set to cuda. The arena_space parameter controls the spacing between parallel environments so that objects in neighboring environments do not overlap.

    # Configure the simulation
    sim_cfg = SimulationManagerCfg(
        width=1920,
        height=1080,
        headless=True,
        num_envs=args.num_envs,
        physics_dt=1.0 / 100.0,  # Physics timestep (100 Hz)
        sim_device="cuda",  # soft simulation only supports cuda device
        render_cfg=RenderCfg(renderer=args.renderer),
        visualization=visualization_cfg_from_args(args),
    )

    # Create the simulation instance
    sim = SimulationManager(sim_cfg)

    print("[INFO]: Scene setup complete!")

Adding a cloth object to the scene#

The grid mesh generated earlier is saved to disk and then passed to SimulationManager.add_cloth_object(). The physical properties of the cloth are controlled through cfg.ClothObjectCfg together with cfg.ClothPhysicalAttributesCfg:

  • cfg.MeshCfg — references the .ply file written to the system temp directory

  • cfg.ClothPhysicalAttributesCfg — material parameters:

    • mass — total mass of the cloth panel (kg)

    • youngs / poissons — elastic stiffness and compressibility

    • thickness — collision thickness of the cloth surface

    • bending_stiffness / bending_damping — resistance to and dissipation of bending motion

    • dynamic_friction — friction between the cloth and other objects

    • min_position_iters — solver iteration count for position constraints

    cloth_verts, cloth_faces = create_2d_grid_mesh(width=0.3, height=0.3, nx=12, ny=12)
    cloth_mesh = o3d.geometry.TriangleMesh(
        vertices=o3d.utility.Vector3dVector(cloth_verts.to("cpu").numpy()),
        triangles=o3d.utility.Vector3iVector(cloth_faces.to("cpu").numpy()),
    )
    cloth_save_path = os.path.join(tempfile.gettempdir(), "cloth_mesh.ply")
    o3d.io.write_triangle_mesh(cloth_save_path, cloth_mesh)
    # add cloth to the scene
    cloth = sim.add_cloth_object(
        cfg=ClothObjectCfg(
            uid="cloth",
            shape=MeshCfg(fpath=cloth_save_path),
            init_pos=[0.5, 0.0, 0.3],
            init_rot=[0, 0, 0],
            physical_attr=ClothPhysicalAttributesCfg(
                mass=0.01,
                youngs=1e9,
                poissons=0.4,
                thickness=0.04,
                bending_stiffness=0.01,
                bending_damping=0.1,
                dynamic_friction=0.95,
                min_position_iters=30,
            ),
        )
    )
    padding_box_cfg = RigidObjectCfg(

Adding a rigid body for interaction#

A small cubic rigid body (padding_box) is placed beneath the cloth so the cloth drapes over it. It is added with SimulationManager.add_rigid_object() using cfg.RigidObjectCfg and cfg.RigidBodyAttributesCfg:

  • cfg.CubeCfg — defines the box dimensions

  • body_type="dynamic" — the box responds to physics; change to "static" for a fixed obstacle

  • static_friction / dynamic_friction — surface friction keeps the cloth from sliding off too easily

    padding_box_cfg = RigidObjectCfg(
        uid="padding_box",
        shape=CubeCfg(
            size=[0.1, 0.1, 0.06],
        ),
        attrs=RigidBodyAttributesCfg(
            mass=1.0,
            static_friction=0.95,
            dynamic_friction=0.9,
            restitution=0.01,
            min_position_iters=32,
            min_velocity_iters=8,
        ),
        body_type="dynamic",
        init_pos=[0.5, 0.0, 0.04],
        init_rot=[0.0, 0.0, 0.0],
    )
    padding_box = sim.add_rigid_object(cfg=padding_box_cfg)
    print("[INFO]: Add soft object complete!")

The Code Execution#

To run the script and see the result, execute the following command:

python scripts/tutorials/sim/create_cloth.py

A window should appear showing a cloth panel falling and draping over a small rigid box. To stop the simulation, close the window or press Ctrl+C in the terminal.

You can also pass arguments to customise the simulation. For example, to run in headless mode with n parallel environments:

python scripts/tutorials/sim/create_cloth.py --headless --num_envs <n>

To view the simulated cloth surface through Viser:

python scripts/tutorials/sim/create_cloth.py \
    --viser \
    --viser-soft-body-fps 5

The browser mesh uses the cloth’s physical vertices and a welded mapping of the source triangles, so its topology matches the simulated cloth surface. Deformable updates are sampled separately from rigid-body poses; adjust --viser-soft-body-fps to balance smoothness and browser/upload cost.

See Browser visualization with Viser for details.

Now that we have a basic understanding of how to create a cloth scene, let’s move on to more advanced topics.