Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
- Rename `RigidBodyChanges::CHANGED` to `::IN_CHANGED_SET` to make its meaning more precise.
- Fix colliders ignoring user-changes after the first simulation step.
- Fix broad-phase incorrectly taking into account disabled colliders attached to an enabled dynamic rigid-body.
- Grouped `IntegrationParameters::contact_natural_frequency` and `IntegrationParameters::contact_damping_ratio` behind
a single `IntegrationParameters::contact_softness` (#789).
- Removed the `Integration_parameters` methods related to `erp` and `cfm`. They are now methods of the
`IntegrationParameters::softness` and `GenericJoint::softness` fields (#789).
- Removed `IntegrationParameters::joint_natural_frequency` and `IntegrationParameters::joint_damping_ratio` in favor of
per-joint softness coefficients `GenericJoint::softness` (#789).

## v0.30.1 (17 Oct. 2025)

Expand Down
24 changes: 22 additions & 2 deletions examples2d/joints2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ pub fn init_world(testbed: &mut Testbed) {
let mut impulse_joints = ImpulseJointSet::new();
let multibody_joints = MultibodyJointSet::new();

/*
* Enable/disable softness.
*/
let settings = testbed.example_settings_mut();
let variable_softness = settings.get_or_set_bool("Variable softness", false);

/*
* Create the balls
*/
Expand Down Expand Up @@ -41,18 +47,32 @@ pub fn init_world(testbed: &mut Testbed) {
let collider = ColliderBuilder::ball(rad);
colliders.insert_with_parent(collider, child_handle, &mut bodies);

let softness = if variable_softness {
// If variable softness is enabled, joints closer to the fixed body are softer.
SpringCoefficients {
natural_frequency: 5.0 * (i.max(k) + 1) as f32,
damping_ratio: 0.1 * (i.max(k) + 1) as f32,
}
} else {
SpringCoefficients::joint_defaults()
};

// Vertical joint.
if i > 0 {
let parent_handle = *body_handles.last().unwrap();
let joint = RevoluteJointBuilder::new().local_anchor2(point![0.0, shift]);
let joint = RevoluteJointBuilder::new()
.local_anchor2(point![0.0, shift])
.softness(softness);
impulse_joints.insert(parent_handle, child_handle, joint, true);
}

// Horizontal joint.
if k > 0 {
let parent_index = body_handles.len() - numi;
let parent_handle = body_handles[parent_index];
let joint = RevoluteJointBuilder::new().local_anchor2(point![-shift, 0.0]);
let joint = RevoluteJointBuilder::new()
.local_anchor2(point![-shift, 0.0])
.softness(softness);
impulse_joints.insert(parent_handle, child_handle, joint, true);
}

Expand Down
26 changes: 24 additions & 2 deletions examples2d/pin_slot_joint2.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use rapier2d::prelude::*;
use crate::utils::character;
use crate::utils::character::CharacterControlMode;
use rapier_testbed2d::Testbed;
use rapier2d::control::{KinematicCharacterController, PidController};
use rapier2d::prelude::*;

pub fn init_world(testbed: &mut Testbed) {
/*
Expand Down Expand Up @@ -77,10 +80,29 @@ pub fn init_world(testbed: &mut Testbed) {
.build();
impulse_joints.insert(character_handle, cube_handle, pin_slot_joint, true);

/*
* Callback to update the character based on user inputs.
*/
let mut control_mode = CharacterControlMode::Kinematic(0.1);
let mut controller = KinematicCharacterController::default();
let mut pid = PidController::default();

testbed.add_callback(move |graphics, physics, _, _| {
if let Some(graphics) = graphics {
character::update_character(
graphics,
physics,
&mut control_mode,
&mut controller,
&mut pid,
character_handle,
);
}
});

/*
* Set up the testbed.
*/
testbed.set_world(bodies, colliders, impulse_joints, multibody_joints);
testbed.set_character_body(character_handle);
testbed.look_at(point![0.0, 1.0], 100.0);
}
253 changes: 114 additions & 139 deletions src/dynamics/integration_parameters.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use crate::math::Real;
use na::RealField;

#[cfg(doc)]
use super::RigidBodyActivation;
use crate::math::Real;
use simba::simd::SimdRealField;

// TODO: enabling the block solver in 3d introduces a lot of jitters in
// the 3D domino demo. So for now we dont enable it in 3D.
Expand Down Expand Up @@ -30,6 +29,114 @@ pub enum FrictionModel {
Coulomb,
}

#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
// TODO: we should be able to combine this with MotorModel.
/// Coefficients for a spring, typically used for configuring constraint softness for contacts and
/// joints.
pub struct SpringCoefficients<N> {
/// Sets the natural frequency (Hz) of the spring-like constraint.
///
/// Higher values make the constraint stiffer and resolve constraint violations more quickly.
pub natural_frequency: N,
/// Sets the damping ratio for the spring-like constraint.
///
/// Larger values make the joint more compliant (allowing more drift before stabilization).
pub damping_ratio: N,
}

impl<N: SimdRealField<Element = Real> + Copy> SpringCoefficients<N> {
/// Initializes spring coefficients from the spring natural frequency and damping ratio.
pub fn new(natural_frequency: N, damping_ratio: N) -> Self {
Self {
natural_frequency,
damping_ratio,
}
}

/// Default softness coefficients for contacts.
pub fn contact_defaults() -> Self {
Self {
natural_frequency: N::splat(30.0),
damping_ratio: N::splat(5.0),
}
}

/// Default softness coefficients for joints.
pub fn joint_defaults() -> Self {
Self {
natural_frequency: N::splat(1.0e6),
damping_ratio: N::splat(1.0),
}
}

/// The contact’s spring angular frequency for constraints regularization.
pub fn angular_frequency(&self) -> N {
self.natural_frequency * N::simd_two_pi()
}

/// The [`Self::erp`] coefficient, multiplied by the inverse timestep length.
pub fn erp_inv_dt(&self, dt: N) -> N {
let ang_freq = self.angular_frequency();
ang_freq / (dt * ang_freq + N::splat(2.0) * self.damping_ratio)
}

/// The effective Error Reduction Parameter applied for calculating regularization forces.
///
/// This parameter is computed automatically from [`Self::natural_frequency`],
/// [`Self::damping_ratio`] and the substep length.
pub fn erp(&self, dt: N) -> N {
dt * self.erp_inv_dt(dt)
}

/// Compute CFM assuming a critically damped spring multiplied by the damping ratio.
///
/// This coefficient softens the impulse applied at each solver iteration.
pub fn cfm_coeff(&self, dt: N) -> N {
let one = N::one();
let erp = self.erp(dt);
let erp_is_not_zero = erp.simd_ne(N::zero());
let inv_erp_minus_one = one / erp - one;

// let stiffness = 4.0 * damping_ratio * damping_ratio * projected_mass
// / (dt * dt * inv_erp_minus_one * inv_erp_minus_one);
// let damping = 4.0 * damping_ratio * damping_ratio * projected_mass
// / (dt * inv_erp_minus_one);
// let cfm = 1.0 / (dt * dt * stiffness + dt * damping);
// NOTE: This simplifies to cfm = cfm_coeff / projected_mass:
let result = inv_erp_minus_one * inv_erp_minus_one
/ ((one + inv_erp_minus_one) * N::splat(4.0) * self.damping_ratio * self.damping_ratio);
result.select(erp_is_not_zero, N::zero())
}

/// The CFM factor to be used in the constraint resolution.
///
/// This parameter is computed automatically from [`Self::natural_frequency`],
/// [`Self::damping_ratio`] and the substep length.
pub fn cfm_factor(&self, dt: N) -> N {
let one = N::one();
let cfm_coeff = self.cfm_coeff(dt);

// We use this coefficient inside the impulse resolution.
// Surprisingly, several simplifications happen there.
// Let `m` the projected mass of the constraint.
// Let `m’` the projected mass that includes CFM: `m’ = 1 / (1 / m + cfm_coeff / m) = m / (1 + cfm_coeff)`
// We have:
// new_impulse = old_impulse - m’ (delta_vel - cfm * old_impulse)
// = old_impulse - m / (1 + cfm_coeff) * (delta_vel - cfm_coeff / m * old_impulse)
// = old_impulse * (1 - cfm_coeff / (1 + cfm_coeff)) - m / (1 + cfm_coeff) * delta_vel
// = old_impulse / (1 + cfm_coeff) - m * delta_vel / (1 + cfm_coeff)
// = 1 / (1 + cfm_coeff) * (old_impulse - m * delta_vel)
// So, setting cfm_factor = 1 / (1 + cfm_coeff).
// We obtain:
// new_impulse = cfm_factor * (old_impulse - m * delta_vel)
//
// The value returned by this function is this cfm_factor that can be used directly
// in the constraint solver.
one / (one + cfm_coeff)
}
}

/// Configuration parameters that control the physics simulation quality and behavior.
///
/// These parameters affect how the physics engine advances time, resolves collisions, and
Expand Down Expand Up @@ -80,34 +187,8 @@ pub struct IntegrationParameters {
/// to numerical instabilities.
pub min_ccd_dt: Real,

/// > 0: the damping ratio used by the springs for contact constraint stabilization.
///
/// Larger values make the constraints more compliant (allowing more visible
/// penetrations before stabilization).
/// (default `5.0`).
pub contact_damping_ratio: Real,

/// > 0: the natural frequency used by the springs for contact constraint regularization.
///
/// Increasing this value will make it so that penetrations get fixed more quickly at the
/// expense of potential jitter effects due to overshooting. In order to make the simulation
/// look stiffer, it is recommended to increase the [`Self::contact_damping_ratio`] instead of this
/// value.
/// (default: `30.0`).
pub contact_natural_frequency: Real,

/// > 0: the natural frequency used by the springs for joint constraint regularization.
///
/// Increasing this value will make it so that penetrations get fixed more quickly.
/// (default: `1.0e6`).
pub joint_natural_frequency: Real,

/// The fraction of critical damping applied to the joint for constraints regularization.
///
/// Larger values make the constraints more compliant (allowing more joint
/// drift before stabilization).
/// (default `1.0`).
pub joint_damping_ratio: Real,
/// Softness coefficients for contact constraints.
pub contact_softness: SpringCoefficients<Real>,

/// The coefficient in `[0, 1]` applied to warmstart impulses, i.e., impulses that are used as the
/// initial solution (instead of 0) at the next simulation step.
Expand Down Expand Up @@ -193,110 +274,7 @@ impl IntegrationParameters {
}
}

/// The contact’s spring angular frequency for constraints regularization.
pub fn contact_angular_frequency(&self) -> Real {
self.contact_natural_frequency * Real::two_pi()
}

/// The [`Self::contact_erp`] coefficient, multiplied by the inverse timestep length.
pub fn contact_erp_inv_dt(&self) -> Real {
let ang_freq = self.contact_angular_frequency();
ang_freq / (self.dt * ang_freq + 2.0 * self.contact_damping_ratio)
}

/// The effective Error Reduction Parameter applied for calculating regularization forces
/// on contacts.
///
/// This parameter is computed automatically from [`Self::contact_natural_frequency`],
/// [`Self::contact_damping_ratio`] and the substep length.
pub fn contact_erp(&self) -> Real {
self.dt * self.contact_erp_inv_dt()
}

/// The joint’s spring angular frequency for constraint regularization.
pub fn joint_angular_frequency(&self) -> Real {
self.joint_natural_frequency * Real::two_pi()
}

/// The [`Self::joint_erp`] coefficient, multiplied by the inverse timestep length.
pub fn joint_erp_inv_dt(&self) -> Real {
let ang_freq = self.joint_angular_frequency();
ang_freq / (self.dt * ang_freq + 2.0 * self.joint_damping_ratio)
}

/// The effective Error Reduction Parameter applied for calculating regularization forces
/// on joints.
///
/// This parameter is computed automatically from [`Self::joint_natural_frequency`],
/// [`Self::joint_damping_ratio`] and the substep length.
pub fn joint_erp(&self) -> Real {
self.dt * self.joint_erp_inv_dt()
}

/// The CFM factor to be used in the constraint resolution.
///
/// This parameter is computed automatically from [`Self::contact_natural_frequency`],
/// [`Self::contact_damping_ratio`] and the substep length.
pub fn contact_cfm_factor(&self) -> Real {
// Compute CFM assuming a critically damped spring multiplied by the damping ratio.
// The logic is similar to [`Self::joint_cfm_coeff`].
let contact_erp = self.contact_erp();
if contact_erp == 0.0 {
return 0.0;
}
let inv_erp_minus_one = 1.0 / contact_erp - 1.0;

// let stiffness = 4.0 * damping_ratio * damping_ratio * projected_mass
// / (dt * dt * inv_erp_minus_one * inv_erp_minus_one);
// let damping = 4.0 * damping_ratio * damping_ratio * projected_mass
// / (dt * inv_erp_minus_one);
// let cfm = 1.0 / (dt * dt * stiffness + dt * damping);
// NOTE: This simplifies to cfm = cfm_coeff / projected_mass:
let cfm_coeff = inv_erp_minus_one * inv_erp_minus_one
/ ((1.0 + inv_erp_minus_one)
* 4.0
* self.contact_damping_ratio
* self.contact_damping_ratio);

// Furthermore, we use this coefficient inside of the impulse resolution.
// Surprisingly, several simplifications happen there.
// Let `m` the projected mass of the constraint.
// Let `m’` the projected mass that includes CFM: `m’ = 1 / (1 / m + cfm_coeff / m) = m / (1 + cfm_coeff)`
// We have:
// new_impulse = old_impulse - m’ (delta_vel - cfm * old_impulse)
// = old_impulse - m / (1 + cfm_coeff) * (delta_vel - cfm_coeff / m * old_impulse)
// = old_impulse * (1 - cfm_coeff / (1 + cfm_coeff)) - m / (1 + cfm_coeff) * delta_vel
// = old_impulse / (1 + cfm_coeff) - m * delta_vel / (1 + cfm_coeff)
// = 1 / (1 + cfm_coeff) * (old_impulse - m * delta_vel)
// So, setting cfm_factor = 1 / (1 + cfm_coeff).
// We obtain:
// new_impulse = cfm_factor * (old_impulse - m * delta_vel)
//
// The value returned by this function is this cfm_factor that can be used directly
// in the constraint solver.
1.0 / (1.0 + cfm_coeff)
}

/// The CFM (constraints force mixing) coefficient applied to all joints for constraints regularization.
///
/// This parameter is computed automatically from [`Self::joint_natural_frequency`],
/// [`Self::joint_damping_ratio`] and the substep length.
pub fn joint_cfm_coeff(&self) -> Real {
// Compute CFM assuming a critically damped spring multiplied by the damping ratio.
// The logic is similar to `Self::contact_cfm_factor`.
let joint_erp = self.joint_erp();
if joint_erp == 0.0 {
return 0.0;
}
let inv_erp_minus_one = 1.0 / joint_erp - 1.0;
inv_erp_minus_one * inv_erp_minus_one
/ ((1.0 + inv_erp_minus_one)
* 4.0
* self.joint_damping_ratio
* self.joint_damping_ratio)
}

/// Amount of penetration the engine won’t attempt to correct (default: `0.001` multiplied by
/// Amount of penetration the engine won't attempt to correct (default: `0.001` multiplied by
/// [`Self::length_unit`]).
pub fn allowed_linear_error(&self) -> Real {
self.normalized_allowed_linear_error * self.length_unit
Expand Down Expand Up @@ -326,10 +304,7 @@ impl Default for IntegrationParameters {
Self {
dt: 1.0 / 60.0,
min_ccd_dt: 1.0 / 60.0 / 100.0,
contact_natural_frequency: 30.0,
contact_damping_ratio: 5.0,
joint_natural_frequency: 1.0e6,
joint_damping_ratio: 1.0,
contact_softness: SpringCoefficients::contact_defaults(),
warmstart_coefficient: 1.0,
num_internal_pgs_iterations: 1,
num_internal_stabilization_iterations: 1,
Expand Down
Loading