mini_collide/
collision.rs

1use crate::{ClosestPoint, LineSegment, Plane, Ray, Sphere, Triangle};
2use mini_math::{NearlyEqual, Point, Vector3};
3
4/// The result of a collision
5#[derive(PartialEq, Debug)]
6pub struct Contact {
7    /// The point at which the collision occurs
8    pub point: Point,
9    /// The surface normal at the point of collision
10    pub normal: Vector3,
11    /// The distance by which the colliding shapes overlap
12    pub overlap: f32,
13}
14
15impl NearlyEqual for &Contact {
16    fn nearly_equals(self, rhs: Self) -> bool {
17        self.point.nearly_equals(&rhs.point)
18            && self.normal.nearly_equals(&rhs.normal)
19            && self.overlap.nearly_equals(rhs.overlap)
20    }
21}
22
23impl Contact {
24    fn new(point: Point, normal: Vector3, overlap: f32) -> Self {
25        Self {
26            point,
27            normal,
28            overlap,
29        }
30    }
31}
32
33/// Trait for determining the collision between two shapes
34pub trait Collision<Rhs> {
35    /// Whether this shape collides with the other, and where
36    fn collides(&self, rhs: &Rhs) -> Option<Contact>;
37}
38
39impl Collision<Sphere> for Sphere {
40    fn collides(&self, sphere: &Sphere) -> Option<Contact> {
41        let combined_radius = self.radius + sphere.radius;
42        let diff = self.center - sphere.center;
43        let distance_squared = diff.magnitude_squared();
44        if distance_squared > combined_radius * combined_radius {
45            None
46        } else {
47            let distance = distance_squared.sqrt();
48            let normal = diff / distance;
49
50            Some(Contact::new(
51                sphere.center + normal * sphere.radius,
52                normal,
53                combined_radius - distance,
54            ))
55        }
56    }
57}
58
59impl Collision<Triangle> for Sphere {
60    fn collides(&self, triangle: &Triangle) -> Option<Contact> {
61        let plane = Plane::from(triangle);
62
63        let p = plane.closest_point(&self.center);
64        let distance_from_plane_squared = (p - self.center).magnitude_squared();
65
66        if distance_from_plane_squared > self.radius * self.radius {
67            None
68        } else {
69            let q = triangle.closest_point(&self.center);
70            let diff = q - self.center;
71            let overlap = self.radius - diff.magnitude();
72            if overlap < 0.0 {
73                None
74            } else {
75                Some(Contact::new(q, plane.normal, overlap))
76            }
77        }
78    }
79}
80
81impl Collision<Triangle> for Ray {
82    fn collides(&self, triangle: &Triangle) -> Option<Contact> {
83        let plane = Plane::from(triangle);
84
85        let n_dot_r = plane.normal.dot(self.direction);
86        // early exit if ray parallel to plane
87        if n_dot_r.abs() < std::f32::EPSILON {
88            return None;
89        }
90
91        let d = plane.normal.dot(Vector3::from(triangle.a));
92        let e = plane.normal.dot(Vector3::from(self.origin));
93        let t = (e + d) / n_dot_r;
94
95        // early exit if triangle entirely behind ray
96        if t > 0.0 {
97            return None;
98        }
99
100        let intersection_point = self.origin + self.direction * -t;
101        if triangle.coplanar_point_inside(intersection_point) {
102            Some(Contact::new(intersection_point, plane.normal, 0.0))
103        } else {
104            None
105        }
106    }
107}
108
109impl Collision<Triangle> for LineSegment {
110    fn collides(&self, triangle: &Triangle) -> Option<Contact> {
111        let plane = Plane::from(triangle);
112
113        let mut direction = self.end - self.start;
114        let length = direction.magnitude();
115        direction /= length;
116
117        let n_dot_r = plane.normal.dot(direction);
118        // early exit if line parallel to plane
119        if n_dot_r.abs() < std::f32::EPSILON {
120            return None;
121        }
122
123        let d = plane.normal.dot(Vector3::from(triangle.a));
124        let e = plane.normal.dot(Vector3::from(self.start));
125        let t = (e + d) / n_dot_r;
126
127        // early exit if triangle is entirely in fornt or behind of the line segment
128        if t > 0.0 || t < -length {
129            return None;
130        }
131
132        let intersection_point = self.start + direction * -t;
133        if triangle.coplanar_point_inside(intersection_point) {
134            Some(Contact::new(intersection_point, plane.normal, 0.0))
135        } else {
136            None
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use mini_math::{Point, Vector3};
145
146    #[test]
147    fn test_sphere_sphere_collision() {
148        let a = Sphere::new(Point::zero(), 1.0);
149        let b = Sphere::new(Point::new(0.0, 1.5, 0.0), 1.0);
150
151        assert_eq!(
152            b.collides(&a),
153            Some(Contact::new(
154                Point::new(0.0, 1.0, 0.0),
155                Vector3::new(0.0, 1.0, 0.0),
156                0.5
157            ))
158        );
159    }
160
161    #[test]
162    fn test_sphere_triangle_collision() {
163        let a = Triangle::new(
164            Point::new(-1.0, 0.0, -1.0),
165            Point::new(1.0, 0.0, -1.0),
166            Point::new(0.0, 0.0, 1.0),
167        );
168        let b = Sphere::new(Point::new(0.0, 0.75, 0.0), 1.0);
169
170        assert_eq!(
171            b.collides(&a),
172            Some(Contact::new(
173                Point::new(0.0, 0.0, 0.0),
174                Vector3::new(0.0, 1.0, 0.0),
175                0.25
176            ))
177        );
178
179        let b = Sphere::new(Point::new(0.0, 1.75, 0.0), 1.0);
180        assert_eq!(b.collides(&a), None);
181
182        let b = Sphere::new(Point::new(0.0, -1.75, 0.0), 1.0);
183        assert_eq!(b.collides(&a), None);
184
185        let b = Sphere::new(Point::new(-3.0, 0.0, -3.0), 1.0);
186        assert_eq!(b.collides(&a), None);
187    }
188
189    #[test]
190    fn test_triangle_ray_collision() {
191        let triangle = Triangle::new(
192            Point::new(-1.0, 0.0, 0.0),
193            Point::new(1.0, 0.0, 0.0),
194            Point::new(0.0, 0.0, 1.0),
195        );
196
197        // parallel
198        let ray = Ray::new(Point::new(0.0, 1.0, 0.0), Vector3::new(0.0, 0.0, 1.0));
199        assert_eq!(ray.collides(&triangle), None);
200
201        // in front
202        let ray = Ray::new(Point::new(0.0, 1.0, 0.0), Vector3::new(0.0, 1.0, 0.0));
203        assert_eq!(ray.collides(&triangle), None);
204
205        // behind
206        let ray = Ray::new(Point::new(0.0, -1.0, 0.0), Vector3::new(0.0, -1.0, 0.0));
207        assert_eq!(ray.collides(&triangle), None);
208
209        // past
210        let ray = Ray::new(Point::new(3.0, 1.0, 3.0), Vector3::new(0.0, -1.0, 0.0));
211        assert_eq!(ray.collides(&triangle), None);
212
213        // straight through
214        let ray = Ray::new(Point::new(0.0, 1.0, 0.0), Vector3::new(0.0, -1.0, 0.0));
215        assert_eq!(
216            ray.collides(&triangle),
217            Some(Contact::new(
218                Point::new(0.0, 0.0, 0.0),
219                Vector3::new(0.0, 1.0, 0.0),
220                0.0
221            ))
222        );
223
224        // diagonally through
225        let ray = Ray::new(
226            Point::new(-0.5, -1.0, 0.0),
227            Vector3::new(0.5, 1.0, 0.0).normalized(),
228        );
229        assert_eq!(
230            ray.collides(&triangle),
231            Some(Contact::new(
232                Point::new(0.0, 0.0, 0.0),
233                Vector3::new(0.0, 1.0, 0.0),
234                0.0
235            ))
236        );
237    }
238}