use super::contact::MAX_MANIFOLD_POINTS;
use super::math::Vec3;
use super::solver::SolverBody;
const MAX_PASSES: usize = 8;
const SETTLED: f32 = 1.0e-4;
pub(crate) type Impulses = [f32; MAX_MANIFOLD_POINTS];
pub(crate) type Errors = [Option<f32>; MAX_MANIFOLD_POINTS];
fn dot(row: &Impulses, impulses: &Impulses) -> f32 {
row.iter()
.zip(impulses)
.map(|(entry, impulse)| entry * impulse)
.sum()
}
#[derive(Debug, Clone, Copy, Default)]
struct Lever {
moment: Vec3,
spin: Vec3,
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct Coupling {
rows: [[f32; MAX_MANIFOLD_POINTS]; MAX_MANIFOLD_POINTS],
mass: [f32; MAX_MANIFOLD_POINTS],
}
impl Coupling {
pub(crate) fn build(
a: &SolverBody,
b: &SolverBody,
normal: Vec3,
anchors: &[(Vec3, Vec3)],
) -> Self {
let count = anchors.len().min(MAX_MANIFOLD_POINTS);
let mut levers = [(Lever::default(), Lever::default()); MAX_MANIFOLD_POINTS];
for (lever, &(ra, rb)) in levers.iter_mut().zip(anchors).take(count) {
let (ma, mb) = (ra.cross(normal), rb.cross(normal));
*lever = (
Lever {
moment: ma,
spin: a.inv_inertia.mul_vec3(ma),
},
Lever {
moment: mb,
spin: b.inv_inertia.mul_vec3(mb),
},
);
}
let linear = a.inv_mass + b.inv_mass;
let mut coupling = Coupling {
rows: [[0.0; MAX_MANIFOLD_POINTS]; MAX_MANIFOLD_POINTS],
mass: [0.0; MAX_MANIFOLD_POINTS],
};
for i in 0..count {
for j in 0..count {
coupling.rows[i][j] = linear
+ levers[i].0.moment.dot(levers[j].0.spin)
+ levers[i].1.moment.dot(levers[j].1.spin);
}
let diagonal = coupling.rows[i][i];
coupling.mass[i] = if diagonal > 0.0 { 1.0 / diagonal } else { 0.0 };
}
coupling
}
pub(crate) fn approach_from(&self, impulses: &Impulses) -> Impulses {
let mut out = [0.0; MAX_MANIFOLD_POINTS];
for (row, slot) in self.rows.iter().zip(&mut out) {
*slot = dot(row, impulses);
}
out
}
pub(crate) fn solve(&self, held: &Impulses, error: &Errors) -> Impulses {
let mut approach = [0.0; MAX_MANIFOLD_POINTS];
let mut mass = [0.0; MAX_MANIFOLD_POINTS];
for i in 0..MAX_MANIFOLD_POINTS {
if let Some(error) = error[i] {
approach[i] = error;
mass[i] = self.mass[i];
}
}
let mut delta = [0.0; MAX_MANIFOLD_POINTS];
if self.sweep(&mut delta, approach, held, &mass) <= SETTLED {
return delta;
}
for _ in 1..MAX_PASSES {
let mut asking = approach;
for (asking, row) in asking.iter_mut().zip(&self.rows) {
*asking += dot(row, &delta);
}
if self.sweep(&mut delta, asking, held, &mass) <= SETTLED {
break;
}
}
delta
}
fn sweep(
&self,
delta: &mut Impulses,
mut approach: Impulses,
held: &Impulses,
mass: &Impulses,
) -> f32 {
let mut moved = 0.0f32;
for i in 0..MAX_MANIFOLD_POINTS {
let carried = held[i] + delta[i];
let total = (carried - approach[i] * mass[i]).max(0.0);
let change = total - carried;
delta[i] = total - held[i];
for (asking, entry) in approach.iter_mut().zip(&self.rows[i]) {
*asking += entry * change;
}
moved = moved.max(change.abs() * self.rows[i][i]);
}
moved
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::physics::sim::body::Body;
use crate::physics::sim::math::{Quat, vec3};
use crate::physics::{ColliderShape, DynamicParams, LayerMask};
const HALF: f32 = 0.1;
fn small_box() -> SolverBody {
SolverBody::from_body(&Body::dynamic(
ColliderShape::Cuboid {
half_extents: [HALF, HALF, HALF],
},
vec3(0.0, HALF, 0.0),
Quat::IDENTITY,
DynamicParams {
mass: 1.0,
friction: 0.0,
restitution: 0.0,
gravity_scale: 1.0,
linear_damping: 0.0,
},
LayerMask::ALL,
))
}
fn floor() -> SolverBody {
SolverBody::from_body(&Body::fixed(
ColliderShape::Cuboid {
half_extents: [20.0, 5.0, 20.0],
},
vec3(0.0, -5.0, 0.0),
Quat::IDENTITY,
0.0,
LayerMask::ALL,
))
}
const FLOOR_CENTRE: Vec3 = vec3(0.0, -5.0, 0.0);
const BOX_CENTRE: Vec3 = vec3(0.0, HALF, 0.0);
fn corner_anchors() -> [(Vec3, Vec3); 4] {
[(-HALF, -HALF), (HALF, -HALF), (HALF, HALF), (-HALF, HALF)].map(|(x, z)| {
let point = vec3(x, 0.0, z);
(point - FLOOR_CENTRE, point - BOX_CENTRE)
})
}
fn corner_coupling() -> Coupling {
Coupling::build(&floor(), &small_box(), Vec3::Y, &corner_anchors())
}
#[test]
fn a_corner_patch_couples_symmetrically() {
let coupling = corner_coupling();
for i in 0..4 {
for j in 0..4 {
let (ij, ji) = (coupling.rows[i][j], coupling.rows[j][i]);
assert!((ij - ji).abs() < 1.0e-4, "[{i}][{j}] {ij} against {ji}");
}
}
assert!(
coupling.rows[0][2] < 0.0,
"opposite corners must oppose: {}",
coupling.rows[0][2]
);
assert!(
coupling.rows[0][0] > 0.0 && coupling.mass[0] > 0.0,
"a movable point has mass"
);
}
#[test]
fn a_face_on_impact_stops_a_body_without_spinning_it() {
let coupling = corner_coupling();
let delta = coupling.solve(&[0.0; 4], &[Some(-2000.0); 4]);
let total: f32 = delta.iter().sum();
assert!((total - 2000.0).abs() < 1.0, "the patch delivered {total}");
let torque = corner_anchors()
.iter()
.zip(&delta)
.fold(Vec3::ZERO, |sum, (&(_, rb), &impulse)| {
sum + rb.cross(Vec3::Y * impulse)
});
assert!(
torque.length() < 1.0,
"the patch left {torque:?} of spin behind"
);
}
#[test]
fn a_solved_patch_leaves_no_approach_behind() {
let coupling = corner_coupling();
let held = [0.0; 4];
let error = [Some(-2000.0), Some(-2000.0), Some(-1800.0), Some(-1800.0)];
let delta = coupling.solve(&held, &error);
let produced = coupling.approach_from(&delta);
for i in 0..4 {
let left = error[i].expect("every point is asked") + produced[i];
assert!(left.abs() < 1.0, "point {i} still approaches at {left}");
}
}
#[test]
fn a_separating_point_takes_nothing() {
let coupling = corner_coupling();
let held = [0.0; 4];
let error = [Some(-10.0), Some(-10.0), Some(50.0), Some(50.0)];
let delta = coupling.solve(&held, &error);
assert_eq!((delta[2], delta[3]), (0.0, 0.0), "{delta:?}");
assert!(delta[0] > 0.0 && delta[1] > 0.0, "{delta:?}");
}
#[test]
fn a_point_the_pass_leaves_alone_does_not_move() {
let coupling = corner_coupling();
let held = [0.5; 4];
let error = [Some(-10.0), None, Some(-10.0), None];
let delta = coupling.solve(&held, &error);
assert_eq!((delta[1], delta[3]), (0.0, 0.0), "{delta:?}");
}
#[test]
fn a_lone_point_matches_the_per_point_answer() {
let coupling = Coupling::build(&floor(), &small_box(), Vec3::Y, &corner_anchors()[..1]);
let delta = coupling.solve(&[0.0; 4], &[Some(-5.0), None, None, None]);
assert!(
(delta[0] - 5.0 * coupling.mass[0]).abs() < 1.0e-4,
"{delta:?}"
);
}
#[test]
fn an_immovable_pair_couples_nothing() {
let coupling = Coupling::build(&floor(), &floor(), Vec3::Y, &corner_anchors());
assert_eq!(coupling.mass[0], 0.0);
assert_eq!(coupling.solve(&[0.0; 4], &[Some(-100.0); 4]), [0.0; 4]);
}
}