Skip to content

Dry joint friction

34 · Joints and drives

Three pendulums released at 70° with joint friction of 0, 0.3 and 1.0 N·m. With enable_joint_friction each DOF gets a row bounded by ±f·h around zero velocity, solved together with contacts and drives, like MuJoCo's frictionloss. A joint sticks wherever gravity's torque is below its friction.

0.00 s

Cost

Time per step, CUDA graph replay, median of 7 interleaved rounds on one NVIDIA RTX PRO 6000 Blackwell Server Edition; ratios are against the first row. Solver allocation is the GPU memory the solver allocates at construction, before warm-up and graph capture. Scene: a driven 6-link chain in each world (target_ke 50, joint friction 0.01).

Option1 world64 worlds1024 worldssolver allocation, 1024 worldsresult in this tutorial
joint friction off0.21 ms0.30 ms0.34 ms48 MBfriction ignored
enable_joint_friction=True0.30 ms 1.43×0.39 ms 1.32×0.44 ms 1.29×48 MB1 N·m pendulum sticks at 12.7°

What to look at

  • Default: all three swing identically, because friction is ignored.
  • Enabled: f = 1.0 stops at about 13°, and f = 0.3 near the bottom.

Key parameter

enable_joint_friction

Experimental, CUDA only. Reads Model.joint_friction [N or N·m] each step.

Try changing

  • Set friction to 2.0 N·m: the pendulum never starts.
  • Combine friction with a drive.

Concept

Dry joint friction

What it is

Each DOF with friction f gets a row whose impulse is bounded by ±f·h around zero velocity, so the joint sticks until the required effort exceeds f and then slides against it, like MuJoCo's frictionloss.

In this scene

The 1 N·m pendulum sticks at 12.7°, near asin(f / mgL); 0.3 N·m stops near the bottom.

Considerations

Cost
One row per DOF with friction: +29 to 43% per step on a 6-link chain.
Interactions
CUDA only; not with contact compliance or pre-elimination; disables the local, paired and sparse solves even when all coefficients are zero.
Use it when
Gearboxes and passive joints that hold position.

The script

cd tutorials && python 34_joint_friction.py prints each variant's final metric; --record out.json also writes the replay. Every tutorial imports replay.py from the same folder.

tutorials/34_joint_friction.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Dry joint friction (MuJoCo frictionloss) with enable_joint_friction."""
import math
import newton
import numpy as np
import warp as wp
from newton.solvers import SolverSteel
from replay import Recorder
DT = 1.0 / 240.0
SUBSTEPS = 8
FRICTION = [0.0, 0.3, 1.0] # N m
def tf(x, y, z):
return wp.transform((x, y, z), wp.quat_identity())
def build():
builder = newton.ModelBuilder()
for i, friction in enumerate(FRICTION):
y = 0.45 * i - 0.45
link = builder.add_link(xform=tf(0.0, y, 0.8))
builder.add_shape_capsule(
link,
xform=tf(0.0, 0.0, -0.2),
radius=0.02,
half_height=0.18,
cfg=newton.ModelBuilder.ShapeConfig(density=400.0),
)
builder.add_shape_sphere(
link,
xform=tf(0.0, 0.0, -0.4),
radius=0.05,
cfg=newton.ModelBuilder.ShapeConfig(density=2000.0),
)
j = builder.add_joint_revolute(
-1, link, axis=newton.Axis.Y, parent_xform=tf(0.0, y, 0.8), friction=friction
)
builder.add_articulation([j])
model = builder.finalize()
model.joint_q.fill_(math.radians(70.0))
return model
def simulate(rec, label, **options):
model = build()
solver = SolverSteel(model, **options)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
newton.eval_fk(model, model.joint_q, model.joint_qd, state_0)
run = rec.variant(label, model)
for _ in range(rec.frames(5.0)):
q = np.degrees(state_0.joint_q.numpy())
run.frame(state_0, **{f"f_{f}": q[i] for i, f in enumerate(FRICTION)})
for _ in range(SUBSTEPS):
pipeline.collide(state_0, contacts)
solver.step(state_0, state_1, control, contacts, DT)
state_0, state_1 = state_1, state_0
run.check(solver)
if __name__ == "__main__":
rec = Recorder()
simulate(rec, "enable_joint_friction=False (default)")
simulate(rec, "enable_joint_friction=True", enable_joint_friction=True)
rec.save()

Recorded with SolverSteel on Newton 1.9.0.dev0, Warp 1.19.0, NVIDIA RTX PRO 6000 Blackwell Server Edition. Replays read the state back every frame, so they are not a speed benchmark. Script · page text