Skip to content

Basic joints

Basics

Joints of every type built with the ModelBuilder: ball, distance, prismatic, revolute and more.

0.00 s
The steel preset (the default), recordedon an NVIDIA RTX PRO 6000 Blackwell.

Run it

Terminal window
cd examples
# the default preset (steel), then the others
uv run shelf play basic_joints
uv run shelf play basic_joints --solver xpbd
uv run shelf play basic_joints --solver vbd
uv run shelf play basic_joints --solver kamino
# change solver settings live
uv run shelf tune basic_joints

Source: examples/shelf/00_basics/basic_joints/ · ported from newton/examples/basic/example_basic_joints.py @ skild-ai/newton 7565a26b1

Presets

PresetSolverStatusChecked
xpbdxpbdworks2026-10-28, newton 7565a26b1, NVIDIA RTX A6000
vbdvbdworks2026-10-28, newton 7565a26b1, NVIDIA RTX A6000
kaminokaminoworks2026-10-28, newton 7565a26b1, NVIDIA RTX A6000

Solvers with no row are untested on this example. The full settings are inpresets.toml.

Code

examples/shelf/00_basics/basic_joints/example.py
# 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 newton
import newton.examples
from common import presets
@wp.func
def _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.func
def _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)