Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make the constraints regularization coefficients confiugrable with angular frequency instead of explicit ERP #635

Merged
merged 7 commits into from
May 25, 2024
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,21 @@

### Modified

- The contact constraints regularization parameters have been changed from `erp/damping_ratio` to
`natural_frequency/damping_ratio`. This helps define them in a timestep-length independent way. The new variables
are named `IntegrationParameters::contact_natural_frequency` and `IntegrationParameters::contact_damping_ratio`.
- The `IntegrationParameters::normalized_max_penetration_correction` has been replaced
by `::normalized_max_corrective_velocity`
to make the parameter more timestep-length independent. It is now set to a non-infinite value to eliminate aggressive
"popping effects".
- The `Multibody::forward_kinematics` method will no longer automatically update the poses of the `RigidBody` associated
to each joint. Instead `Multibody::update_rigid_bodies` has to be called explicitly.
- The `Multibody::forward_kinematics` method will automatically adjust the multibody’s degrees of freedom if the root
rigid-body changed type (between dynamic and non-dynamic). It can also optionally apply the root’s rigid-body pose
instead of the root link’s pose (useful for example if you modified the root rigid-body pose externally and wanted
to propagate it to the multibody).
- Remove an internal special-case for contact constraints on fast contacts. The doesn’t seem necessary with the substep
solver.

## v0.19.0 (05 May 2024)

Expand Down
2 changes: 2 additions & 0 deletions benchmarks3d/all_benchmarks3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod joint_fixed3;
mod joint_prismatic3;
mod joint_revolute3;
mod keva3;
mod many_pyramids3;
mod many_sleep3;
mod many_static3;
mod pyramid3;
Expand Down Expand Up @@ -66,6 +67,7 @@ pub fn main() {
("ImpulseJoint fixed", joint_fixed3::init_world),
("ImpulseJoint revolute", joint_revolute3::init_world),
("ImpulseJoint prismatic", joint_prismatic3::init_world),
("Many pyramids", many_pyramids3::init_world),
("Keva tower", keva3::init_world),
];

Expand Down
80 changes: 80 additions & 0 deletions benchmarks3d/many_pyramids3.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use rapier3d::prelude::*;
use rapier_testbed3d::Testbed;

fn create_pyramid(
bodies: &mut RigidBodySet,
colliders: &mut ColliderSet,
offset: Vector<f32>,
stack_height: usize,
rad: f32,
) {
let shift = rad * 2.0;

for i in 0usize..stack_height {
for j in i..stack_height {
let fj = j as f32;
let fi = i as f32;
let x = (fi * shift / 2.0) + (fj - fi) * shift;
let y = fi * shift;

// Build the rigid body.
let rigid_body = RigidBodyBuilder::dynamic().translation(vector![x, y, 0.0] + offset);
let handle = bodies.insert(rigid_body);
let collider = ColliderBuilder::cuboid(rad, rad, rad);
colliders.insert_with_parent(collider, handle, bodies);
}
}
}

pub fn init_world(testbed: &mut Testbed) {
/*
* World
*/
let mut bodies = RigidBodySet::new();
let mut colliders = ColliderSet::new();
let impulse_joints = ImpulseJointSet::new();
let multibody_joints = MultibodyJointSet::new();

let rad = 0.5;
let pyramid_count = 40;
let spacing = 4.0;

/*
* Ground
*/
let ground_size = 50.0;
let ground_height = 0.1;

let rigid_body = RigidBodyBuilder::fixed().translation(vector![0.0, -ground_height, 0.0]);
let ground_handle = bodies.insert(rigid_body);
let collider = ColliderBuilder::cuboid(
ground_size,
ground_height,
pyramid_count as f32 * spacing / 2.0 + ground_size,
);
colliders.insert_with_parent(collider, ground_handle, &mut bodies);

/*
* Create the cubes
*/
for pyramid_index in 0..pyramid_count {
let bottomy = rad;
create_pyramid(
&mut bodies,
&mut colliders,
vector![
0.0,
bottomy,
(pyramid_index as f32 - pyramid_count as f32 / 2.0) * spacing
],
20,
rad,
);
}

/*
* Set up the testbed.
*/
testbed.set_world(bodies, colliders, impulse_joints, multibody_joints);
testbed.look_at(point![100.0, 100.0, 100.0], Point::origin());
}
141 changes: 94 additions & 47 deletions src/dynamics/integration_parameters.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::math::Real;
use na::RealField;
use std::num::NonZeroUsize;

