use crate::ColliderShape;
use crate::sim::collide::{Pose, support_vertex};
use crate::sim::math::Vec3;
use super::simplex::closest_to_origin;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Support {
half: Vec3,
pub(crate) radius: f32,
pub(crate) pose: Pose,
pub(crate) shape: ColliderShape,
}
impl Support {
pub(crate) fn new(shape: &ColliderShape, pose: Pose) -> Self {
let (half, radius) = match *shape {
ColliderShape::Ball { radius } => (Vec3::ZERO, libm::fabsf(radius)),
ColliderShape::Capsule {
half_height,
radius,
} => (
Vec3::from_array([0.0, libm::fabsf(half_height), 0.0]),
libm::fabsf(radius),
),
ColliderShape::Cuboid { half_extents } => (Vec3::from_array(half_extents).abs(), 0.0),
};
Support {
half,
radius,
pose,
shape: *shape,
}
}
fn core_support(&self, direction: Vec3) -> Vec3 {
let local = self.pose.rotation.inverse_rotate(direction);
self.pose.to_world(support_vertex(self.half, local))
}
pub(crate) fn contains(&self, point: Vec3) -> bool {
let local = self.pose.to_local(point);
let nearest = local.clamp(-self.half, self.half);
(local - nearest).length_squared() <= self.radius * self.radius
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Separation {
pub(crate) gap: f32,
pub(crate) direction: Vec3,
pub(crate) on_b: Vec3,
}
impl Separation {
pub(crate) fn is_entangled(&self) -> bool {
self.direction == Vec3::ZERO
}
}
const MAX_ITERATIONS: usize = 32;
const TOLERANCE: f32 = 1.0e-6;
pub(crate) fn separation(a: &Support, b: &Support) -> Separation {
let radii = a.radius + b.radius;
let mut cso = [Vec3::ZERO; 4];
let mut witness_b = [Vec3::ZERO; 4];
let start = (a.pose.position - b.pose.position).normalize_or(Vec3::X);
(cso[0], witness_b[0]) = support(a, b, start);
let mut count = 1usize;
let mut closest = closest_to_origin(&cso[..count]);
let mut proven_apart = false;
for _ in 0..MAX_ITERATIONS {
let v = closest.point;
let v_dot_v = v.length_squared();
if closest.encloses_origin || v_dot_v <= f32::MIN_POSITIVE {
return entangled(radii);
}
let (point, on_b) = support(a, b, -v);
let bound = v.dot(point);
proven_apart = bound > 0.0;
if v_dot_v - bound <= TOLERANCE * v_dot_v || cso[..count].contains(&point) {
break;
}
cso[count] = point;
witness_b[count] = on_b;
count += 1;
closest = closest_to_origin(&cso[..count]);
if closest.encloses_origin {
return entangled(radii);
}
count = closest.count;
let (mut kept_cso, mut kept_witness) = ([Vec3::ZERO; 4], [Vec3::ZERO; 4]);
for i in 0..count {
kept_cso[i] = cso[closest.keep[i]];
kept_witness[i] = witness_b[closest.keep[i]];
closest.keep[i] = i;
}
(cso, witness_b) = (kept_cso, kept_witness);
}
let core_distance = closest.point.length();
if !proven_apart || core_distance <= f32::MIN_POSITIVE {
return entangled(radii);
}
let direction = closest.point * (1.0 / core_distance);
let mut on_b_core = Vec3::ZERO;
for i in 0..closest.count {
on_b_core += witness_b[closest.keep[i]] * closest.weights[i];
}
Separation {
gap: core_distance - radii,
direction,
on_b: on_b_core + direction * b.radius,
}
}
fn support(a: &Support, b: &Support, direction: Vec3) -> (Vec3, Vec3) {
let on_a = a.core_support(direction);
let on_b = b.core_support(-direction);
(on_a - on_b, on_b)
}
fn entangled(radii: f32) -> Separation {
Separation {
gap: -radii,
direction: Vec3::ZERO,
on_b: Vec3::ZERO,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sim::math::{Quat, vec3};
const BALL: ColliderShape = ColliderShape::Ball { radius: 0.5 };
const CUBE: ColliderShape = ColliderShape::Cuboid {
half_extents: [0.5, 0.5, 0.5],
};
const CAPSULE: ColliderShape = ColliderShape::Capsule {
half_height: 0.5,
radius: 0.25,
};
fn at(shape: &ColliderShape, position: Vec3) -> Support {
Support::new(
shape,
Pose {
position,
rotation: Quat::IDENTITY,
},
)
}
fn turned(shape: &ColliderShape, position: Vec3, euler_deg: [f32; 3]) -> Support {
Support::new(
shape,
Pose {
position,
rotation: Quat::from_euler_deg(euler_deg),
},
)
}
#[test]
fn two_balls_are_apart_by_the_gap_between_their_surfaces() {
let a = at(&BALL, vec3(0.0, 3.0, 0.0));
let b = at(&BALL, Vec3::ZERO);
let s = separation(&a, &b);
assert!((s.gap - 2.0).abs() < 1.0e-4, "{s:?}");
assert!((s.direction - Vec3::Y).length() < 1.0e-4, "{s:?}");
assert!((s.on_b - vec3(0.0, 0.5, 0.0)).length() < 1.0e-4, "{s:?}");
}
#[test]
fn overlapping_balls_report_a_negative_gap_and_still_give_a_direction() {
let s = separation(&at(&BALL, vec3(0.0, 0.6, 0.0)), &at(&BALL, Vec3::ZERO));
assert!((s.gap - (-0.4)).abs() < 1.0e-4, "{s:?}");
assert!(!s.is_entangled(), "the cores are two distinct points");
assert!((s.direction - Vec3::Y).length() < 1.0e-4, "{s:?}");
}
#[test]
fn overlapping_boxes_report_themselves_entangled() {
let s = separation(&at(&CUBE, vec3(0.0, 0.2, 0.0)), &at(&CUBE, Vec3::ZERO));
assert!(s.is_entangled(), "{s:?}");
assert!(s.gap <= 0.0, "{s:?}");
}
#[test]
fn a_box_buried_in_a_far_larger_one_is_entangled_whatever_the_disparity() {
let slab = ColliderShape::Cuboid {
half_extents: [3.0, 3.0, 0.5],
};
for offset in [0.0, 0.1, -0.2, 0.35] {
let s = separation(&at(&CUBE, vec3(0.0, 0.0, offset)), &at(&slab, Vec3::ZERO));
assert!(s.is_entangled(), "offset {offset}: {s:?}");
}
let clear = separation(&at(&CUBE, vec3(0.0, 0.0, -2.0)), &at(&slab, Vec3::ZERO));
assert!(!clear.is_entangled(), "{clear:?}");
assert!((clear.gap - 1.0).abs() < 1.0e-4, "{clear:?}");
}
#[test]
fn a_rounded_shape_overlapping_a_box_still_reports_which_way_out() {
let slab = ColliderShape::Cuboid {
half_extents: [3.0, 3.0, 0.5],
};
let s = separation(&at(&BALL, vec3(0.0, 0.0, -0.8)), &at(&slab, Vec3::ZERO));
assert!(!s.is_entangled(), "{s:?}");
assert!((s.gap - (-0.2)).abs() < 1.0e-4, "{s:?}");
assert!((s.direction + Vec3::Z).length() < 1.0e-4, "{s:?}");
}
#[test]
fn a_box_and_a_ball_agree_with_the_distance_worked_out_by_hand() {
let s = separation(&at(&BALL, vec3(0.0, 3.0, 0.0)), &at(&CUBE, Vec3::ZERO));
assert!((s.gap - 2.0).abs() < 1.0e-4, "{s:?}");
assert!((s.on_b - vec3(0.0, 0.5, 0.0)).length() < 1.0e-4, "{s:?}");
let corner = separation(&at(&BALL, vec3(3.0, 3.0, 3.0)), &at(&CUBE, Vec3::ZERO));
let expected = (vec3(3.0, 3.0, 3.0) - vec3(0.5, 0.5, 0.5)).length() - 0.5;
assert!((corner.gap - expected).abs() < 1.0e-3, "{corner:?}");
}
#[test]
fn two_boxes_face_to_face_are_apart_by_the_gap_between_their_faces() {
let s = separation(&at(&CUBE, vec3(0.0, 4.0, 0.0)), &at(&CUBE, Vec3::ZERO));
assert!((s.gap - 3.0).abs() < 1.0e-4, "{s:?}");
assert!((s.direction - Vec3::Y).length() < 1.0e-3, "{s:?}");
}
#[test]
fn a_capsule_is_measured_from_its_segment_not_its_centre() {
let s = separation(&at(&CAPSULE, vec3(0.0, 4.0, 0.0)), &at(&CUBE, Vec3::ZERO));
assert!((s.gap - (4.0 - 0.75 - 0.5)).abs() < 1.0e-4, "{s:?}");
let sideways = separation(
&turned(&CAPSULE, vec3(0.0, 4.0, 0.0), [0.0, 0.0, 90.0]),
&at(&CUBE, Vec3::ZERO),
);
assert!(
(sideways.gap - (4.0 - 0.25 - 0.5)).abs() < 1.0e-4,
"{sideways:?}"
);
}
#[test]
fn two_crossed_capsules_are_apart_by_the_gap_between_their_segments() {
let a = turned(&CAPSULE, vec3(0.0, 2.0, 0.0), [0.0, 0.0, 90.0]);
let b = turned(&CAPSULE, Vec3::ZERO, [90.0, 0.0, 0.0]);
let s = separation(&a, &b);
assert!((s.gap - (2.0 - 0.5)).abs() < 1.0e-3, "{s:?}");
}
#[test]
fn swapping_the_pair_reverses_the_direction_and_keeps_the_gap() {
for pair in [(BALL, CUBE), (CAPSULE, CUBE), (CAPSULE, BALL), (CUBE, CUBE)] {
let a = turned(&pair.0, vec3(1.5, 2.5, 0.5), [10.0, 20.0, 30.0]);
let b = turned(&pair.1, Vec3::ZERO, [0.0, 45.0, 0.0]);
let forward = separation(&a, &b);
let backward = separation(&b, &a);
assert!(
(forward.gap - backward.gap).abs() < 1.0e-3,
"{pair:?}: {forward:?} {backward:?}"
);
assert!(
(forward.direction + backward.direction).length() < 1.0e-3,
"{pair:?}: {forward:?} {backward:?}"
);
}
}
#[test]
fn the_witness_point_lands_on_the_far_shapes_surface() {
for shape in [BALL, CUBE, CAPSULE] {
let a = at(&BALL, vec3(0.0, 5.0, 0.0));
let b = turned(&shape, Vec3::ZERO, [0.0, 30.0, 0.0]);
let s = separation(&a, &b);
assert!(b.contains(s.on_b + s.direction * -1.0e-3), "{shape:?}");
assert!(!b.contains(s.on_b + s.direction * 1.0e-2), "{shape:?}");
}
}
#[test]
fn containment_answers_for_every_shape() {
assert!(at(&BALL, Vec3::ZERO).contains(vec3(0.0, 0.4, 0.0)));
assert!(!at(&BALL, Vec3::ZERO).contains(vec3(0.0, 0.6, 0.0)));
assert!(at(&CUBE, Vec3::ZERO).contains(vec3(0.4, -0.4, 0.4)));
assert!(!at(&CUBE, Vec3::ZERO).contains(vec3(0.6, 0.0, 0.0)));
assert!(at(&CAPSULE, Vec3::ZERO).contains(vec3(0.0, 0.7, 0.0)));
assert!(!at(&CAPSULE, Vec3::ZERO).contains(vec3(0.0, 0.8, 0.0)));
assert!(!at(&CAPSULE, Vec3::ZERO).contains(vec3(0.3, 0.0, 0.0)));
}
#[test]
fn the_same_query_returns_the_same_bits() {
let a = turned(&CAPSULE, vec3(0.3, 2.7, -1.1), [15.0, 40.0, 5.0]);
let b = turned(&CUBE, Vec3::ZERO, [0.0, 22.5, 0.0]);
let first = separation(&a, &b);
let second = separation(&a, &b);
assert_eq!(first.gap.to_bits(), second.gap.to_bits());
assert_eq!(first.direction.to_array(), second.direction.to_array());
}
}