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 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.set_defaults(device="cuda", physics="newton")
 36    return parser
 37
 38
 39if __name__ == "__main__":
 40    # Parse before importing optional simulation/planning dependencies.
 41    _cli_args = build_parser().parse_args()
 42
 43
 44import os
 45import tempfile
 46
 47import open3d as o3d
 48import torch
 49from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
 50from embodichain.lab.visualization import visualization_cfg_from_args
 51from embodichain.lab.sim.cfg import (
 52    SurfaceElementPropertiesCfg,
 53    MassPropertiesCfg,
 54    NewtonCollisionPipelineCfg,
 55    NewtonPhysicsCfg,
 56    RenderCfg,
 57    SurfaceDeformableObjectCfg,
 58    SurfaceDeformablePhysicsCfg,
 59    RigidBodyMaterialCfg,
 60    RigidBodyPhysicsCfg,
 61    RigidObjectCfg,
 62)
 63from embodichain.lab.sim.shapes import MeshCfg, CubeCfg
 64from embodichain.lab.sim.objects import SurfaceDeformableObject
 65
 66
 67def create_2d_grid_mesh(
 68    width: float, height: float, nx: int = 1, ny: int = 1
 69) -> tuple[torch.Tensor, torch.Tensor]:
 70    """Create a flat rectangle in the XY plane centered at `origin`.
 71
 72    The rectangle is subdivided into an `nx` by `ny` grid (cells) and
 73    triangulated. `nx=1, ny=1` yields the simple two-triangle rectangle.
 74
 75    Returns an vertices and triangles.
 76    """
 77    w = float(width)
 78    h = float(height)
 79    if nx < 1 or ny < 1:
 80        raise ValueError("nx and ny must be >= 1")
 81
 82    # Vectorized vertex positions using PyTorch
 83    x_lin = torch.linspace(-w / 2.0, w / 2.0, steps=nx + 1, dtype=torch.float64)
 84    y_lin = torch.linspace(-h / 2.0, h / 2.0, steps=ny + 1, dtype=torch.float64)
 85    yy, xx = torch.meshgrid(y_lin, x_lin, indexing="ij")
 86    xx_flat = xx.reshape(-1)
 87    yy_flat = yy.reshape(-1)
 88    zz_flat = torch.full_like(xx_flat, 0, dtype=torch.float64)
 89    verts = torch.stack([xx_flat, yy_flat, zz_flat], dim=1)  # (Nverts, 3)
 90
 91    # Vectorized triangle indices
 92    idx = torch.arange((nx + 1) * (ny + 1), dtype=torch.int64).reshape(ny + 1, nx + 1)
 93    v0 = idx[:-1, :-1].reshape(-1)
 94    v1 = idx[:-1, 1:].reshape(-1)
 95    v2 = idx[1:, :-1].reshape(-1)
 96    v3 = idx[1:, 1:].reshape(-1)
 97    tri1 = torch.stack([v0, v1, v3], dim=1)
 98    tri2 = torch.stack([v0, v3, v2], dim=1)
 99    faces = torch.cat([tri1, tri2], dim=0).to(torch.int32)