// TODO: enabling the block solver in 3d introduces a lot of jitters in
Expand All @@ -9,9 +10,9 @@ pub(crate) static BLOCK_SOLVER_ENABLED: bool = cfg!(feature = "dim2");
#[derive(Copy, Clone, Debug)]
#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
pub struct IntegrationParameters {
/// The timestep length (default: `1.0 / 60.0`)
/// The timestep length (default: `1.0 / 60.0`).
pub dt: Real,
/// Minimum timestep size when using CCD with multiple substeps (default `1.0 / 60.0 / 100.0`)
/// Minimum timestep size when using CCD with multiple substeps (default: `1.0 / 60.0 / 100.0`).
///
/// When CCD with multiple substeps is enabled, the timestep is subdivided
/// into smaller pieces. This timestep subdivision won't generate timestep
Expand All @@ -23,43 +24,54 @@ pub struct IntegrationParameters {
/// to numerical instabilities.
pub min_ccd_dt: Real,

/// 0-1: multiplier for how much of the constraint violation (e.g. contact penetration)
/// will be compensated for during the velocity solve.
/// (default `0.1`).
pub erp: Real,
/// 0-1: the damping ratio used by the springs for Baumgarte constraints stabilization.
/// Lower values make the constraints more compliant (more "springy", allowing more visible penetrations
/// before stabilization).
/// (default `20.0`).
pub damping_ratio: 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-1: multiplier for how much of the joint violation
/// will be compensated for during the velocity solve.
/// (default `1.0`).
pub joint_erp: 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,

/// 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.
///
/// This should generally be set to 1. Can be set to 0 if using a large [`Self::erp`] value.
/// This should generally be set to 1.
///
/// (default `1.0`).
pub warmstart_coefficient: Real,

/// The approximate size of most dynamic objects in the scene.
///
/// This value is used internally to estimate some length-based tolerance. In particular, the
/// values [`IntegrationParameters::allowed_linear_error`],
/// [`IntegrationParameters::max_penetration_correction`],
/// [`IntegrationParameters::max_corrective_velocity`],
/// [`IntegrationParameters::prediction_distance`], [`RigidBodyActivation::linear_threshold`]
/// are scaled by this value implicitly.
///
/// This value can be understood as the number of units-per-meter in your physical world compared
/// to a human-sized world in meter. For example, in a 2d game, if your typical object size is 100
/// pixels, set the `[`Self::length_unit`]` parameter to 100.0. The physics engine will interpret
/// pixels, set the [`Self::length_unit`] parameter to 100.0. The physics engine will interpret
/// it as if 100 pixels is equivalent to 1 meter in its various internal threshold.
/// (default `1.0`).
pub length_unit: Real,
Expand All @@ -68,17 +80,17 @@ pub struct IntegrationParameters {
///
/// This value is implicitly scaled by [`IntegrationParameters::length_unit`].
pub normalized_allowed_linear_error: Real,
/// Maximum amount of penetration the solver will attempt to resolve in one timestep.
/// Maximum amount of penetration the solver will attempt to resolve in one timestep (default: `10.0`).
///
/// This value is implicitly scaled by [`IntegrationParameters::length_unit`].
pub normalized_max_penetration_correction: Real,
pub normalized_max_corrective_velocity: Real,
/// The maximal distance separating two objects that will generate predictive contacts (default: `0.002m`).
///
/// This value is implicitly scaled by [`IntegrationParameters::length_unit`].
pub normalized_prediction_distance: Real,
/// The number of solver iterations run by the constraints solver for calculating forces (default: `4`).
pub num_solver_iterations: NonZeroUsize,
/// Number of addition friction resolution iteration run during the last solver sub-step (default: `4`).
/// Number of addition friction resolution iteration run during the last solver sub-step (default: `0`).
pub num_additional_friction_iterations: usize,
/// Number of internal Project Gauss Seidel (PGS) iterations run at each solver iteration (default: `1`).
pub num_internal_pgs_iterations: usize,
Expand Down Expand Up @@ -123,20 +135,53 @@ impl IntegrationParameters {
}
}

/// The ERP coefficient, multiplied by the inverse timestep length.
pub fn erp_inv_dt(&self) -> Real {
self.erp * self.inv_dt()
/// 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 ERP coefficient, multiplied by the inverse timestep length.
/// 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 {
self.joint_erp * self.inv_dt()
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 constraints resolution.
pub fn cfm_factor(&self) -> Real {
/// 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.
let inv_erp_minus_one = 1.0 / self.erp - 1.0;
let inv_erp_minus_one = 1.0 / self.contact_erp() - 1.0;

// let stiffness = 4.0 * damping_ratio * damping_ratio * projected_mass
// / (dt * dt * inv_erp_minus_one * inv_erp_minus_one);
Expand All @@ -145,7 +190,10 @@ impl IntegrationParameters {
// 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.damping_ratio * self.damping_ratio);
/ ((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.
Expand All @@ -166,11 +214,14 @@ impl IntegrationParameters {
1.0 / (1.0 + cfm_coeff)
}

/// The CFM (constraints force mixing) coefficient applied to all joints for constraints regularization
/// 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::cfm_factor`.
let inv_erp_minus_one = 1.0 / self.joint_erp - 1.0;
let inv_erp_minus_one = 1.0 / self.joint_erp() - 1.0;
inv_erp_minus_one * inv_erp_minus_one
/ ((1.0 + inv_erp_minus_one)
* 4.0
Expand All @@ -186,11 +237,11 @@ impl IntegrationParameters {

/// Maximum amount of penetration the solver will attempt to resolve in one timestep.
///
/// This is equal to [`Self::normalized_max_penetration_correction`] multiplied by
/// This is equal to [`Self::normalized_max_corrective_velocity`] multiplied by
/// [`Self::length_unit`].
pub fn max_penetration_correction(&self) -> Real {
if self.normalized_max_penetration_correction != Real::MAX {
self.normalized_max_penetration_correction * self.length_unit
pub fn max_corrective_velocity(&self) -> Real {
if self.normalized_max_corrective_velocity != Real::MAX {
self.normalized_max_corrective_velocity * self.length_unit
} else {
Real::MAX
}
Expand All @@ -210,9 +261,9 @@ impl IntegrationParameters {
Self {
dt: 1.0 / 60.0,
min_ccd_dt: 1.0 / 60.0 / 100.0,
erp: 0.1,
damping_ratio: 20.0,
joint_erp: 1.0,
contact_natural_frequency: 30.0,
contact_damping_ratio: 5.0,
joint_natural_frequency: 1.0e6,
joint_damping_ratio: 1.0,
warmstart_coefficient: 1.0,
num_internal_pgs_iterations: 1,
Expand All @@ -226,7 +277,7 @@ impl IntegrationParameters {
// tons of islands, reducing SIMD parallelism opportunities.
min_island_size: 128,
normalized_allowed_linear_error: 0.001,
normalized_max_penetration_correction: Real::MAX,
normalized_max_corrective_velocity: 10.0,
normalized_prediction_distance: 0.002,
max_ccd_substeps: 1,
length_unit: 1.0,
Expand All @@ -240,8 +291,7 @@ impl IntegrationParameters {
/// warmstarting proves to be undesirable for your use-case.
pub fn tgs_soft_without_warmstart() -> Self {
Self {
erp: 0.6,
damping_ratio: 1.0,
contact_damping_ratio: 0.25,
warmstart_coefficient: 0.0,
num_additional_friction_iterations: 4,
..Self::tgs_soft()
Expand All @@ -253,12 +303,9 @@ impl IntegrationParameters {
/// This exists mainly for testing and comparison purpose.
pub fn pgs_legacy() -> Self {
Self {
erp: 0.8,
damping_ratio: 0.25,
warmstart_coefficient: 0.0,
num_additional_friction_iterations: 4,
num_solver_iterations: NonZeroUsize::new(1).unwrap(),
..Self::tgs_soft()
num_internal_pgs_iterations: 4,
..Self::tgs_soft_without_warmstart()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,8 @@ impl GenericOneBodyConstraintBuilder {
.unwrap()
.local_to_world;

self.inner.update_with_positions(
params,
solved_dt,
pos2,
self.ccd_thickness,
&mut constraint.inner,
);
self.inner
.update_with_positions(params, solved_dt, pos2, &mut constraint.inner);
}
}

Expand Down
Loading