Boxes chain cloth soft
Coupling
Rigid boxes and a pendulum chain meet a cloth sheet and soft bodies.
steel_vbd preset (the default), recordedon an NVIDIA RTX PRO 6000 Blackwell.Run it
cd examples# the default preset (steel_vbd), then the othersuv run shelf play boxes_chain_cloth_softuv run shelf play boxes_chain_cloth_soft --solver vbduv run shelf play boxes_chain_cloth_soft --solver mujoco_vbduv run shelf play boxes_chain_cloth_soft --solver kamino_vbd# change solver settings liveuv run shelf tune boxes_chain_cloth_softSource: examples/shelf/09_coupling/boxes_chain_cloth_soft/ · ported from newton/examples/multiphysics/example_mujoco_vbd_coupled_solver.py @ skild-ai/newton 7565a26b1
Presets
| Preset | Solver | Status | Checked |
|---|---|---|---|
steel_vbddefault | coupled | works | 2026-11-04, newton 7565a26b1, NVIDIA GeForce RTX 3090 |
vbd | vbd | works | 2026-10-28, newton 7565a26b1, NVIDIA RTX A6000 |
mujoco_vbd | coupled | works | 2026-10-28, newton 7565a26b1, NVIDIA RTX A6000 |
kamino_vbd | coupled | works | 2026-10-28, newton 7565a26b1, NVIDIA RTX A6000 |
Solvers with no row are untested on this example. The full settings are inpresets.toml.
Code
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers# SPDX-License-Identifier: Apache-2.0# Modified for Steel Shelf: preset-driven execution and shared runner integration.
############################################################################ Example Rigid-VBD Coupled Solver## Rigid boxes and an articulated pendulum chain (driven by MuJoCo, Kamino or# Steel) interact with a cloth sheet and several soft bodies (simulated# by VBD). Contact forces from the deformables are fed back to the# rigid bodies, causing them to bounce and settle realistically.## This example builds rigid/VBD proxy coupling directly through# ``SolverCoupledProxy`` so the generic proxy path exercises the default API.## Pass ``--solver vbd`` to run the same scene with a single VBD solver# (no coupling) as a reference baseline.## Command: python -m shelf run boxes_chain_cloth_soft# python -m shelf run boxes_chain_cloth_soft --solver steel_vbd# python -m shelf run boxes_chain_cloth_soft --solver vbd############################################################################
from __future__ import annotations
from collections.abc import Callable
import numpy as npimport warp as wpfrom newton.solvers.experimental.coupled import SolverCoupledProxy
import newtonimport newton.examplesfrom newton.solvers import SolverKamino, SolverMuJoCo, SolverSteelfrom common import presets
def _register_rigid_solver_custom_attributes(builder: newton.ModelBuilder, rigid_solver: str) -> None: if rigid_solver == "kamino": SolverKamino.register_custom_attributes(builder)
def _make_kamino_config(settings: dict[str, object]) -> SolverKamino.Config: # Start from SolverKamino.Config() (not Config.from_model) and apply the preset's # settings; nested tables set fields of the nested configs. config = SolverKamino.Config() for key, value in settings.items(): if isinstance(value, dict): for sub_key, sub_value in value.items(): setattr(getattr(config, key), sub_key, sub_value) else: setattr(config, key, value) return config
def _rigid_solver_entry_args(rigid_solver: str, settings: dict[str, object]): if rigid_solver == "kamino": return "kamino", SolverKamino, {"config": _make_kamino_config(settings)} if rigid_solver == "mujoco": return "mjc", SolverMuJoCo, dict(settings) if rigid_solver == "steel": return "steel", SolverSteel, dict(settings) raise ValueError(f"Unsupported rigid solver '{rigid_solver}'")
def _capture_frame_graph(model: newton.Model, simulate: Callable[[], None], *, enabled: bool = True): if not enabled: return None
with wp.ScopedDevice(model.device): with wp.ScopedCapture() as capture: simulate()
if capture.graph is None: raise RuntimeError(f"Graph capture failed on device {model.device}") return capture.graph
def _launch_frame_graph(model: newton.Model, graph) -> bool: if graph is None: return False
with wp.ScopedDevice(model.device): wp.capture_launch(graph) return True
class Example: def __init__(self, viewer, args): self.preset = presets.load(__file__) self.args = args self.viewer = viewer self.sim_time = 0.0 self.fps = self.preset.fps self.frame_dt = 1.0 / self.fps self.sim_substeps = self.preset.substeps self.sim_dt = self.frame_dt / self.sim_substeps # Presets: "<rigid>_vbd" couples a rigid solver with VBD; "vbd" is the pure-VBD baseline. self.use_coupled = self.preset.name != "vbd"
self.rigid_solver = self.preset.name.split("_")[0]
builder = newton.ModelBuilder() builder.default_shape_cfg.ke = 2.0e4 _register_rigid_solver_custom_attributes(builder, self.rigid_solver) builder.add_ground_plane()
# ---- Rigid bodies (free-floating + articulated) ---- rigid_body_start = builder.body_count self._emit_rigid_bodies(builder) self._emit_articulated_chain(builder) rigid_body_end = builder.body_count
# ---- Cloth ---- self._emit_cloth(builder)
# ---- Soft bodies (tetrahedral, owned by VBD) ---- self._emit_soft_bodies(builder)
# Color the mesh for VBD solver builder.color()
self.model = builder.finalize() newton.eval_fk(self.model, self.model.joint_q, self.model.joint_qd, self.model)
# Contact parameters self.model.soft_contact_ke = 1.0e5 self.model.soft_contact_mu = 0.5
self.rigid_body_start = rigid_body_start self.rigid_body_end = rigid_body_end self.build_solver()
# Simulation state self.state_0 = self.model.state() self.state_1 = self.model.state() self.collision_pipeline = newton.CollisionPipeline(self.model) self.contacts = self.collision_pipeline.contacts() self.control = self.model.control()
newton.examples.configure_coupled_view(self, args)
newton.eval_fk(self.model, self.model.joint_q, self.model.joint_qd, self.state_0)
self.capture()
def build_solver(self): """Build the solver and everything bound to its instance; the tuner calls this again to swap it.""" rigid_body_start = self.rigid_body_start rigid_body_end = self.rigid_body_end if self.use_coupled: # ---------- Coupled path: rigid solver + VBD ---------- coupling = self.preset.solver vbd_entry = self.preset.entry("vbd") rigid_name, rigid_solver, rigid_kwargs = _rigid_solver_entry_args( self.rigid_solver, self.preset.entry(self.rigid_solver).solver ) rigid_body_indices = wp.array(list(range(rigid_body_start, rigid_body_end)), dtype=int) vbd_body_indices = wp.array( [i for i in range(self.model.body_count) if i < rigid_body_start or i >= rigid_body_end], dtype=int, )
self.solver = SolverCoupledProxy( model=self.model, entries=[ SolverCoupledProxy.Entry( name=rigid_name, solver=lambda v: rigid_solver(model=v, **rigid_kwargs), bodies=[int(i) for i in rigid_body_indices.numpy()], joints=list(range(self.model.joint_count)), ), SolverCoupledProxy.Entry( name="vbd", solver=lambda v: vbd_entry.make_solver(v), bodies=[int(i) for i in vbd_body_indices.numpy()], particles=list(range(self.model.particle_count)), ), ], coupling=SolverCoupledProxy.Config( proxies=[ SolverCoupledProxy.Proxy( source=rigid_name, destination="vbd", bodies=[int(i) for i in rigid_body_indices.numpy()], mass_scale=coupling.mass_scale, mode=coupling.coupling_mode, collision_pipeline=lambda model: newton.examples.create_collision_pipeline( model, self.args ), collide_interval=coupling.collide_interval, ) ], iterations=coupling.proxy_iterations, ), )
else: # ---------- Pure-VBD path (reference baseline) ---------- self.solver = self.preset.make_solver(self.model)
def capture(self): self.graph = _capture_frame_graph(self.model, self.simulate)
def simulate(self): self.collision_pipeline.collide(self.state_0, self.contacts) for substep in range(self.sim_substeps): self.state_0.clear_forces() newton.examples.apply_coupled_viewer_forces(self, self.state_0) self.solver.step(self.state_0, self.state_1, self.control, self.contacts, self.sim_dt) if self.sim_substeps % 2 == 1 and substep == self.sim_substeps - 1: # an odd count under graph capture: copy the last result into state_0 instead of swapping, # so every replay of the captured graph starts from the newest state self.state_0.assign(self.state_1) else: self.state_0, self.state_1 = self.state_1, self.state_0
def step(self): if not _launch_frame_graph(self.model, self.graph): self.simulate() self.sim_time += self.frame_dt
def test_final(self): # Rigid bodies should have settled onto the cloth above the ground newton.examples.test_body_state( self.model, self.state_0, "all rigid bodies are above the ground", lambda q, qd: q[2] > -0.1, ) # Particles (cloth) should not have exploded particle_q = self.state_0.particle_q.numpy() min_pos = np.min(particle_q, axis=0) max_pos = np.max(particle_q, axis=0) bbox_size = np.linalg.norm(max_pos - min_pos) assert bbox_size < 20.0, f"Bounding box exploded: size={bbox_size:.2f}" assert min_pos[2] > -0.5, f"Excessive penetration: z_min={min_pos[2]:.4f}"
def render(self): self.viewer.begin_frame(self.sim_time) newton.examples.log_coupled_view(self, self.contacts) self.viewer.end_frame()
def _emit_rigid_bodies(self, builder: newton.ModelBuilder): """Add a few rigid boxes above the cloth.""" boxes = [ # (position, half-extents, mass) (wp.vec3(0.0, 0.0, 2.0), (0.15, 0.15, 0.15), 10.0), (wp.vec3(0.3, 0.1, 2.5), (0.10, 0.20, 0.10), 5.0), (wp.vec3(-0.2, -0.1, 3.0), (0.12, 0.12, 0.12), 8.0), ] for pos, (hx, hy, hz), mass in boxes: body = builder.add_body( xform=wp.transform(p=pos, q=wp.quat_identity()), mass=mass, ) builder.add_shape_box(body, hx=hx, hy=hy, hz=hz)
def _emit_cloth(self, builder: newton.ModelBuilder): """Add a cloth sheet at z=1.0, fixed at left and right edges.""" tri_ke = 1.0e5 edge_ke = 0.01 builder.add_cloth_grid( pos=wp.vec3(-0.5, -0.5, 1.0), rot=wp.quat_identity(), vel=wp.vec3(0.0), fix_left=True, fix_right=True, dim_x=30, dim_y=30, cell_x=1.0 / 30.0, cell_y=1.0 / 30.0, mass=0.1, tri_ke=tri_ke, tri_ka=tri_ke, tri_kd=1.0e-2 * tri_ke, edge_ke=edge_ke, edge_kd=1.0e-2 * edge_ke, particle_radius=0.01, )
def _emit_articulated_chain(self, builder: newton.ModelBuilder): """Add a 3-link pendulum chain anchored to the world.
The rigid solver owns the joints; VBD sees the links as disjoint proxy bodies. """ hx, hy, hz = 0.21, 0.05, 0.05 damping = 5.0 anchor = wp.vec3(0.6, 0.0, 2.25)
link_0 = builder.add_link() builder.add_shape_box(link_0, hx=hx, hy=hy, hz=hz) j0 = builder.add_joint_revolute( parent=-1, child=link_0, axis=wp.vec3(0.0, 1.0, 0.0), target_kd=damping, parent_xform=wp.transform(p=anchor, q=wp.quat_identity()), child_xform=wp.transform(p=wp.vec3(-hx, 0.0, 0.0), q=wp.quat_identity()), )
link_1 = builder.add_link() builder.add_shape_box(link_1, hx=hx, hy=hy, hz=hz) j1 = builder.add_joint_revolute( parent=link_0, child=link_1, axis=wp.vec3(0.0, 1.0, 0.0), target_kd=damping, parent_xform=wp.transform(p=wp.vec3(hx, 0.0, 0.0), q=wp.quat_identity()), child_xform=wp.transform(p=wp.vec3(-hx, 0.0, 0.0), q=wp.quat_identity()), )
link_2 = builder.add_link() builder.add_shape_box(link_2, hx=hx, hy=hy, hz=hz) j2 = builder.add_joint_revolute( parent=link_1, child=link_2, axis=wp.vec3(0.0, 1.0, 0.0), target_kd=damping, parent_xform=wp.transform(p=wp.vec3(hx, 0.0, 0.0), q=wp.quat_identity()), child_xform=wp.transform(p=wp.vec3(-hx, 0.0, 0.0), q=wp.quat_identity()), )
builder.add_articulation([j0, j1, j2], label="pendulum")
def _emit_soft_bodies(self, builder: newton.ModelBuilder): """Add several volumetric soft bodies above the cloth.""" grids = [ # (position, dims, cell_size, density, stiffness) (wp.vec3(-0.15, -0.15, 1.3), (3, 3, 3), 0.07, 1.0e3, 1.0e6), (wp.vec3(0.25, 0.20, 1.5), (2, 2, 4), 0.07, 1.0e3, 1.0e6), (wp.vec3(-0.30, 0.25, 1.8), (2, 4, 2), 0.07, 1.0e3, 1.0e6), ] for pos, (dx, dy, dz), cell, density, stiffness in grids: builder.add_soft_grid( pos=pos, rot=wp.quat_identity(), vel=wp.vec3(0.0, 0.0, 0.0), dim_x=dx, dim_y=dy, dim_z=dz, cell_x=cell, cell_y=cell, cell_z=cell, density=density, k_mu=stiffness, k_lambda=stiffness, k_damp=1.0e-3 * stiffness, particle_radius=0.025, )
@staticmethod def create_parser(): parser = newton.examples.create_parser() newton.examples.add_coupled_view_args(parser) return parser
if __name__ == "__main__": parser = Example.create_parser() viewer, args = newton.examples.init(parser) example = Example(viewer, args) newton.examples.run(example, args)example = "boxes_chain_cloth_soft"source = "newton/examples/multiphysics/example_mujoco_vbd_coupled_solver.py @ skild-ai/newton 7565a26b1"default = "steel_vbd"
[check]# Arguments `python -m shelf check` passes in test mode (from Newton's test_examples.py).args = ["--num-frames", "2", "--set", "solver.proxy_iterations=1"] # original: --proxy-iterations 1
[gallery]short = "Rigid boxes and a pendulum chain meet a cloth sheet and soft bodies."
[xpbd]status = "untested"
[vbd]status = "works"sim = { fps = 60, substeps = 8 }solver = { iterations = 10, friction_epsilon = 0.01, particle_enable_self_contact = true, particle_self_contact_margin = 0.01, particle_self_contact_gap = 0.0 }check = { args = ["--num-frames", "2"] }checked = "2026-10-28, newton 7565a26b1, NVIDIA RTX A6000"
[mujoco]status = "untested"
[featherstone]status = "untested"
[semi_implicit]status = "untested"
[feather_pgs]status = "untested"
[steel]status = "untested"
[kamino]status = "untested"
[implicit_mpm]status = "untested"
[style3d]status = "untested"
[mujoco_vbd]status = "works"sim = { fps = 60, substeps = 8 }solver = { coupling_mode = "lagged", proxy_iterations = 1, mass_scale = 1.0, collide_interval = 1 }entries.mujoco.solver = { use_mujoco_contacts = false, njmax = 200 }entries.vbd.solver = { iterations = 10, friction_epsilon = 0.01, particle_enable_self_contact = true, particle_self_contact_margin = 0.01, particle_self_contact_gap = 0.0 }checked = "2026-10-28, newton 7565a26b1, NVIDIA RTX A6000"
[kamino_vbd]status = "works"sim = { fps = 60, substeps = 8 }solver = { coupling_mode = "lagged", proxy_iterations = 1, mass_scale = 1.0, collide_interval = 1 }entries.kamino.solver = { use_collision_detector = false, use_fk_solver = false, dynamics = { preconditioning = true }, padmm = { max_iterations = 120, primal_tolerance = 1.0e-5, dual_tolerance = 1.0e-5, compl_tolerance = 1.0e-5, rho_0 = 0.1, use_acceleration = true, warmstart_mode = "containers" } }entries.vbd.solver = { iterations = 10, friction_epsilon = 0.01, particle_enable_self_contact = true, particle_self_contact_margin = 0.01, particle_self_contact_gap = 0.0 }checked = "2026-10-28, newton 7565a26b1, NVIDIA RTX A6000"
[steel_vbd]status = "works"# 6 substeps x 4 Steel iterations: 16.7 ms/frame vs MuJoCo's 20.1 at 8 (Steel's default 12 iterations at 8: 20.7).# Rigid penetration 23.0 / 0.21 mm peak / mean vs MuJoCo's 28.1 / 0.32: in both the peak is a falling box meeting# another between the once-per-frame collision passes. Cloth/soft penetration 20.7 / 0.52 mm vs 20.1 / 0.71.# 4 substeps doubles the box-box peak (37.9 mm); 4 iterations at 8 substeps raise the soft mean to 1.66 mm.sim = { fps = 60, substeps = 6 }solver = { coupling_mode = "lagged", proxy_iterations = 1, mass_scale = 1.0, collide_interval = 1 }# The pendulum's revolute joints have no limits, so enable_joint_limits has nothing to enforce.entries.steel.solver = { solve_mode = "matrix_free", iterations = 4 }entries.vbd.solver = { iterations = 10, friction_epsilon = 0.01, particle_enable_self_contact = true, particle_self_contact_margin = 0.01, particle_self_contact_gap = 0.0 }checked = "2026-11-04, newton 7565a26b1, NVIDIA GeForce RTX 3090"Steel docs and tutorials are licensed under Apache-2.0. Third-party notices. "Skild", "Skild AI" and the Skild logo are trademarks of Skild AI and are not licensed under Apache-2.0.