Basic joints
Basics
Joints of every type built with the ModelBuilder: ball, distance, prismatic, revolute and more.
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 basic_jointsuv run shelf play basic_joints --solver xpbduv run shelf play basic_joints --solver vbduv run shelf play basic_joints --solver kamino# change solver settings liveuv run shelf tune basic_jointsSource: examples/shelf/00_basics/basic_joints/ · ported from newton/examples/basic/example_basic_joints.py @ skild-ai/newton 7565a26b1
Presets
| Preset | Solver | Status | Checked |
|---|---|---|---|
steeldefault | steel | works | 2026-11-04, newton 7565a26b1, NVIDIA GeForce RTX 3090 |
xpbd | xpbd | works | 2026-10-28, newton 7565a26b1, NVIDIA RTX A6000 |
vbd | vbd | works | 2026-10-28, newton 7565a26b1, NVIDIA RTX A6000 |
kamino | kamino | 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 Basic Joints## Shows how to use the ModelBuilder API to programmatically create different# joint types: BALL, DISTANCE, PRISMATIC, and REVOLUTE.## Command: python -m shelf run basic_joints --solver xpbd# python -m shelf run basic_joints --solver vbd# python -m shelf run basic_joints --solver kamino############################################################################
import warp as wp
import newtonimport newton.examplesfrom common import presets
@wp.funcdef _ball_body_stays_on_joint_sphere(q: wp.transform, qd: wp.spatial_vector): return abs(wp.length(wp.transform_get_translation(q) - wp.vec3(0.0, 3.0, 2.05)) - 0.75) < 5e-3
@wp.funcdef _slider_constrained_motion_has_stopped(q: wp.transform, qd: wp.spatial_vector): return ( wp.length(wp.cross(wp.spatial_top(qd), wp.vec3(0.0, 0.0, 1.0))) < 1e-5 and wp.length(wp.spatial_bottom(qd)) < 1e-5 )
class Example: def __init__(self, viewer, args): self.preset = presets.load(__file__) self.viewer = viewer self.args = args self.solver_type = self.preset.name
# setup simulation parameters first self.fps = self.preset.fps self.frame_dt = 1.0 / self.fps self.sim_time = 0.0 self.sim_substeps = self.preset.substeps self.sim_dt = self.frame_dt / self.sim_substeps
builder = newton.ModelBuilder() if self.solver_type == "kamino": newton.solvers.SolverKamino.register_custom_attributes(builder)
static_cfg = newton.ModelBuilder.ShapeConfig() static_cfg.density = 0.0
# add ground plane builder.add_ground_plane()
# common geometry settings cuboid_hx = 0.1 cuboid_hy = 0.1 cuboid_hz = 0.75 upper_hz = 0.25 * cuboid_hz
# layout positions (y-rows) rows = [-3.0, 0.0, 3.0] drop_z = 2.0
# ----------------------------- # REVOLUTE (hinge) joint demo # ----------------------------- y = rows[0]
a_rev = builder.add_link(xform=wp.transform(p=wp.vec3(0.0, y, drop_z + upper_hz), q=wp.quat_identity())) b_rev = builder.add_link( xform=wp.transform( p=wp.vec3(0.0, y, drop_z - cuboid_hz), q=wp.quat_from_axis_angle(wp.vec3(1.0, 0.0, 0.0), 0.15) ), label="b_rev", ) builder.add_shape_box(a_rev, hx=cuboid_hx, hy=cuboid_hy, hz=upper_hz, cfg=static_cfg) builder.add_shape_box(b_rev, hx=cuboid_hx, hy=cuboid_hy, hz=cuboid_hz)
j_fixed_rev = builder.add_joint_fixed( parent=-1, child=a_rev, parent_xform=wp.transform(p=wp.vec3(0.0, y, drop_z + upper_hz), q=wp.quat_identity()), child_xform=wp.transform(p=wp.vec3(0.0, 0.0, 0.0), q=wp.quat_identity()), label="fixed_revolute_anchor", ) j_revolute = builder.add_joint_revolute( parent=a_rev, child=b_rev, axis=wp.vec3(1.0, 0.0, 0.0), parent_xform=wp.transform(p=wp.vec3(0.0, 0.0, -upper_hz), q=wp.quat_identity()), child_xform=wp.transform(p=wp.vec3(0.0, 0.0, +cuboid_hz), q=wp.quat_identity()), label="revolute_a_b", ) # Create articulation from joints builder.add_articulation([j_fixed_rev, j_revolute], label="revolute_articulation")
# set initial joint angle builder.joint_q[-1] = wp.pi * 0.5
# ----------------------------- # PRISMATIC (slider) joint demo # ----------------------------- y = rows[1] a_pri = builder.add_link(xform=wp.transform(p=wp.vec3(0.0, y, drop_z + upper_hz), q=wp.quat_identity())) b_pri = builder.add_link( xform=wp.transform( p=wp.vec3(0.0, y, drop_z - cuboid_hz), q=wp.quat_from_axis_angle(wp.vec3(0.0, 1.0, 0.0), 0.12) ), label="b_prismatic", ) builder.add_shape_box(a_pri, hx=cuboid_hx, hy=cuboid_hy, hz=upper_hz, cfg=static_cfg) builder.add_shape_box(b_pri, hx=cuboid_hx, hy=cuboid_hy, hz=cuboid_hz)
j_fixed_pri = builder.add_joint_fixed( parent=-1, child=a_pri, parent_xform=wp.transform(p=wp.vec3(0.0, y, drop_z + upper_hz), q=wp.quat_identity()), child_xform=wp.transform(p=wp.vec3(0.0, 0.0, 0.0), q=wp.quat_identity()), label="fixed_prismatic_anchor", ) j_prismatic = builder.add_joint_prismatic( parent=a_pri, child=b_pri, axis=wp.vec3(0.0, 0.0, 1.0), # slide along Z parent_xform=wp.transform(p=wp.vec3(0.0, 0.0, -upper_hz), q=wp.quat_identity()), child_xform=wp.transform(p=wp.vec3(0.0, 0.0, +cuboid_hz), q=wp.quat_identity()), limit_lower=-0.3, limit_upper=0.3, limit_kd=1.0e3, label="prismatic_a_b", ) # Create articulation from joints builder.add_articulation([j_fixed_pri, j_prismatic], label="prismatic_articulation")
# ----------------------------- # BALL joint demo (sphere + cuboid) # ----------------------------- y = rows[2] radius = 0.3 z_offset = -1.0 # Shift down by 2 units
# kinematic (massless) sphere as the parent anchor a_ball = builder.add_link( xform=wp.transform(p=wp.vec3(0.0, y, drop_z + radius + cuboid_hz + z_offset), q=wp.quat_identity()) ) b_ball = builder.add_link( xform=wp.transform( p=wp.vec3(0.0, y, drop_z + radius + z_offset), q=wp.quat_from_axis_angle(wp.vec3(1.0, 1.0, 0.0), 0.1) ), label="b_ball", )
builder.add_shape_sphere(a_ball, radius=radius, cfg=static_cfg) builder.add_shape_box(b_ball, hx=cuboid_hx, hy=cuboid_hy, hz=cuboid_hz)
# Connect parent to world j_fixed_ball = builder.add_joint_fixed( parent=-1, child=a_ball, parent_xform=wp.transform(p=wp.vec3(0.0, y, drop_z + radius + cuboid_hz + z_offset), q=wp.quat_identity()), child_xform=wp.transform(p=wp.vec3(0.0, 0.0, 0.0), q=wp.quat_identity()), label="fixed_ball_anchor", ) j_ball = builder.add_joint_ball( parent=a_ball, child=b_ball, parent_xform=wp.transform(p=wp.vec3(0.0, 0.0, 0.0), q=wp.quat_identity()), child_xform=wp.transform(p=wp.vec3(0.0, 0.0, +cuboid_hz), q=wp.quat_identity()), label="ball_a_b", )
# Create articulation from joints builder.add_articulation([j_fixed_ball, j_ball], label="ball_articulation")
# set initial joint angle builder.joint_q[-4:] = wp.quat_rpy(0.5, 0.6, 0.7)
# finalize model builder.color() self.model = builder.finalize() # SolverVBD uses model.body_q as its structural rest pose, so keep it # consistent with the joint_q edits above before constructing the solver. newton.eval_fk(self.model, self.model.joint_q, self.model.joint_qd, self.model)
self.solver = self.preset.make_solver(self.model)
self.state_0 = self.model.state() self.state_1 = self.model.state() self.control = self.model.control()
newton.eval_fk(self.model, self.model.joint_q, self.model.joint_qd, self.state_0)
if self.solver_type == "kamino": self.collision_pipeline = None self.contacts = None else: self.collision_pipeline = newton.CollisionPipeline(self.model) self.contacts = self.collision_pipeline.contacts()
self.viewer.set_model(self.model)
self.capture()
def capture(self): with wp.ScopedCapture() as capture: self.simulate() self.graph = capture.graph
def simulate(self): contacts = None if self.solver_type == "kamino" else self.contacts for substep in range(self.sim_substeps): self.state_0.clear_forces()
# apply forces to the model self.viewer.apply_forces(self.state_0)
if self.collision_pipeline is not None: self.collision_pipeline.collide(self.state_0, self.contacts) self.solver.step(self.state_0, self.state_1, self.control, contacts, self.sim_dt)
# swap states 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 self.graph: wp.capture_launch(self.graph) else: self.simulate()
self.sim_time += self.frame_dt
def test_post_step(self): newton.examples.test_body_state( self.model, self.state_0, "revolute motion in plane", lambda q, qd: wp.length(abs(wp.cross(wp.spatial_bottom(qd), wp.vec3(1.0, 0.0, 0.0)))) < 1e-5, indices=[self.model.body_label.index("b_rev")], )
newton.examples.test_body_state( self.model, self.state_0, "linear motion on axis", _slider_constrained_motion_has_stopped, indices=[self.model.body_label.index("b_prismatic")], )
def test_final(self): newton.examples.test_body_state( self.model, self.state_0, "ball body stays on joint sphere", _ball_body_stays_on_joint_sphere, indices=[self.model.body_label.index("b_ball")], ) newton.examples.test_body_state( self.model, self.state_0, "static bodies are not moving", lambda q, qd: max(abs(qd)) == 0.0, indices=[2, 4], ) newton.examples.test_body_state( self.model, self.state_0, "fixed link body has come to a rest", lambda q, qd: max(abs(qd)) < 1e-2, indices=[0], ) newton.examples.test_body_state( self.model, self.state_0, "slider link constrained motion has come to a rest", _slider_constrained_motion_has_stopped, indices=[3], ) newton.examples.test_body_state( self.model, self.state_0, "slider link free-axis motion is slow", lambda q, qd: abs(wp.dot(wp.spatial_top(qd), wp.vec3(0.0, 0.0, 1.0))) < 1e-2, indices=[3], ) newton.examples.test_body_state( self.model, self.state_0, "movable links are not moving too fast", lambda q, qd: max(abs(qd)) < 3.0, indices=[1, 5], ) newton.examples.test_body_state( self.model, self.state_0, "revolute link responds to gravity", lambda q, qd: wp.length(wp.transform_get_translation(q) - wp.vec3(0.0, -2.25, 2.0)) > 0.1, indices=[self.model.body_label.index("b_rev")], ) newton.examples.test_body_state( self.model, self.state_0, "prismatic link responds to gravity", lambda q, qd: wp.length(wp.transform_get_translation(q) - wp.vec3(0.0, 0.0, 1.25)) > 0.1, indices=[self.model.body_label.index("b_prismatic")], ) newton.examples.test_body_state( self.model, self.state_0, "ball-joint link responds to gravity", lambda q, qd: wp.length(wp.transform_get_translation(q) - wp.vec3(-0.515887, 3.0355964, 1.5067748)) > 0.1, indices=[self.model.body_label.index("b_ball")], )
def render(self): self.viewer.begin_frame(self.sim_time) self.viewer.log_state(self.state_0) if self.contacts is not None: self.viewer.log_contacts(self.contacts, self.state_0) self.viewer.end_frame()
if __name__ == "__main__": # Parse arguments and initialize viewer viewer, args = newton.examples.init()
# Create viewer and run newton.examples.run(Example(viewer, args), args)example = "basic_joints"source = "newton/examples/basic/example_basic_joints.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 = "Joints of every type built with the ModelBuilder: ball, distance, prismatic, revolute and more."
[xpbd]status = "works"sim = { fps = 100, substeps = 10 }checked = "2026-10-28, newton 7565a26b1, NVIDIA RTX A6000"
[vbd]status = "works"sim = { fps = 100, substeps = 10 }solver = { iterations = 2, rigid_joint_linear_ke = 1.0e6, rigid_joint_angular_ke = 1.0e6 }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 4 iterations, immediate response, joint limits on: the slider's +-0.3 m limit is part of the scene# (XPBD enforces it); without it the slider drops 0.2 m past the limit onto the ground and the step gets slower# (0.44 ms vs 0.30 ms) from the extra contacts. 2 iterations cost the same; odd substeps freeze this example's graph.sim = { fps = 100, substeps = 2 }solver = { solve_mode = "matrix_free", iterations = 4, articulated_contact_response = "immediate", enable_joint_limits = true }checked = "2026-11-04, newton 7565a26b1, NVIDIA GeForce RTX 3090"
[kamino]status = "works"sim = { fps = 100, substeps = 8 }solver = { dynamics_solver = "dvi", sparse_dynamics = true, sparse_jacobian = true, dvi = { bilateral_solver_type = "LLTBRCM", max_alternating_iterations = 5, bilateral_solve_interval = 5, inequality_sweeps_per_iteration = 1 } }checked = "2026-10-28, newton 7565a26b1, NVIDIA RTX A6000"
[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.