Trebuchet
Many contacts
A counterweight trebuchet hurls a boulder at a brick wall.
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 trebuchet# change solver settings liveuv run shelf tune trebuchetSource: examples/shelf/02_many_contacts/trebuchet/ · ported from steel-webgpu tools/scenes.py trebuchet() @ ad03e49
Presets
| Preset | Solver | Status | Checked |
|---|---|---|---|
steeldefault | steel | works | 2026-11-10, newton f6fc82a8b, NVIDIA RTX PRO 6000 Blackwell Server Edition |
Solvers with no row are untested on this example. The full settings are inpresets.toml.
Code
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors# SPDX-License-Identifier: Apache-2.0# Direct source: steel-webgpu; see presets.toml and THIRD_PARTY.md.# Modified for Steel Shelf: preset-driven execution and shared runner integration.
############################################################################ Example Trebuchet## Trebuchet: a hinged arm with a hung counterweight throws a boulder from a cup at a dry brick wall.## Ported from steel-webgpu's tools/scenes.py (trebuchet); the scene builds# as it does there and the shelf's preset drives the solver.## Command: python -m shelf run trebuchet############################################################################
import math
import numpy as npimport warp as wp
import newtonimport newton.examplesfrom common.steel_scenes import BLUE, GOLD, GREY, RED, TEAL, SceneExample, cfg, tf
class Example(SceneExample): file = __file__ camera = {'eye': (6.0, -30.0, 10.0), 'target': (16.0, 0.0, 4.0)}
def build(self): """Trebuchet: a hinged arm with a hung counterweight throws a boulder from a cup at a dry brick wall.""" b = newton.ModelBuilder() b.add_ground_plane(cfg=cfg(mu=0.9)) pivot_z, short_arm, long_arm, cock = 4.2, 1.5, 5.2, 0.7 # avbd cocks to 0.98 rad, which buries the cup frame = [b.add_shape_box(-1, xform=tf((0, sy * 1.5, pivot_z / 2)), hx=0.25, hy=0.25, hz=pivot_z / 2, cfg=cfg(mu=0.8), color=GREY) for sy in (-1, 1)] frame.append(b.add_shape_box(-1, xform=tf((0, 0, pivot_z)), hx=0.3, hy=1.2, hz=0.3, cfg=cfg(mu=0.5), color=GREY)) q_arm = wp.quat_from_axis_angle(wp.vec3(0, 1, 0), -cock) off = (long_arm - short_arm) / 2 pivot = wp.vec3(0, 0, pivot_z) arm_c = pivot + wp.quat_rotate(q_arm, wp.vec3(-off, 0, 0)) arm = b.add_link(xform=wp.transform(arm_c, q_arm), label="arm") b.add_shape_box(arm, hx=(long_arm + short_arm) / 2, hy=0.2, hz=0.175, cfg=cfg(density=2500, mu=0.5), color=(0.55, 0.38, 0.22)) # The cup is welded to the arm: its parts are arm shapes in the arm frame. tip = wp.vec3(-long_arm + off, 0, 0) for size, p in (((1.5, 1.3, 0.12), (0, 0, 0.24)), ((0.14, 1.3, 0.6), (0.72, 0, 0.55)), ((0.14, 1.3, 0.42), (-0.72, 0, 0.46)), ((1.5, 0.12, 0.5), (0, 0.66, 0.5)), ((1.5, 0.12, 0.5), (0, -0.66, 0.5))): b.add_shape_box(arm, xform=tf(tip + wp.vec3(*p)), hx=size[0] / 2, hy=size[1] / 2, hz=size[2] / 2, cfg=cfg(density=600, mu=0.5), color=(0.45, 0.30, 0.18)) # The arm turns inside the axle housing, as a joint's bodies do not collide. for a in [i for i, owner in enumerate(b.shape_body) if owner == arm]: for f in frame: b.add_shape_collision_filter_pair(a, f) j_arm = b.add_joint_revolute(-1, arm, axis=(0, 1, 0), parent_xform=wp.transform(pivot, q_arm), child_xform=tf((off, 0, 0))) anchor = wp.vec3(short_arm + off, 0, 0) cw_pos = pivot + wp.quat_rotate(q_arm, wp.vec3(short_arm, 0, 0)) + wp.vec3(0, 0, -1.15) cw = b.add_link(xform=tf(cw_pos), label="counterweight") b.add_shape_box(cw, hx=0.85, hy=0.85, hz=0.75, cfg=cfg(density=10000, mu=0.6), color=GREY) j_cw = b.add_joint_ball(arm, cw, parent_xform=wp.transform(anchor, wp.quat_inverse(q_arm)), child_xform=tf((0, 0, 1.15))) b.add_articulation([j_arm, j_cw]) up = wp.quat_rotate(q_arm, wp.vec3(0, 0, 1)) boulder = b.add_body(xform=tf(pivot + wp.quat_rotate(q_arm, wp.vec3(-long_arm, 0, 0)) + up * 0.85)) b.add_shape_sphere(boulder, radius=0.55, cfg=cfg(density=3000, mu=0.4), color=(0.35, 0.33, 0.30)) b.add_shape_box(-1, xform=tf((-1.9, 0, pivot_z + 2.9)), hx=0.35, hy=2.1, hz=0.35, cfg=cfg(mu=0.9), color=GREY) # Dry-stacked brick wall downrange (avbd-metal's castle is mortared; the port has no breakable joints). brick = (1.1, 0.55, 0.55) for h in range(8): # Running bond: offset courses carry one brick fewer so no brick overhangs the end. for i in range(10 - h % 2): y = -4.5 * brick[0] + i * brick[0] + (brick[0] / 2 if h % 2 else 0) body = b.add_body(xform=tf((31.5, y, h * brick[2] + brick[2] / 2))) b.add_shape_box(body, hx=brick[1] * 0.93 / 2, hy=brick[0] * 0.93 / 2, hz=brick[2] / 2, cfg=cfg(density=800, mu=0.85), color=[(0.72, 0.45, 0.33), (0.62, 0.38, 0.28)][(h + i) % 2]) return b, {}, "Trebuchet: a hinged arm with a hung counterweight throws a boulder from its cup at a dry-stacked brick wall."
if __name__ == "__main__": parser = Example.create_parser() viewer, args = newton.examples.init(parser) newton.examples.run(Example(viewer, args), args)example = "trebuchet"source = "steel-webgpu tools/scenes.py trebuchet() @ ad03e49"default = "steel"
[check]# Arguments `python -m shelf check` passes in test mode.args = []
[gallery]short = "A counterweight trebuchet hurls a boulder at a brick wall."title = "Trebuchet"frame = 240
[xpbd]status = "untested"
[vbd]status = "untested"
[mujoco]status = "untested"
[featherstone]status = "untested"
[semi_implicit]status = "untested"
[feather_pgs]status = "untested"
[steel]status = "works"# The port's 4 x 12 sized the propagation rows at the default 544 while the wall needs ~1770: up to 1116 contact rows# were dropped every step, so the dry-stacked wall slumped and fell apart before the boulder arrived (bricks down# 3.5 m by frame 200). 2560 rows hold it (peak 1765, contacts 927).# 3 substeps x 16 iterations (4.7-5.0 ms/frame on an RTX PRO 6000; the port's 4 x 12 6.7 with the rows dropped, 5.4# with them kept): the wall stands still until the hit (bricks < 3 cm/s), the boulder smashes through and 44 of# 76 bricks come down, as 16 x 32 (36-44) and 8 x 16 (37). Fewer sweeps leave the wall nearly whole (2 x 12: 1,# 3 x 12: 9, 3 x 14: 22-33, 4 x 8: 15). Penetration: mean 0.18 mm; the 19-21 mm peak is a brick driven into the# ground by the 20 m/s boulder in the frames of the hit (every configuration, 16 x 32 included, shows one).# Sleeping on: bricks at rest sleep (without it 7.1 ms). The joints have no finite limits, so limit rows are off.sim = { fps = 60, substeps = 3 }solver = { solve_mode = "matrix_free", iterations = 16, articulated_contact_response = "propagation_colored", enable_joint_limits = false, enable_sleeping = true, propagation_constraint_max = 2560 }checked = "2026-11-10, newton f6fc82a8b, NVIDIA RTX PRO 6000 Blackwell Server Edition"
[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.