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.plyfile written to the system temp directorycfg.ClothPhysicalAttributesCfg— material parameters:mass— total mass of the cloth panel (kg)youngs/poissons— elastic stiffness and compressibilitythickness— collision thickness of the cloth surfacebending_stiffness/bending_damping— resistance to and dissipation of bending motiondynamic_friction— friction between the cloth and other objectsmin_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 dimensionsbody_type="dynamic"— the box responds to physics; change to"static"for a fixed obstaclestatic_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.