Skip to content

Restitution

15 · Impacts and penetration

Balls with e = 0, 0.3, 0.6 and 0.9 drop from 1 m onto a ground with e = 0. An impact replaces the contact's position bias with a rebound target of −e times the closing speed. With the default average combine, the ground's 0 halves every coefficient. With restitution_combine="max" each ball keeps its own.

0.00 s

What to look at

  • Average: even the e = 0.9 ball bounces only about 20 cm.
  • Max: it bounces back to about 80 cm.

Key parameter

ShapeConfig.restitution

Per shape, 0 to 1, default 0. Pairs combine with restitution_combine.

Try changing

  • Give the ground e = 1 and keep "average".
  • Set enable_restitution=False to turn bouncing off everywhere.

Concept

Restitution

What it is

On impact, a contact's position correction is replaced by a target: the separating velocity must be at least e times the closing velocity at the start of the step. The combined coefficient comes from restitution_combine.

u_after ≥ −e · u_start

In this scene

With the default average, the ground's e = 0 halves the e = 0.9 ball's bounce to about 20 cm; with max it returns to about 80 cm.

Considerations

Cost
No extra rows.
Accuracy
The target uses the start-of-step velocity, so forces in the step do not enlarge the bounce. Large steps make the impact time less precise.
Interactions
contact_compliance rejects positive restitution unless enable_restitution=False.
Use it when
Balls and dropped parts; leave e = 0 for grasping and stacking.

The script

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

tutorials/15_restitution.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Restitution: per-shape coefficients and the restitution_combine mode."""
import newton
import warp as wp
from newton.solvers import SolverSteel
from replay import Recorder
DT = 1.0 / 240.0
SUBSTEPS = 8
BALL_E = [0.0, 0.3, 0.6, 0.9]
def build():
builder = newton.ModelBuilder()
builder.add_ground_plane(cfg=newton.ModelBuilder.ShapeConfig(restitution=0.0))
for i, e in enumerate(BALL_E):
ball = builder.add_body(xform=wp.transform((0.3 * i - 0.45, 0.0, 1.06), wp.quat_identity()))
builder.add_shape_sphere(
ball, radius=0.06, cfg=newton.ModelBuilder.ShapeConfig(density=500.0, restitution=e)
)
return builder.finalize()
def simulate(rec, combine):
model = build()
solver = SolverSteel(model, restitution_combine=combine)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
run = rec.variant(f'restitution_combine="{combine}"', model)
for _ in range(rec.frames(4.0)):
heights = state_0.body_q.numpy()[:, 2] - 0.06
run.frame(state_0, **{f"e_{e}": h for e, h in zip(BALL_E, heights)})
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, "average") # default: the ground's 0 halves every coefficient
simulate(rec, "max")
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