Summary
In 0.35.0-beta.0, restitution has almost no effect: a ball dropped from 2 m
rebounds ~1% of the drop regardless of its restitution coefficient. 0.34.0
matches the textbook e² on the same test, so this looks like a regression
introduced in the beta.
Reproduced in pure Rust (no bindings involved), release build, macOS arm64,
rustc 1.92.0.
| collider restitution |
expected (e²) |
0.34.0 |
0.35.0-beta.0 |
| 0.30 |
0.09 |
0.07 |
0.01 |
| 0.50 |
0.25 |
0.25 |
0.01 |
| 0.80 |
0.64 |
0.66 |
0.01 |
| 0.95 |
0.90 |
0.94 |
0.01 |
(Numbers are the fraction of the 2 m drop recovered at the first apex after
impact.)
Repro
Two files, cargo run --release. Flipping the one version line in Cargo.toml
between =0.35.0-beta.0 and =0.34.0 is the whole diff — the source compiles
unchanged on both.
Cargo.toml
[package]
name = "rapier-restitution-repro"
version = "0.0.0"
edition = "2021"
[dependencies]
rapier3d = "=0.35.0-beta.0" # swap for "=0.34.0" to see the expected values
src/main.rs
use rapier3d::prelude::*;
fn rebound(e: f32) -> f32 {
let mut bodies = RigidBodySet::new();
let mut colliders = ColliderSet::new();
let ground = bodies.insert(RigidBodyBuilder::fixed());
colliders.insert_with_parent(
ColliderBuilder::cuboid(30.0, 0.1, 30.0)
.restitution(e)
.restitution_combine_rule(CoefficientCombineRule::Average),
ground,
&mut bodies,
);
let ball = bodies.insert(RigidBodyBuilder::dynamic().translation(Vec3::new(0.0, 2.3, 0.0)));
colliders.insert_with_parent(
ColliderBuilder::ball(0.2)
.density(1.0)
.restitution(e)
.restitution_combine_rule(CoefficientCombineRule::Average),
ball,
&mut bodies,
);
let mut pipeline = PhysicsPipeline::new();
let mut islands = IslandManager::new();
let mut broad_phase = DefaultBroadPhase::new();
let mut narrow_phase = NarrowPhase::new();
let mut impulse_joints = ImpulseJointSet::new();
let mut multibody_joints = MultibodyJointSet::new();
let mut ccd = CCDSolver::new();
let params = IntegrationParameters::default();
let gravity = Vec3::new(0.0, -9.81, 0.0);
let mut touched = false;
let mut apex = 0.0f32;
for _ in 0..400 {
pipeline.step(
gravity, ¶ms, &mut islands, &mut broad_phase, &mut narrow_phase,
&mut bodies, &mut colliders, &mut impulse_joints, &mut multibody_joints,
&mut ccd, &(), &(),
);
let y = bodies[ball].translation().y;
if !touched && y < 0.35 { touched = true; }
if touched && y > apex { apex = y; }
}
(apex - 0.3) / 2.0 // fraction of the 2 m drop recovered
}
fn main() {
println!("restitution expected(e^2) measured");
for e in [0.3f32, 0.5, 0.8, 0.95] {
println!(" {:.2} {:.2} {:.2}", e, e * e, rebound(e));
}
}
What I ruled out
- Not the JS/Wasm bindings. The table above is native Rust. For what it's
worth the bindings agree: @dimforge/rapier3d-compat@0.19.3 (rapier 0.30)
gives 0.07/0.25/0.66/0.94, and bindings built from 0.35.0-beta.0 give 0.01
across the board.
- Not the changed contact defaults alone. Setting
normalized_prediction_distance back to 0.002, or
normalized_allowed_linear_error to 0.001/0, changes little (0.01 → 0.06
at best).
- Not the solver skipping the restitution pass entirely — as far as I can
tell solve is called with solve_restitution: true
(staged_island_solver/solve.rs).
- Stiffer contacts recover part of it but not the shape of the curve:
contact_natural_frequency = 180 with num_solver_iterations = 8 gets
restitution 0.8 to 0.23 (expected 0.64), and it is still flat across
coefficients.
A guess, offered tentatively
The restitution target is built at constraint-generation time as
is_bouncy * restitution * projected_velocity, i.e. sampled from the closing
velocity when the contact is new. This beta also added a refresh_rhs_wo_bias
pass that recomputes the right-hand side from the bodies' current state during
relaxation. If that recomputation reaches the restitution term after the impact
velocity has been absorbed, the target would collapse to ~0 — which would match
what the numbers do. I have not verified that, so please treat it only as a
starting point.
Side effect worth mentioning
Because restitution is inert, a KinematicPositionBased body pushing a dynamic
one hands over exactly its own velocity, where 0.34.0 gives the expected
(1 + e) overshoot (measured 1.26x at e = 0.3, 1.75x at e = 0.8, against a
flat 1.00x in the beta). Same root cause as far as I can tell, just a more
visible symptom for character-controller-style setups.
Summary
In
0.35.0-beta.0,restitutionhas almost no effect: a ball dropped from 2 mrebounds ~1% of the drop regardless of its restitution coefficient.
0.34.0matches the textbook
e²on the same test, so this looks like a regressionintroduced in the beta.
Reproduced in pure Rust (no bindings involved), release build, macOS arm64,
rustc 1.92.0.
e²)(Numbers are the fraction of the 2 m drop recovered at the first apex after
impact.)
Repro
Two files,
cargo run --release. Flipping the one version line inCargo.tomlbetween
=0.35.0-beta.0and=0.34.0is the whole diff — the source compilesunchanged on both.
Cargo.tomlsrc/main.rsWhat I ruled out
worth the bindings agree:
@dimforge/rapier3d-compat@0.19.3(rapier 0.30)gives 0.07/0.25/0.66/0.94, and bindings built from
0.35.0-beta.0give 0.01across the board.
normalized_prediction_distanceback to0.002, ornormalized_allowed_linear_errorto0.001/0, changes little (0.01 → 0.06at best).
tell
solveis called withsolve_restitution: true(
staged_island_solver/solve.rs).contact_natural_frequency = 180withnum_solver_iterations = 8getsrestitution 0.8 to 0.23 (expected 0.64), and it is still flat across
coefficients.
A guess, offered tentatively
The restitution target is built at constraint-generation time as
is_bouncy * restitution * projected_velocity, i.e. sampled from the closingvelocity when the contact is new. This beta also added a
refresh_rhs_wo_biaspass that recomputes the right-hand side from the bodies' current state during
relaxation. If that recomputation reaches the restitution term after the impact
velocity has been absorbed, the target would collapse to ~0 — which would match
what the numbers do. I have not verified that, so please treat it only as a
starting point.
Side effect worth mentioning
Because restitution is inert, a
KinematicPositionBasedbody pushing a dynamicone hands over exactly its own velocity, where 0.34.0 gives the expected
(1 + e)overshoot (measured 1.26x ate = 0.3, 1.75x ate = 0.8, against aflat 1.00x in the beta). Same root cause as far as I can tell, just a more
visible symptom for character-controller-style setups.