Skip to content

Adhesion

21 · Compliance and adhesion

A box under a ceiling. The per-shape steel:adhesion gives every contact point a pull of A newtons while the gap is open by less than the shapes' 4 mm gap. Steel applies the pull to the normal row, so friction cones are unchanged. Below the box's weight it falls; above, it hangs until a downward pull ramps past the total adhesion.

0.00 s

What to look at

  • 0.5 × weight: the box falls.
  • 1.5 × weight: it hangs.
  • Pulled down from 0.6 s: it pops off when the pull exceeds the adhesion.

Key parameter

steel:adhesion

Per shape [N] per contact point; a contact pulls with the sum of its two shapes' values. Experimental.

Try changing

  • Set the ceiling's adhesion to zero after one second to drop the box.
  • Tilt the ceiling 30° and watch friction hold the box.

Concept

Adhesion

What it is

steel:adhesion lets a contact pull with up to A newtons while its gap is open. Steel shifts the normal impulse by A·h and solves in the unchanged friction cone, so resting penetration is the same.

λ′ = λ + A·h ≥ 0

In this scene

Below its weight the box falls; at 1.5× weight it hangs; pulled down from 0.6 s it pops off when the pull exceeds the total adhesion.

Considerations

Accuracy
Pull-off grows with contact count: four corner contacts give four times A.
Interactions
Not allowed with sleeping, contact compliance, contact_regularization or torsion; keeps dense mass factors.
Use it when
Suction cups, magnets and sticky surfaces; write zero to release.

The script

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

tutorials/21_adhesion.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Sticky contacts with steel:adhesion: a box hangs under a ceiling until it is pulled off."""
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
HALF, GAP = 0.05, 0.004
WEIGHT = 1000.0 * (2 * HALF) ** 3 * 9.81 # 9.81 N
def build(factor):
builder = newton.ModelBuilder()
SolverSteel.register_custom_attributes(builder)
cfg = newton.ModelBuilder.ShapeConfig(mu=0.5, gap=GAP)
builder.add_ground_plane()
ceiling = builder.add_shape_box(
-1,
xform=wp.transform((0.0, 0.0, 1.05), wp.quat_identity()),
hx=0.2,
hy=0.2,
hz=0.05,
cfg=cfg,
)
box = builder.add_body(xform=wp.transform((0.0, 0.0, 1.0 - HALF), wp.quat_identity()))
builder.add_shape_box(box, hx=HALF, hy=HALF, hz=HALF, cfg=cfg)
model = builder.finalize()
adhesion = model.steel.adhesion.numpy()
adhesion[ceiling] = factor * WEIGHT / 4.0 # [N] per contact point; the face has four
model.steel.adhesion.assign(adhesion)
return model
def simulate(rec, label, factor, pull_at=None):
model = build(factor)
solver = SolverSteel(model)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
run = rec.variant(label, model)
for frame in range(rec.frames(2.5)):
t = frame / rec.fps
run.frame(state_0, drop_mm=1000.0 * ((1.0 - HALF) - state_0.body_q.numpy()[0, 2]))
for _ in range(SUBSTEPS):
state_0.clear_forces()
if pull_at is not None and t > pull_at:
pull = 2.5 * WEIGHT * min(1.0, t - pull_at) # ramps to 2.5 x weight over 1 s
state_0.body_f.assign(np.array([[0, 0, -pull, 0, 0, 0]], dtype=np.float32))
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, "adhesion 0.5 x weight", 0.5)
simulate(rec, "adhesion 1.5 x weight", 1.5)
simulate(rec, "1.5 x weight, then pulled down", 1.5, pull_at=0.6)
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