100    return verts, faces
101
102
103def main(args: argparse.Namespace | None = None) -> None:
104    """Main function to create and run the simulation scene."""
105
106    # Parse command line arguments
107    parser = build_parser()
108    if args is None:
109        args = parser.parse_args()
110    if args.physics != "newton":
111        parser.error("Cloth requires --physics newton.")
112    if not str(args.device).startswith("cuda"):
113        parser.error("Cloth requires a CUDA device.")
114
115    # Configure the simulation
116    sim_cfg = SimulationManagerCfg(
117        width=1920,
118        height=1080,
119        headless=args.headless,
120        num_envs=args.num_envs,
121        arena_space=args.arena_space,
122        gpu_id=args.gpu_id,
123        physics_dt=1.0 / 100.0,  # Physics timestep (100 Hz)
124        device=args.device,
125        render_cfg=RenderCfg(renderer=args.renderer),
126        physics_cfg=NewtonPhysicsCfg(
127            num_substeps=4,
128            solver_cfg={
129                "solver_type": "vbd",
130                "iterations": 5,
131                "particle_enable_self_contact": False,
132                "particle_self_contact_radius": 0.002,
133                "particle_self_contact_margin": 0.002,
134                "particle_enable_tile_solve": True,
135                "soft_contact_ke": 1.0e4,
136                "soft_contact_kd": 1.0e-2,
137                "soft_contact_mu": 0.8,
138            },
139            collision_cfg=NewtonCollisionPipelineCfg(
140                soft_contact_margin=0.002,
141            ),
142        ),
143        visualization=visualization_cfg_from_args(args),
144    )
145
146    # Create the simulation instance
147    sim = SimulationManager(sim_cfg)
148
149    print("[INFO]: Scene setup complete!")
150
151    cloth_verts, cloth_faces = create_2d_grid_mesh(width=0.3, height=0.3, nx=12, ny=12)
152    cloth_mesh = o3d.geometry.TriangleMesh(
153        vertices=o3d.utility.Vector3dVector(cloth_verts.to("cpu").numpy()),
154        triangles=o3d.utility.Vector3iVector(cloth_faces.to("cpu").numpy()),
155    )
156    cloth_save_path = os.path.join(tempfile.gettempdir(), "cloth_mesh.ply")
157    o3d.io.write_triangle_mesh(cloth_save_path, cloth_mesh)
158    # add cloth to the scene
159    cloth = sim.add_deformable_object(
160        cfg=SurfaceDeformableObjectCfg(
161            uid="cloth",
162            shape=MeshCfg(fpath=cloth_save_path),
163            init_pos=[0.5, 0.0, 0.8],
164            init_rot=[0, 0, 0],
165            # The grid spacing is 0.025 m, so avoid Newton's much larger
166            # 0.1 m default particle radius for this small cloth mesh.
167            particle_radius=0.01,
168            attrs=SurfaceDeformablePhysicsCfg(
169                density=0.02,
170                surface_props=SurfaceElementPropertiesCfg(
171                    tri_ke=2.0e3,
172                    tri_ka=2.0e3,
173                    tri_kd=0.1,
174                    edge_ke=2.0,
175                    edge_kd=0.1,
176                ),
177            ),
178        )
179    )
180    padding_box_cfg = RigidObjectCfg(
181        uid="padding_box",
182        shape=CubeCfg(
183            size=[0.1, 0.1, 0.06],
184        ),
185        attrs=RigidBodyPhysicsCfg(
186            mass_props=MassPropertiesCfg(mass=1.0),
187            material_props=RigidBodyMaterialCfg(
188                static_friction=0.95,
189                dynamic_friction=0.9,
190                restitution=0.01,
191            ),
192        ),
193        body_type="dynamic",
194        init_pos=[0.5, 0.0, 0.04],
195        init_rot=[0.0, 0.0, 0.0],
196    )
197    sim.add_rigid_object(cfg=padding_box_cfg)
198    print("[INFO]: Add cloth object complete!")
199
200    sim.prepare()
201
202    # Open window when the scene has been set up
203    if not args.headless:
204        sim.open_window()
205
206    print(f"[INFO]: Running simulation with {args.num_envs} environment(s)")
207    print("[INFO]: Press Ctrl+C to stop the simulation")
208
209    # Run the simulation
210    run_simulation(sim, cloth)
211
212
213def run_simulation(sim: SimulationManager, cloth: SurfaceDeformableObject) -> None:
214    """Run the simulation loop.
215
216    Args:
217        sim: The SimulationManager instance to run
218        cloth: The cloth object to simulate.
219    """
220
221    try:
222        step_count = 0
223        last_time = time.perf_counter()
224        last_step = 0
225        while True:
226            # Update physics simulation
227            sim.update(step=1)
228            step_count += 1
229
230            if step_count % 100 == 0:
231                current_time = time.perf_counter()
232                elapsed = current_time - last_time
233                fps = (
234                    sim.num_envs * (step_count - last_step) / elapsed
235                    if elapsed > 0.0
236                    else 0.0
237                )
238                print(f"[INFO]: Simulation step: {step_count}, FPS: {fps:.2f}")
239                last_time = current_time
240                last_step = step_count
241
242    except KeyboardInterrupt:
243        print("\n[INFO]: Stopping simulation...")
244    finally:
245        # Clean up resources
246        sim.destroy()
247        print("[INFO]: Simulation terminated successfully")
248
249
250if __name__ == "__main__":
251    main(_cli_args)

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
) -> tuple[torch.Tensor, torch.Tensor]:
    """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, indexing="ij")
    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=args.headless,
        num_envs=args.num_envs,
        arena_space=args.arena_space,
        gpu_id=args.gpu_id,
        physics_dt=1.0 / 100.0,  # Physics timestep (100 Hz)
        device=args.device,
        render_cfg=RenderCfg(renderer=args.renderer),
        physics_cfg=NewtonPhysicsCfg(
            num_substeps=4,
            solver_cfg={
                "solver_type": "vbd",
                "iterations": 5,
                "particle_enable_self_contact": False,
                "particle_self_contact_radius": 0.002,
                "particle_self_contact_margin": 0.002,
                "particle_enable_tile_solve": True,
                "soft_contact_ke": 1.0e4,
                "soft_contact_kd": 1.0e-2,
                "soft_contact_mu": 0.8,
            },
            collision_cfg=NewtonCollisionPipelineCfg(
                soft_contact_margin=0.002,
            ),
        ),
        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_deformable_object(). The physical properties of the cloth are controlled through cfg.SurfaceDeformableObjectCfg together with cfg.SurfaceDeformablePhysicsCfg:

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

  • cfg.SurfaceDeformablePhysicsCfg — material parameters:

    • density — surface density (kg/m²)

    • surface_props.tri_ke / tri_ka — triangle elastic and area stiffness

    • surface_props.tri_kd — triangle damping

    • surface_props.edge_ke / edge_kd — bending stiffness and damping

    • add_springs / spring_ke / spring_kd — optional mesh springs

The object stores these properties in attrs. Set particle_radius on SurfaceDeformableObjectCfg to control the particle contact radius.

    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_deformable_object(
        cfg=SurfaceDeformableObjectCfg(
            uid="cloth",
            shape=MeshCfg(fpath=cloth_save_path),
            init_pos=[0.5, 0.0, 0.8],
            init_rot=[0, 0, 0],
            # The grid spacing is 0.025 m, so avoid Newton's much larger
            # 0.1 m default particle radius for this small cloth mesh.
            particle_radius=0.01,
            attrs=SurfaceDeformablePhysicsCfg(
                density=0.02,
                surface_props=SurfaceElementPropertiesCfg(
                    tri_ke=2.0e3,
                    tri_ka=2.0e3,
                    tri_kd=0.1,
                    edge_ke=2.0,
                    edge_kd=0.1,
                ),
            ),
        )
    )
    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.RigidBodyPhysicsCfg:

  • 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=RigidBodyPhysicsCfg(
            mass_props=MassPropertiesCfg(mass=1.0),
            material_props=RigidBodyMaterialCfg(
                static_friction=0.95,
                dynamic_friction=0.9,
                restitution=0.01,
            ),
        ),
        body_type="dynamic",
        init_pos=[0.5, 0.0, 0.04],
        init_rot=[0.0, 0.0, 0.0],
    )
    sim.add_rigid_object(cfg=padding_box_cfg)
    print("[INFO]: Add cloth 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.