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.plyfile written to the system temp directorycfg.SurfaceDeformablePhysicsCfg— material parameters:density— surface density (kg/m²)surface_props.tri_ke/tri_ka— triangle elastic and area stiffnesssurface_props.tri_kd— triangle dampingsurface_props.edge_ke/edge_kd— bending stiffness and dampingadd_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 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=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.