bevy_physimple/shapes/
mod.rs

1use crate::physics_components::Transform2D;
2use bevy::{math::Mat2, prelude::*};
3
4mod aabb;
5mod circle;
6mod square;
7mod capsule;
8mod triangle;
9
10pub use aabb::*;
11pub use circle::*;
12pub use square::*;
13pub use capsule::*;
14pub use triangle::*;
15
16pub trait SAT {
17    /// Gets the Axis Aligned Bounding Box of the shape
18    fn aabb(&self, trans: &Transform2D) -> Aabb {
19        let (xmin, xmax) = self.project(trans, Vec2::X);
20        let (ymin, ymax) = self.project(trans, Vec2::Y);
21
22        let min = Vec2::new(xmin, ymin);
23        let max = Vec2::new(xmax, ymax);
24
25        let extents = (max - min) * 0.5;
26        let position = min + extents;
27
28        Aabb { extents, position }
29    }
30
31    /// Gets the normals to use in the SAT algorithm(should simply be the normals of the edges)
32    ///
33    /// HINT: there is no need to give 2 parallel normals(as they produce the same results) 
34    fn get_normals(&self, trans: &Transform2D) -> Box<dyn Iterator<Item = Vec2> + '_>;
35
36    /// Gets the projection of the shape on the given normal
37    ///
38    /// (min, max)
39    fn project(&self, trans: &Transform2D, normal: Vec2) -> (f32,f32);
40
41    /// Gets the closest vertex to the given point, used for SAT vs Special shapes(Circle and Capsule)
42    fn get_closest_vertex(&self, trans: &Transform2D, vertex: Vec2) -> Vec2;
43
44    /// Gets the collision with a ray
45    ///
46    /// ray_origin: The tail of the ray
47    ///
48    /// ray_cast: The point(relative to ray_origin) the ray points to 
49    fn ray(&self, trans: &Transform2D, ray_origin: Vec2, ray_cast:  Vec2) -> Option<f32>;
50}
51
52/// Collides 2 shapes and returns the MTV relative to a
53///
54/// MTV - Minimal Tranlsation Vector
55pub fn collide(a: &CollisionShape, trans_a: &Transform2D, b: &CollisionShape, trans_b: &Transform2D) -> Option<Vec2> {
56    if let CollisionShape::Multiple(v) = a {
57        // If a is multiple shapes just break it up and attempt to combine the output
58        let mut sum = Vec2::ZERO;
59        for s in v {
60            if let Some(c) = collide(s, trans_a, b, trans_b) {
61                // I know we want to better check if we arnt already exiting the shape
62                // but it seems like way to much extra complexity for now
63                sum += c; 
64            }
65        }
66        if sum.length_squared() < 0.01 {
67            return None;
68        }
69        else {
70            return Some(sum);
71        }
72        
73    }
74    // It looks weird i know, but we need to check for both a and b, if both are multiple we need to check on all T_T
75    if let CollisionShape::Multiple(v) = b {
76        // If a is multiple shapes just break it up and attempt to combine the output
77        let mut sum = Vec2::ZERO;
78        for s in v {
79            if let Some(c) = collide(a, trans_a, s, trans_b) {
80                // I know we want to better check if we arnt already exiting the shape
81                // but it seems like way to much extra complexity for now
82                sum += c; 
83            }
84        }
85        if sum.length_squared() < 0.01 {
86            return None;
87        }
88        else {
89            return Some(sum);
90        }
91    }
92
93    let sat_a = a.sat();
94    let sat_b = b.sat();
95
96    match (sat_a, sat_b) {
97        (Some(a), Some(b)) => sat_normal(a, trans_a, b, trans_b),
98        (Some(a), None) => sat_special(a, trans_a, b, trans_b), // Special vs sat
99        (None, Some(b)) => sat_special(b, trans_b, a, trans_a).map(|c| -c), // Special vs sat - we need to flip here
100        (None, None) => collide_special(a, trans_a, b, trans_b), // Special vs Special
101    }
102}
103
104fn sat_normal(a: &dyn SAT, ta: &Transform2D, b: &dyn SAT, tb: &Transform2D) -> Option<Vec2> {
105    let na = a.get_normals(ta);
106    let nb = b.get_normals(tb);
107
108    let mut minimal_dis = f32::INFINITY;
109    let mut minimal_n = Vec2::ZERO;
110
111    for n in na.chain(nb) {
112        let (mina, maxa) = a.project(ta, n);
113        let (minb, maxb) = b.project(tb, n);
114
115        if mina < maxb && minb < maxa {
116            // collision on this axis - lets get the mtv
117            let p1 = maxb - mina;
118            let p2 = minb - maxa;
119
120            let p = if p1.abs() < p2.abs() { p1 } else { p2 };
121
122            if p.abs() < minimal_dis.abs() {
123                minimal_dis = p;
124                minimal_n = n;
125            }
126        }
127        else {
128            // if we find a non colliding axis, we know they dont collide :D
129            return None;
130        }
131    }
132    Some(minimal_dis * minimal_n)
133}
134
135fn sat_special(a: &dyn SAT, ta: &Transform2D, b: &CollisionShape, tb: &Transform2D) -> Option<Vec2> {
136    let na = a.get_normals(ta);
137    let b_rot = Mat2::from_angle(tb.rotation());
138    let nb = match b {
139        CollisionShape::Circle(c) => {
140            let offset = b_rot * c.offset;
141            let v = a.get_closest_vertex(ta, tb.translation() + offset);
142            (tb.translation() + offset - v).normalize()
143        },
144        CollisionShape::Capsule(c) => {
145            let offset = b_rot * c.offset;
146            let v = a.get_closest_vertex(ta, tb.translation() + offset);
147            c.sat_normal(tb, v)
148        }
149        _ => panic!("Shouldn't happen, if this occur to you please report it as a bug(and how you got here)")
150    };
151
152    let mut minimal_dis = f32::INFINITY;
153    let mut minimal_n = Vec2::ZERO;
154
155    for n in na.chain([nb]) {
156        let (mina, maxa) = a.project(ta, n);
157        let (minb, maxb) = match b {
158            CollisionShape::Circle(c) => {
159                let center = tb.translation() + b_rot * c.offset;
160                let center = center.dot(n);
161
162                (center - c.radius, center + c.radius)
163            },
164            CollisionShape::Capsule(c) => c.project(tb, n),
165            _ => panic!("If you paniced here, something is REALLY wrong")
166        };
167
168        if mina < maxb && minb < maxa {
169            // collision on this axis - lets get the mtv
170            let p1 = maxb - mina;
171            let p2 = minb - maxa;
172
173            let p = if p1.abs() < p2.abs() { p1 } else { p2 };
174
175            if p.abs() < minimal_dis.abs() {
176                minimal_dis = p;
177                minimal_n = n;
178            }
179        }
180        else {
181            // if we find a non colliding axis, we know they dont collide :D
182            return None;
183        }
184    }
185    Some(minimal_dis * minimal_n)
186}
187
188fn collide_special(a: &CollisionShape, ta: &Transform2D, b: &CollisionShape, tb: &Transform2D) -> Option<Vec2> {
189    #[allow(clippy::enum_glob_use)]
190    use CollisionShape::*;
191    
192    match (a, b) {
193        (Circle(a), Circle(b)) => {
194            let ac = ta.translation() + Mat2::from_angle(ta.rotation()) * a.offset;
195            let bc = tb.translation() + Mat2::from_angle(tb.rotation()) * b.offset;
196            let d = ac - bc;
197            let d_len = d.length();
198
199            if d_len < a.radius + b.radius {
200                // collision
201                Some((a.radius + b.radius - d_len) * (d / d_len))
202            }
203            else {
204                None
205            }
206        },
207        (Circle(a), Capsule(b)) => collide_circle_capsule(a, ta, b, tb),
208        (Capsule(a), Circle(b)) => collide_circle_capsule(b, tb, a, ta).map(|v| -v),
209        (Capsule(a), Capsule(b)) => {
210            let a_rot = Mat2::from_angle(ta.rotation());
211            let b_rot = Mat2::from_angle(tb.rotation());
212
213            // When you make 2 capsules obey SAT rules :D(they are still not fully SAT tho)
214
215            let n1 = a_rot * Vec2::X;
216            let n2 = b_rot * Vec2::X;
217
218            // get the closer vertex of b(relative to a)
219            let n3 = {
220                let b1 = b_rot * Vec2::new(0.0,  b.half_height) + tb.translation() + b_rot * b.offset;
221                let b2 = b_rot * Vec2::new(0.0, -b.half_height) + tb.translation() + b_rot * b.offset;
222
223                let v = ta.translation() + a_rot * a.offset;
224
225                let d1 = b1 - v;
226                let d2 = b2 - v;
227
228                if d1.length_squared() < d2.length_squared() {
229                    d1.normalize_or_zero()
230                }
231                else {
232                    d2.normalize_or_zero()
233                }
234            };
235
236            let mut minimal_dis = f32::INFINITY;
237            let mut minimal_n = Vec2::ZERO;
238
239            for n in [n1,n2,n3] {
240                let (mina, maxa) = a.project(ta, n);
241                let (minb, maxb) = b.project(tb, n);
242
243                if mina < maxb && minb < maxa {
244                    // collision on this axis - lets get the mtv
245                    let p1 = maxb - mina;
246                    let p2 = minb - maxa;
247
248                    let p = if p1.abs() < p2.abs() { p1 } else { p2 };
249
250                    if p.abs() < minimal_dis.abs() {
251                        minimal_dis = p;
252                        minimal_n = n;
253                    }
254                }
255                else {
256                    // if we find a non colliding axis, we know they dont collide :D
257                    return None;
258                }
259            }
260            Some(minimal_dis * minimal_n)
261        },
262        _ => panic!("Something is missing, please report it on github(with the shapes used)"),
263    }
264}
265
266fn collide_circle_capsule(a: &Circle, ta: &Transform2D, b: &Capsule, tb: &Transform2D) -> Option<Vec2> {
267    let brot = Mat2::from_angle(tb.rotation());
268    
269    // get the distance of the circle's center to the capsule's center line
270    let (ba, bb) = b.center_line(tb);
271
272    let acenter = ta.translation() + Mat2::from_angle(ta.rotation()) * a.offset;
273
274    let n = brot * Vec2::X;
275    let p = brot * Vec2::Y;
276
277    let bn = n.dot(ba); // n.dot(ba) should be equal n.dot(bb) should be equal n.dot(capsule_center)
278    let bap = p.dot(ba);
279    let bbp = p.dot(bb);
280    
281    let an = n.dot(acenter);
282    let ap = p.dot(acenter);
283    
284    let bpmin = bap.min(bbp);
285    let bpmax = bap.max(bbp);
286
287    let dp = if ap > bpmax { ap - bpmax } else if ap < bpmin { ap - bpmin } else { 0.0 };
288
289    let dis = n * (an - bn) + p * dp;
290
291    let dis_n = dis.normalize();
292    let dis_l = dis.dot(dis_n);
293
294    if dis_l < (a.radius + b.radius) {
295        Some(dis_n * (a.radius + b.radius - dis_l))
296    } else {
297        None
298    }
299}
300
301/**
302    # CollisionShape
303
304    Enum which can hold all possible collision shapes.
305
306    If you want to use a custom shape,
307    you can do so by implementing the `SAT` trait for your shape(check the `convex` example),
308    and box it.
309
310    Alternatively, you can build it from a vector of `CollisionShape`,
311    using `CollisionShape::Multiple`(see `showcase` example)
312    
313    Do note that this library is using the Seperate Axis Theorem, which doesnt work for concave shapes.
314    (unless of course borken down into multiple convex shapes using `CollisionShape::Multiple`)
315*/
316#[derive(Component)]
317pub enum CollisionShape {
318    Square(Square),
319    Triangle(Triangle),
320    Circle(Circle),
321    Capsule(Capsule),
322    Multiple(Vec<CollisionShape>),
323    Convex(Box<dyn SAT + Send + Sync>),
324}
325impl CollisionShape {
326    pub fn sat(&self) -> Option<&dyn SAT> {
327        match self {
328            CollisionShape::Square(s) => Some(s),
329            CollisionShape::Triangle(t) => Some(t),
330            CollisionShape::Circle(_) => None,
331            CollisionShape::Capsule(_) => None,
332            CollisionShape::Multiple(_) => None,
333            CollisionShape::Convex(s) => Some(s.as_ref())
334        }
335    }
336
337    pub fn aabb(&self, t: &Transform2D) -> Aabb {
338        if let Some(sat) = self.sat() {
339            sat.aabb(t)
340        }
341        else {
342            match self {
343                CollisionShape::Circle(c) => c.aabb(t),
344                CollisionShape::Capsule(c) => c.aabb(t),
345                CollisionShape::Multiple(v) => {
346                    // Make sure we have at least 1 shape :D
347                    assert!(v.len() != 0, "CollisionShape::Multiple cannot be empty!");
348
349                    let (mut min, mut max) = v[0].aabb(t).min_max();
350
351                    // Skip the first as we already checked him
352                    for s in v.iter().skip(1) {
353                        let (sn, sx) = s.aabb(t).min_max();
354                        min = min.min(sn);
355                        max = max.max(sx);
356                    }
357                    Aabb::from_min_max(min, max)
358                }
359                _ => panic!("Something is missing, please report on github(with the shape used)"),
360            }
361        }
362    }
363
364    pub fn ray(&self, trans: &Transform2D, ray_origin: Vec2, ray_cast: Vec2) -> Option<f32> {
365        if let Some(sat) = self.sat() {
366            sat.ray(trans, ray_origin, ray_cast)
367        }
368        else {
369            match self {
370                CollisionShape::Circle(c) => c.ray(trans, ray_origin, ray_cast),
371                CollisionShape::Capsule(c) => c.ray(trans, ray_origin, ray_cast),
372                CollisionShape::Multiple(v) => {
373                    // Make sure we have at least 1 shape :D
374                    assert!(v.len() != 0, "CollisionShape::Multiple cannot be empty!");
375                    
376                    let mut res = None;
377                    for s in v {
378                        if let Some(r) = s.ray(trans, ray_origin, ray_cast) {
379                            if r < res.unwrap_or(f32::INFINITY) {
380                                res = Some(r);
381                            }
382                        }
383                    }
384                    res
385                }
386                _ => panic!("Something is missing, please report on github(with the shape used)"),
387            }
388        }
389    }
390}
391impl Default for CollisionShape {
392    fn default() -> Self {
393        CollisionShape::Square(Square::default())
394    }
395}
396
397#[cfg(test)]
398mod sat_tests {
399    use super::*;
400
401    use std::f32::consts::PI;
402    // Use a much higher value of epsilon due to the trigo functions in the rotation calculations having
403    //  around 0.0000005 miss
404    const EPSILON: f32 = 0.001;
405
406    #[test]
407    fn squares() {
408        let s1 = Square {
409            offset: Vec2::ZERO,
410            extents: Vec2::splat(1.0),
411        };
412
413        let t1 = Transform2D::new(
414            Vec2::ZERO,
415            0.0,
416            Vec2::splat(1.0),
417        );
418
419        let s2 = Square {
420            offset: Vec2::ZERO,
421            extents: Vec2::splat(1.0),
422        };
423
424        let t2 = Transform2D::new(
425            Vec2::new(1.5, 0.0),
426            0.0,
427            Vec2::splat(1.0),
428        );
429
430        let cs1 = CollisionShape::Square(s1);
431        let cs2 = CollisionShape::Square(s2);
432
433        assert_eq!(
434            collide(&cs1, &t1, &cs2, &t2),
435            Some(Vec2::new(-0.5, 0.0))
436        );
437        assert_eq!(
438            collide(&cs2, &t2, &cs1, &t1),
439            Some(Vec2::new(0.5,0.0))
440        );
441    }
442    #[test]
443    fn squares_rotation() {
444        let a = Square {
445            offset: Vec2::ZERO,
446            extents: Vec2::splat(1.0),
447        };
448        let ta = Transform2D::new(
449            Vec2::ZERO,
450            0.0,
451            Vec2::splat(1.0)
452        );
453
454        let b = Square {
455            offset: Vec2::ZERO,
456            extents: Vec2::splat(1.0),
457        };
458        let tb = Transform2D::new(
459            Vec2::new(2.0, 0.5),
460            PI * 0.25,
461            Vec2::splat(1.0),
462        );
463
464        let c = collide(&CollisionShape::Square(a), &ta, &CollisionShape::Square(b), &tb);
465
466        assert!((c.unwrap() + Vec2::new(2.0_f32.sqrt() - 1.0, 0.0)).length() < EPSILON);
467    }
468}