use he3d_math::Vec3;
use he3d_math::scalar::sqrt;
use crate::body::Body;
use crate::contact::ContactConstraint;
use crate::storage::{Slot, constraint_bodies, constraint_bodies_ref};
pub(crate) const DEFAULT_VELOCITY_ITERATIONS: u32 = 12;
const BAUMGARTE: f32 = 0.2;
const PENETRATION_SLOP: f32 = 0.005;
const RESTITUTION_THRESHOLD: f32 = 1.0;
const POSITION_ITERATIONS: u32 = 3;
const INFINITE_EFFECTIVE_MASS: f32 = 1e-8;
const MINIMUM_WARM_START_IMPULSE_SQUARED: f32 = 1e-8;
#[derive(Clone, Copy, Debug)]
pub(crate) struct SolverSettings {
pub(crate) velocity_iterations: u32,
}
impl Default for SolverSettings {
fn default() -> Self {
Self {
velocity_iterations: DEFAULT_VELOCITY_ITERATIONS,
}
}
}
pub(crate) fn is_solvable(first: &Body, second: &Body) -> bool {
first.motion.is_pushable() || second.motion.is_pushable()
}
pub(crate) fn prepare(slots: &[Slot], constraints: &mut [ContactConstraint], delta_time: f32) {
let inverse_delta = 1.0 / delta_time;
for constraint in constraints.iter_mut() {
let Some((first, second)) =
constraint_bodies_ref(slots, constraint.a as usize, constraint.b as usize)
else {
continue;
};
let normal = constraint.normal;
let tangents = constraint.tangents;
let inverse_mass_sum = first.mass.inv_mass + second.map_or(0.0, |body| body.mass.inv_mass);
for point in constraint.points_mut() {
let arm_first = point.anchor_a;
let arm_second = point.anchor_b;
let normal_term = normal_effective_mass(
first,
second,
arm_first,
arm_second,
normal,
inverse_mass_sum,
);
point.normal_mass = reciprocal_or_zero(normal_term);
for (index, tangent) in tangents.into_iter().enumerate() {
let term = normal_effective_mass(
first,
second,
arm_first,
arm_second,
tangent,
inverse_mass_sum,
);
point.tangent_mass[index] = reciprocal_or_zero(term);
}
let approach = relative_velocity(first, second, arm_first, arm_second, normal);
let mut bias = 0.0;
if approach < -RESTITUTION_THRESHOLD {
let restitution = second.map_or(0.0, |body| body.restitution);
let restitution = first.restitution.max(restitution);
if restitution > 0.0 {
bias = -restitution * approach;
}
}
point.bias = bias;
point.position_bias =
BAUMGARTE * inverse_delta * (point.depth - PENETRATION_SLOP).max(0.0);
}
}
}
pub(crate) fn warm_start(slots: &mut [Slot], constraints: &[ContactConstraint]) {
for constraint in constraints {
let (first_index, second_index) = (constraint.a as usize, constraint.b as usize);
let normal = constraint.normal;
let tangents = constraint.tangents;
for point in constraint.points() {
let mut impulse = normal * point.normal_impulse;
for (index, tangent) in tangents.into_iter().enumerate() {
impulse += tangent * point.tangent_impulse[index];
}
if impulse.length_squared() <= MINIMUM_WARM_START_IMPULSE_SQUARED {
continue;
}
let Some((first, second)) = constraint_bodies(slots, first_index, second_index) else {
continue;
};
apply_impulse(first, second, point.anchor_a, point.anchor_b, impulse);
}
}
}
pub(crate) fn solve_velocities(
slots: &mut [Slot],
constraints: &mut [ContactConstraint],
settings: SolverSettings,
) {
for _ in 0..settings.velocity_iterations {
for constraint in constraints.iter_mut() {
let (first_index, second_index) = (constraint.a as usize, constraint.b as usize);
let normal = constraint.normal;
let tangents = constraint.tangents;
let friction = constraint.friction;
for point in constraint.points_mut() {
{
let Some((first, second)) = constraint_bodies(slots, first_index, second_index)
else {
continue;
};
let relative = relative_velocity(
first,
second.as_deref(),
point.anchor_a,
point.anchor_b,
normal,
);
let lambda = -point.normal_mass * (relative - point.bias);
let previous = point.normal_impulse;
point.normal_impulse = (previous + lambda).max(0.0);
let delta = point.normal_impulse - previous;
apply_impulse(
first,
second,
point.anchor_a,
point.anchor_b,
normal * delta,
);
}
for (index, tangent) in tangents.into_iter().enumerate() {
let Some((first, second)) = constraint_bodies(slots, first_index, second_index)
else {
continue;
};
let relative = relative_velocity(
first,
second.as_deref(),
point.anchor_a,
point.anchor_b,
tangent,
);
let lambda = -point.tangent_mass[index] * relative;
let limit = friction * point.normal_impulse;
let previous = point.tangent_impulse[index];
point.tangent_impulse[index] = (previous + lambda).clamp(-limit, limit);
let delta = point.tangent_impulse[index] - previous;
apply_impulse(
first,
second,
point.anchor_a,
point.anchor_b,
tangent * delta,
);
}
}
}
}
}
pub(crate) fn solve_positions(slots: &mut [Slot], constraints: &mut [ContactConstraint]) {
for _ in 0..POSITION_ITERATIONS {
for constraint in constraints.iter_mut() {
let (first_index, second_index) = (constraint.a as usize, constraint.b as usize);
let normal = constraint.normal;
for point in constraint.points_mut() {
let Some((first, second)) = constraint_bodies(slots, first_index, second_index) else {
continue;
};
let relative = pseudo_relative_velocity(
first,
second.as_deref(),
point.anchor_a,
point.anchor_b,
normal,
);
let lambda = -point.normal_mass * (relative - point.position_bias);
let previous = point.position_impulse;
point.position_impulse = (previous + lambda).max(0.0);
let delta = point.position_impulse - previous;
apply_pseudo_impulse(
first,
second,
point.anchor_a,
point.anchor_b,
normal * delta,
);
}
}
}
}
fn relative_velocity(
first: &Body,
second: Option<&Body>,
arm_first: Vec3,
arm_second: Vec3,
axis: Vec3,
) -> f32 {
let first_share = first.point_velocity(arm_first).dot(axis);
let second_share = second.map_or(0.0, |body| body.point_velocity(arm_second).dot(axis));
second_share - first_share
}
fn pseudo_relative_velocity(
first: &Body,
second: Option<&Body>,
arm_first: Vec3,
arm_second: Vec3,
axis: Vec3,
) -> f32 {
let first_share = first.pseudo_point_velocity(arm_first).dot(axis);
let second_share = second.map_or(0.0, |body| body.pseudo_point_velocity(arm_second).dot(axis));
second_share - first_share
}
fn normal_effective_mass(
first: &Body,
second: Option<&Body>,
arm_first: Vec3,
arm_second: Vec3,
axis: Vec3,
inverse_mass_sum: f32,
) -> f32 {
let first_arm_cross = arm_first.cross(axis);
let second_term = second.map_or(0.0, |body| {
let second_arm_cross = arm_second.cross(axis);
second_arm_cross.dot(body.apply_inv_inertia(second_arm_cross))
});
inverse_mass_sum + first_arm_cross.dot(first.apply_inv_inertia(first_arm_cross)) + second_term
}
fn reciprocal_or_zero(value: f32) -> f32 {
if value.is_finite() && value > INFINITE_EFFECTIVE_MASS {
1.0 / value
} else {
0.0
}
}
fn apply_impulse(
first: &mut Body,
second: Option<&mut Body>,
arm_first: Vec3,
arm_second: Vec3,
impulse: Vec3,
) {
first.linear_velocity -= impulse * first.mass.inv_mass;
first.angular_velocity -= first.apply_inv_inertia(arm_first.cross(impulse));
if let Some(second) = second {
second.linear_velocity += impulse * second.mass.inv_mass;
second.angular_velocity += second.apply_inv_inertia(arm_second.cross(impulse));
}
}
fn apply_pseudo_impulse(
first: &mut Body,
second: Option<&mut Body>,
arm_first: Vec3,
arm_second: Vec3,
impulse: Vec3,
) {
first.pseudo_linear_velocity -= impulse * first.mass.inv_mass;
first.pseudo_angular_velocity -= first.apply_inv_inertia(arm_first.cross(impulse));
if let Some(second) = second {
second.pseudo_linear_velocity += impulse * second.mass.inv_mass;
second.pseudo_angular_velocity += second.apply_inv_inertia(arm_second.cross(impulse));
}
}
pub(crate) fn combine_friction(first: f32, second: f32) -> f32 {
sqrt(first.max(0.0) * second.max(0.0))
}
#[cfg(test)]
mod tests {
use super::{combine_friction, is_solvable, reciprocal_or_zero};
use crate::body::{Body, BodyDesc};
use crate::shape::Shape;
fn body(motion: fn(Shape) -> BodyDesc) -> Body {
Body::from_desc(&motion(Shape::sphere(0.5)))
}
#[test]
fn two_immovable_bodies_are_not_solvable() {
let stationary = body(BodyDesc::stationary);
let kinematic = body(BodyDesc::kinematic);
let dynamic = Body::from_desc(&BodyDesc::dynamic(Shape::sphere(0.5), 1.0));
assert!(!is_solvable(&stationary, &kinematic));
assert!(is_solvable(&stationary, &dynamic));
assert!(is_solvable(&dynamic, &kinematic));
}
#[test]
fn friction_combines_through_a_geometric_mean() {
assert!((combine_friction(0.0, 0.9) - 0.0).abs() < 1e-6);
assert!((combine_friction(0.25, 1.0) - 0.5).abs() < 1e-6);
assert!((combine_friction(-1.0, 1.0) - 0.0).abs() < 1e-6);
}
#[test]
fn a_degenerate_effective_mass_yields_no_impulse() {
assert!(reciprocal_or_zero(0.0).abs() < 1e-9);
assert!(reciprocal_or_zero(-1.0).abs() < 1e-9);
assert!(reciprocal_or_zero(f32::NAN).abs() < 1e-9);
assert!((reciprocal_or_zero(2.0) - 0.5).abs() < 1e-6);
}
}