Shapes in box
Many contacts
Rigid bodies of every shape type dropped into a box container.
steel preset (the default), recordedon an NVIDIA RTX PRO 6000 Blackwell.Run it
cd examples# the default preset (steel), then the othersuv run shelf play shapes_in_boxuv run shelf play shapes_in_box --solver vbd# change solver settings liveuv run shelf tune shapes_in_boxSource: examples/shelf/02_many_contacts/shapes_in_box/ · ported from newton/examples/vbd/example_vbd_rigid_rigid_contact.py @ skild-ai/newton 7565a26b1
Presets
| Preset | Solver | Status | Checked |
|---|---|---|---|
steeldefault | steel | works | 2026-11-04, newton 7565a26b1, NVIDIA GeForce RTX 3090 |
vbd | vbd | 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) 2025 The Newton Developers# SPDX-License-Identifier: Apache-2.0# Modified for Steel Shelf: preset-driven execution and shared runner integration.
############################################################################ Example VBD Rigid Drop## Rigid bodies of all supported shape types (sphere, box, capsule,# cylinder, cone, mesh bear) are dropped into a static rigid box# container and settle under gravity. This exercises pure rigid-rigid# contact handling in the VBD solver at a small (centimeter) scale.## Command: python -m shelf run shapes_in_box############################################################################
import os
import numpy as npimport warp as wpfrom pxr import Usd, UsdGeom
import newtonimport newton.examplesfrom common import presets
PARAMS = { "shape_names": ["mesh", "cone", "sphere", "box", "capsule", "cylinder"], "shape_size": 0.042, "shape_margin": 0.005, "rigid_gap": 0.01, "box_width_scale": 10.0, "box_depth_scale": 10.0, "box_height_scale": 5.0, "box_elevation": 0.30, "box_wall_thickness": 0.005, "settle_frames": 300, "shape_density": 100.0, "shape_ke": 1e3, "shape_kd": 0, "shape_mu": 0.5, "container_ke": 1e3, "container_kd": 0, "container_mu": 0.8, "gravity": (0.0, 0.0, -9.8), "body_drop_offset_scale": 4.0, "body_drop_spacing_scale": 3.0,}
def _load_bear_mesh(target_size): bear_path = os.path.join(newton.examples.get_asset_directory(), "bear.usd") stage = Usd.Stage.Open(bear_path) geom = UsdGeom.Mesh(stage.GetPrimAtPath("/root/bear/bear"))
points = np.array(geom.GetPointsAttr().Get(), dtype=np.float32) indices = np.array(geom.GetFaceVertexIndicesAttr().Get(), dtype=np.int32)
center = (points.max(axis=0) + points.min(axis=0)) / 2.0 points -= center extent = (points.max(axis=0) - points.min(axis=0)).max() points *= (target_size * 2.0) / extent
return points, indices.tolist()
def build_model(builder, params, seed=42): rng = np.random.default_rng(seed) builder.rigid_gap = params["rigid_gap"]
r = params["shape_size"] hx = r * params["box_width_scale"] / 2 hy = r * params["box_depth_scale"] / 2 hz = r * params["box_height_scale"] elev = params["box_elevation"] t = params["box_wall_thickness"]
container_cfg = newton.ModelBuilder.ShapeConfig() container_cfg.ke = params["container_ke"] container_cfg.kd = params["container_kd"] container_cfg.mu = params["container_mu"] container_cfg.margin = params["shape_margin"]
# Floor builder.add_shape_box( -1, xform=wp.transform(wp.vec3(0.0, 0.0, elev - t / 2), wp.quat_identity()), hx=hx + t, hy=hy + t, hz=t / 2, cfg=container_cfg, ) # Front wall (-Y) builder.add_shape_box( -1, xform=wp.transform(wp.vec3(0.0, -(hy + t / 2), elev + hz / 2), wp.quat_identity()), hx=hx + t, hy=t / 2, hz=hz / 2, cfg=container_cfg, ) # Back wall (+Y) builder.add_shape_box( -1, xform=wp.transform(wp.vec3(0.0, hy + t / 2, elev + hz / 2), wp.quat_identity()), hx=hx + t, hy=t / 2, hz=hz / 2, cfg=container_cfg, ) # Left wall (-X) builder.add_shape_box( -1, xform=wp.transform(wp.vec3(-(hx + t / 2), 0.0, elev + hz / 2), wp.quat_identity()), hx=t / 2, hy=hy, hz=hz / 2, cfg=container_cfg, ) # Right wall (+X) builder.add_shape_box( -1, xform=wp.transform(wp.vec3(hx + t / 2, 0.0, elev + hz / 2), wp.quat_identity()), hx=t / 2, hy=hy, hz=hz / 2, cfg=container_cfg, )
# Rigid bodies margin = params["shape_margin"] interior_x = hx - r - margin * 2 interior_y = hy - r - margin * 2 min_spacing = r * 2 + margin * 3 body_indices = [] positions = []
bear_mesh = None shape_names = params["shape_names"]
for i in range(len(shape_names)): if shape_names[i] == "mesh": positions.append((0.0, 0.0)) else: for _ in range(200): x = rng.uniform(-interior_x, interior_x) y = rng.uniform(-interior_y, interior_y) ok = all(np.sqrt((x - px) ** 2 + (y - py) ** 2) >= min_spacing for px, py in positions) if ok: positions.append((x, y)) break else: positions.append((x, y))
cfg = newton.ModelBuilder.ShapeConfig() cfg.density = params["shape_density"] cfg.ke = params["shape_ke"] cfg.kd = params["shape_kd"] cfg.mu = params["shape_mu"] cfg.margin = margin
for i, name in enumerate(shape_names): px, py = positions[i] drop_z = elev + r * params["body_drop_offset_scale"] + i * r * params["body_drop_spacing_scale"]
body = builder.add_body(xform=wp.transform(wp.vec3(px, py, drop_z), wp.quat_identity())) body_indices.append(body)
if name == "sphere": builder.add_shape_sphere(body, radius=r, cfg=cfg) elif name == "box": builder.add_shape_box(body, hx=r, hy=r, hz=r, cfg=cfg) elif name == "capsule": builder.add_shape_capsule(body, radius=r * 0.7, half_height=r, cfg=cfg) elif name == "cylinder": builder.add_shape_cylinder(body, radius=r, half_height=r * 0.5, cfg=cfg) elif name == "cone": builder.add_shape_cone(body, radius=r, half_height=r, cfg=cfg) elif name == "mesh": if bear_mesh is None: bear_pts, bear_idx = _load_bear_mesh(r) bear_mesh = newton.Mesh(bear_pts, np.array(bear_idx, dtype=np.int32)) builder.add_shape_mesh(body, mesh=bear_mesh, cfg=cfg)
builder.color()
return { "body_indices": body_indices, }
def setup_sim(builder, preset): model = builder.finalize()
solver = preset.make_solver(model)
return model, solver
class Example: def __init__(self, viewer, args): self.preset = presets.load(__file__) self.viewer = viewer self.params = PARAMS 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 self.frame = 0
seed = getattr(args, "seed", 42) builder = newton.ModelBuilder(gravity=self.params["gravity"]) self.info = build_model(builder, self.params, seed=seed) self.model, self.solver = setup_sim(builder, self.preset)
self.state_0 = self.model.state() self.state_1 = self.model.state() self.control = self.model.control() self.collision_pipeline = newton.CollisionPipeline(self.model) self.contacts = self.collision_pipeline.contacts()
self.viewer.set_model(self.model) if hasattr(self.viewer, "renderer"): self.viewer.renderer.draw_wireframe = True self.viewer.set_camera(wp.vec3(0.35, -0.35, 0.55), -25.0, 135.0)
self.capture()
def capture(self): if wp.get_device().is_cuda: with wp.ScopedCapture() as capture: self.simulate() self.graph = capture.graph else: self.graph = None
def simulate(self): for substep in range(self.sim_substeps): self.state_0.clear_forces() self.viewer.apply_forces(self.state_0) self.collision_pipeline.collide(self.state_0, self.contacts) 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): self.frame += 1 if self.graph: wp.capture_launch(self.graph) else: self.simulate() self.sim_time += self.frame_dt
def render(self): self.viewer.begin_frame(self.sim_time) self.viewer.log_state(self.state_0) self.viewer.log_contacts(self.contacts, self.state_0) self.viewer.end_frame()
def test_final(self): body_q = self.state_0.body_q.numpy() body_indices = self.info["body_indices"] elev = self.params["box_elevation"] box_height = self.params["shape_size"] * self.params["box_height_scale"]
settled = 0 for bi in body_indices: z = body_q[bi][2] if not np.isnan(z) and z > elev and z < elev + box_height: settled += 1
assert settled >= len(body_indices) - 1, ( f"Only {settled}/{len(body_indices)} rigid bodies settled inside the box" )
@staticmethod def create_parser(): parser = newton.examples.create_parser() parser.add_argument("--seed", type=int, default=42) parser.set_defaults(paused=True) 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 = "shapes_in_box"source = "newton/examples/vbd/example_vbd_rigid_rigid_contact.py @ skild-ai/newton 7565a26b1"default = "steel"
[check]# Arguments `python -m shelf check` passes in test mode (from Newton's test_examples.py).args = []
[gallery]short = "Rigid bodies of every shape type dropped into a box container."
[xpbd]status = "untested"
[vbd]status = "works"sim = { fps = 60, substeps = 5 }solver = { iterations = 15, rigid_body_contact_buffer_size = 256 }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 = "works"# 2 substeps x 6 iterations, immediate response: the fastest setting with lower penetration than VBD's# (peak 1.6 vs 10.1 mm, mean 0.028 vs 0.052) and all six bodies settled in the box; 2 x 3 is 7% faster# but sinks deeper on average than VBD; the colored sweep is as fast at 4 iterations. Odd substep# counts lose a substep per frame in this example's captured graph (VBD's 5 included), so keep it even.sim = { fps = 60, substeps = 2 }solver = { solve_mode = "matrix_free", iterations = 6, articulated_contact_response = "immediate" }checked = "2026-11-04, newton 7565a26b1, NVIDIA GeForce RTX 3090"
[kamino]status = "untested"
[implicit_mpm]status = "untested"
[style3d]status = "untested"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.