Skip to content

Restitution has almost no effect in 0.35.0-beta.0 (regression from 0.34.0) #974

Description

@twpride

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 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 () 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, &params, &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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions