Skip to content

Patch friction vs point friction

03 · Contact and friction

A box rests on a slope at 98% of its friction limit (tan θ = 0.49, μ = 0.5). Point friction gives each of the four contacts its own friction cone, so the load shifts between them and static friction creeps. Patch friction, the default, pools the contacts of a body pair into a region whose friction acts at up to two anchors. The anchors carry their tangential displacement across steps and pull it back, so a held object does not creep.

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: the 8-box stack in each world, 8 steps of 1/240 s per frame.

Option1 world64 worlds1024 worldssolver allocation, 1024 worldsresult in this tutorial
patch friction (default)1.00 ms1.20 ms1.65 ms215 MBslope creep 0.45 µm in 10 s
point friction1.38 ms 1.38×1.52 ms 1.27×1.78 ms 1.08×132 MBslope creep 2.24 mm

What to look at

  • The point-friction box creeps about 2 mm in 10 s.
  • The patch-friction box stays within a micrometre.

Key parameter

friction_anchor_beta

Position gain of the friction anchors. The default None means 0.2; 0 selects point friction, two tangent rows per contact.

Try changing

  • Raise the slope to tan θ = 0.51: both boxes must slide.
  • Try friction_anchor_beta=0.05 and 0.5.

Concept

Friction patches and anchors

What it is

A patch groups the contacts of one body pair that share a normal, plane and friction coefficient. Its friction acts at up to two anchors that share the pooled normal load and remember how far they have been displaced; the friction rows push that displacement back with gain β. An anchor is released when its patch unloads or slides at the edge of the friction cone.

tangent target = β · displacement / h

In this scene

At 98% of the friction limit, point friction creeps 2.24 mm in 10 s; the patch holds the box to 0.45 µm.

Considerations

Cost
Here patches were cheaper than point friction: 1.0 vs 1.4 ms per step in one world, 1.6 vs 1.8 ms in 1024 worlds, since two anchors replace two rows per contact.
Interactions
Contact compliance and friction_mode other than "fixed_normal" switch to point friction. Call reset after teleporting a body to drop its anchors.
Use it when
Default. Point friction (β = 0) suits scenes that need per-contact friction forces.

The script

cd tutorials && python 03_patch_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/03_patch_friction.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Patch friction vs point friction: a box at 98% of its friction limit on a slope."""
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 # 30 Hz frames
MU = 0.5
SLOPE = math.atan(0.49) # tan(slope) = 0.98 * MU
def build():
builder = newton.ModelBuilder()
tilt = wp.quat_from_axis_angle(wp.vec3(0.0, 1.0, 0.0), -SLOPE)
builder.add_shape_box(
-1,
xform=wp.transform((0.0, 0.0, 0.3), tilt),
hx=0.8,
hy=0.3,
hz=0.05,
cfg=newton.ModelBuilder.ShapeConfig(mu=MU),
)
normal = np.array(wp.quat_rotate(tilt, wp.vec3(0.0, 0.0, 1.0)))
start = np.array([0.0, 0.0, 0.3]) + normal * 0.11
box = builder.add_body(xform=wp.transform(start, tilt))
builder.add_shape_box(
box, hx=0.12, hy=0.08, hz=0.06, cfg=newton.ModelBuilder.ShapeConfig(mu=MU)
)
return builder.finalize(), start
def simulate(rec, label, **options):
model, start = build()
solver = SolverSteel(model, **options)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
run = rec.variant(label, model)
for _ in range(rec.frames(10.0)):
slip = np.linalg.norm(state_0.body_q.numpy()[0, :3] - start)
run.frame(state_0, slip_mm=1000.0 * slip)
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, "point friction (friction_anchor_beta=0)", friction_anchor_beta=0.0)
simulate(rec, "patch friction (default)")
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