use crate::collision::ContactPoint;
use crate::components::{BoxShape, CapsuleShape, ColliderShape, SphereShape};
use gizmo_math::Vec3;
const EPA_TOLERANCE: f32 = 0.001;
const EPA_MAX_ITERATIONS: usize = 32;
#[derive(Clone, Copy)]
pub(crate) struct SupportPoint {
v: Vec3,
a: Vec3,
b: Vec3,
}
pub struct Gjk;
impl Gjk {
pub fn test_collision(
shape_a: &ColliderShape,
pos_a: Vec3,
rot_a: gizmo_math::Quat,
shape_b: &ColliderShape,
pos_b: Vec3,
rot_b: gizmo_math::Quat,
) -> bool {
let support = |dir: Vec3| {
let sa = Self::support_point(shape_a, pos_a, rot_a, dir);
let sb = Self::support_point(shape_b, pos_b, rot_b, -dir);
SupportPoint { v: sa - sb, a: sa, b: sb }
};
Self::gjk_with_simplex(support).is_some()
}
pub fn get_contact(
shape_a: &ColliderShape,
pos_a: Vec3,
rot_a: gizmo_math::Quat,
shape_b: &ColliderShape,
pos_b: Vec3,
rot_b: gizmo_math::Quat,
) -> Option<ContactPoint> {
let support = |dir: Vec3| {
let sa = Self::support_point(shape_a, pos_a, rot_a, dir);
let sb = Self::support_point(shape_b, pos_b, rot_b, -dir);
SupportPoint { v: sa - sb, a: sa, b: sb }
};
if let Some(simplex) = Self::gjk_with_simplex(support) {
if let Some(contact) = Self::epa(simplex, shape_a, pos_a, rot_a, shape_b, pos_b, rot_b)
{
Some(contact)
} else {
Some(ContactPoint {
point: (pos_a + pos_b) * 0.5,
normal: (pos_b - pos_a).try_normalize().unwrap_or(Vec3::Y),
penetration: 0.01,
..Default::default()
})
}
} else {
None
}
}
}
mod epa;
mod simplex;
mod support;
#[cfg(test)]
mod tests {
use super::*;
use gizmo_math::{Quat, Vec3};
#[test]
fn test_sphere_vs_sphere_collision() {
let shape = ColliderShape::Sphere(SphereShape { radius: 1.0 });
assert!(Gjk::test_collision(
&shape,
Vec3::ZERO,
Quat::IDENTITY,
&shape,
Vec3::new(1.5, 0.0, 0.0),
Quat::IDENTITY
));
assert!(!Gjk::test_collision(
&shape,
Vec3::ZERO,
Quat::IDENTITY,
&shape,
Vec3::new(2.5, 0.0, 0.0),
Quat::IDENTITY
));
}
#[test]
fn test_box_vs_box_collision() {
let shape = ColliderShape::Box(BoxShape {
half_extents: Vec3::new(1.0, 1.0, 1.0),
});
assert!(Gjk::test_collision(
&shape,
Vec3::ZERO,
Quat::IDENTITY,
&shape,
Vec3::new(1.5, 0.0, 0.0),
Quat::IDENTITY
));
assert!(!Gjk::test_collision(
&shape,
Vec3::ZERO,
Quat::IDENTITY,
&shape,
Vec3::new(2.5, 0.0, 0.0),
Quat::IDENTITY
));
}
#[test]
fn test_epa_contact_generation() {
let shape_a = ColliderShape::Box(BoxShape {
half_extents: Vec3::new(1.0, 1.0, 1.0),
});
let shape_b = ColliderShape::Box(BoxShape {
half_extents: Vec3::new(1.0, 1.0, 1.0),
});
let contact = Gjk::get_contact(
&shape_a,
Vec3::ZERO,
Quat::IDENTITY,
&shape_b,
Vec3::new(1.5, 0.0, 0.0),
Quat::IDENTITY,
);
assert!(contact.is_some(), "EPA failed to generate contact");
let contact = contact.unwrap();
assert!(
(contact.penetration - 0.5).abs() < 0.001,
"Penetration depth is wrong: {}",
contact.penetration
);
assert!(
(contact.normal.x.abs() - 1.0).abs() < 0.001,
"Normal is wrong: {:?}",
contact.normal
);
}
#[test]
fn test_speculative_contact_approaching() {
let shape = ColliderShape::Sphere(SphereShape { radius: 0.5 });
let contact = Gjk::speculative_contact(
&shape,
Vec3::new(-5.0, 0.0, 0.0),
Quat::IDENTITY,
Vec3::new(10.0, 0.0, 0.0),
1.0,
&shape,
Vec3::new(5.0, 0.0, 0.0),
Quat::IDENTITY,
Vec3::new(-10.0, 0.0, 0.0),
1.0,
1.0,
);
assert!(
contact.is_some(),
"Speculative contact missed approaching spheres"
);
}
#[test]
fn test_conservative_advancement_sphere_sphere_toi() {
let shape = ColliderShape::Sphere(SphereShape { radius: 0.5 });
let hit = Gjk::conservative_advancement(
&shape,
Vec3::new(-5.0, 0.0, 0.0),
Quat::IDENTITY,
Vec3::new(10.0, 0.0, 0.0),
&shape,
Vec3::new(5.0, 0.0, 0.0),
Quat::IDENTITY,
Vec3::ZERO,
2.0,
);
let (toi, normal) = hit.expect("CA must find the sphere-sphere impact within max_t");
assert!((toi - 0.9).abs() < 0.02, "TOI wrong: {toi} (expected ≈ 0.9)");
assert!(normal.x.abs() > 0.99, "impact normal must be ±x, got {normal:?}");
let miss = Gjk::conservative_advancement(
&shape,
Vec3::new(-5.0, 0.0, 0.0),
Quat::IDENTITY,
Vec3::new(-10.0, 0.0, 0.0),
&shape,
Vec3::new(5.0, 0.0, 0.0),
Quat::IDENTITY,
Vec3::ZERO,
2.0,
);
assert!(miss.is_none(), "separating spheres must not report an impact");
}
#[test]
fn test_compute_face_normal_follows_winding_not_origin() {
let mk = |x: f32, y: f32, z: f32| SupportPoint {
v: Vec3::new(x, y, z),
a: Vec3::ZERO,
b: Vec3::ZERO,
};
let simplex = [mk(0.0, 0.0, -0.01), mk(1.0, 0.0, -0.01), mk(0.0, 1.0, -0.01)];
let n = Gjk::compute_face_normal(&simplex, 0, 1, 2);
assert!(
n.z > 0.9,
"face normal must follow winding a→b→c (expected ≈ +Z), got {:?}",
n
);
let n_rev = Gjk::compute_face_normal(&simplex, 0, 2, 1);
assert!(
n_rev.z < -0.9,
"reversed winding must flip the normal, got {:?}",
n_rev
);
}
#[test]
fn test_epa_shallow_contact_normal_outward() {
let shape = ColliderShape::Box(BoxShape {
half_extents: Vec3::new(1.0, 1.0, 1.0),
});
let contact = Gjk::get_contact(
&shape,
Vec3::ZERO,
Quat::IDENTITY,
&shape,
Vec3::new(1.98, 0.0, 0.0),
Quat::IDENTITY,
)
.expect("EPA must produce a contact for a shallow overlap");
assert!(
contact.penetration > 0.0 && contact.penetration < 0.1,
"shallow penetration should be small and positive, got {}",
contact.penetration
);
assert!(contact.normal.is_finite(), "normal must be finite");
assert!(
(contact.normal.length() - 1.0).abs() < 1e-3,
"normal must be unit length, got {}",
contact.normal.length()
);
assert!(
contact.normal.x.abs() > 0.99,
"separating axis should be X, got normal {:?}",
contact.normal
);
}
#[test]
fn test_speculative_contact_separating() {
let shape = ColliderShape::Sphere(SphereShape { radius: 0.5 });
let contact = Gjk::speculative_contact(
&shape,
Vec3::new(-5.0, 0.0, 0.0),
Quat::IDENTITY,
Vec3::new(-10.0, 0.0, 0.0),
1.0,
&shape,
Vec3::new(5.0, 0.0, 0.0),
Quat::IDENTITY,
Vec3::new(10.0, 0.0, 0.0),
1.0,
1.0,
);
assert!(
contact.is_none(),
"Speculative contact incorrectly fired for separating shapes"
);
}
}