Skip to content

Restitution threshold

16 · Impacts and penetration

A ball with e = 0.8 dropped from 0.5 m. Only impacts faster than restitution_velocity_threshold bounce; slower ones use the ordinary contact law, so a resting object pressed by gravity never jitters. The threshold therefore sets when a bouncing ball comes to rest.

0.00 s

What to look at

  • 0.05 m/s: many small bounces before it settles.
  • 0.5 m/s (default): it stops after about 2.5 s.
  • 1.5 m/s: it stops after the fourth bounce. With enable_restitution=False it does not bounce.

Key parameter

restitution_velocity_threshold

Minimum closing speed [m/s] at the start of the step for a rebound. Default 0.5, like PhysX's bounceThreshold.

Try changing

  • Drop from 2 m with threshold 3 m/s.
  • Set e = 1 and threshold 0: does the ball keep its height?

Concept

Restitution threshold

What it is

Only contacts that close faster than restitution_velocity_threshold at the start of a step bounce. Slower contacts use the ordinary resting law, so objects pressed by gravity or a gripper never jitter.

In this scene

A ball with e = 0.8 stops after four bounces with a 1.5 m/s threshold, after about 2.5 s with the default 0.5 m/s, and keeps making small bounces with 0.05 m/s.

Considerations

Accuracy
A low threshold keeps low-speed bounces but can make resting contacts chatter.
Use it when
Keep 0.5 m/s, the PhysX default, unless slow bounces matter; set enable_restitution=False to turn bouncing off everywhere.

The script

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

tutorials/16_restitution_threshold.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""restitution_velocity_threshold: the slowest impact that still bounces."""
import newton
import warp as wp
from newton.solvers import SolverSteel
from replay import Recorder
DT = 1.0 / 240.0
SUBSTEPS = 8
def build():
builder = newton.ModelBuilder()
cfg = newton.ModelBuilder.ShapeConfig(restitution=0.8)
builder.add_ground_plane(cfg=cfg)
ball = builder.add_body(xform=wp.transform((0.0, 0.0, 0.56), wp.quat_identity()))
builder.add_shape_sphere(ball, radius=0.06, cfg=cfg)
return builder.finalize()
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()
run = rec.variant(label, model)
for _ in range(rec.frames(4.0)):
run.frame(state_0, height=state_0.body_q.numpy()[0, 2] - 0.06)
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, "threshold 0.5 m/s (default)")
simulate(rec, "threshold 0.05 m/s", restitution_velocity_threshold=0.05)
simulate(rec, "threshold 1.5 m/s", restitution_velocity_threshold=1.5)
simulate(rec, "enable_restitution=False", enable_restitution=False)
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