use crate::physics::GRAVITY;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SimConfig {
pub gravity: f32,
pub substeps: u32,
pub contact_hertz: f32,
pub contact_damping_ratio: f32,
pub joint_hertz: f32,
pub joint_damping_ratio: f32,
pub max_push_velocity: f32,
pub linear_slop: f32,
pub speculative_margin: f32,
pub restitution_threshold: f32,
pub allow_sleep: bool,
pub sleep_linear_velocity: f32,
pub sleep_angular_velocity: f32,
pub time_to_sleep: f32,
pub bounds_margin: f32,
pub ccd_enabled: bool,
pub ccd_motion_ratio: f32,
}
impl Default for SimConfig {
fn default() -> Self {
SimConfig {
gravity: GRAVITY,
substeps: 4,
contact_hertz: 30.0,
contact_damping_ratio: 10.0,
joint_hertz: 60.0,
joint_damping_ratio: 0.5,
max_push_velocity: 3.0,
linear_slop: 0.005,
speculative_margin: 0.02,
restitution_threshold: 1.0,
allow_sleep: true,
sleep_linear_velocity: 0.05,
sleep_angular_velocity: 0.1,
time_to_sleep: 0.5,
bounds_margin: 0.05,
ccd_enabled: true,
ccd_motion_ratio: 0.5,
}
}
}
impl SimConfig {
pub(crate) fn substep_count(&self) -> u32 {
self.substeps.max(1)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Softness {
pub(crate) bias_rate: f32,
pub(crate) mass_scale: f32,
pub(crate) impulse_scale: f32,
}
impl Softness {
pub(crate) fn new(hertz: f32, damping_ratio: f32, h: f32) -> Self {
if hertz <= 0.0 || h <= 0.0 {
return Softness {
bias_rate: 0.0,
mass_scale: 1.0,
impulse_scale: 0.0,
};
}
let omega = 2.0 * core::f32::consts::PI * hertz;
let a1 = 2.0 * damping_ratio + h * omega;
let a2 = h * omega * a1;
let a3 = 1.0 / (1.0 + a2);
Softness {
bias_rate: omega / a1,
mass_scale: a2 * a3,
impulse_scale: a3,
}
}
pub(crate) const RIGID: Softness = Softness {
bias_rate: 0.0,
mass_scale: 1.0,
impulse_scale: 0.0,
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_matches_the_engine_gravity_and_simulates_something() {
let c = SimConfig::default();
assert_eq!(c.gravity, GRAVITY);
assert!(c.substep_count() >= 1);
assert!(c.speculative_margin > c.linear_slop);
assert!(
c.joint_hertz > c.contact_hertz,
"a joint has to be held stiffer than a contact"
);
assert!(c.ccd_enabled, "a fast body must not pass through geometry");
assert!(
c.ccd_motion_ratio > 0.0 && c.ccd_motion_ratio <= 1.0,
"past one width of motion a body can tunnel, so the gate has to \
be armed before then"
);
}
#[test]
fn a_zero_substep_config_still_takes_one_pass() {
let c = SimConfig {
substeps: 0,
..SimConfig::default()
};
assert_eq!(c.substep_count(), 1);
}
#[test]
fn softness_coefficients_stay_in_the_solvers_range() {
for hertz in [1.0, 30.0, 240.0] {
for zeta in [0.5, 1.0, 10.0] {
let s = Softness::new(hertz, zeta, 1.0 / 240.0);
assert!(s.bias_rate > 0.0, "{hertz} {zeta}: {s:?}");
assert!((0.0..=1.0).contains(&s.mass_scale), "{hertz} {zeta}: {s:?}");
assert!(
(0.0..=1.0).contains(&s.impulse_scale),
"{hertz} {zeta}: {s:?}"
);
assert!(
(s.mass_scale + s.impulse_scale - 1.0).abs() < 1.0e-5,
"the two scales partition one impulse: {s:?}"
);
}
}
}
#[test]
fn a_higher_frequency_raises_the_bias_rate() {
let h = 1.0 / 240.0;
let soft = Softness::new(10.0, 10.0, h);
let stiff = Softness::new(60.0, 10.0, h);
assert!(stiff.bias_rate > soft.bias_rate, "{soft:?} {stiff:?}");
}
#[test]
fn a_disabled_frequency_degrades_to_the_rigid_solve() {
assert_eq!(Softness::new(0.0, 1.0, 1.0 / 240.0), Softness::RIGID);
assert_eq!(Softness::new(30.0, 1.0, 0.0), Softness::RIGID);
}
}