cgmath_culling/
lib.rs

1extern crate cgmath;
2
3use std::mem;
4
5use cgmath::{BaseFloat, Matrix4, Ortho, Perspective, PerspectiveFov, Vector3, prelude::*};
6
7#[derive(Debug, Copy, Clone, PartialEq)]
8pub struct FrustumCuller<S> {
9    nx_x: S,
10    nx_y: S,
11    nx_z: S,
12    nx_w: S,
13    px_x: S,
14    px_y: S,
15    px_z: S,
16    px_w: S,
17    ny_x: S,
18    ny_y: S,
19    ny_z: S,
20    ny_w: S,
21    py_x: S,
22    py_y: S,
23    py_z: S,
24    py_w: S,
25    nz_x: S,
26    nz_y: S,
27    nz_z: S,
28    nz_w: S,
29    pz_x: S,
30    pz_y: S,
31    pz_z: S,
32    pz_w: S,
33}
34
35#[repr(C)]
36#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
37pub struct BoundingBox<S> {
38    /// min point
39    pub min: Vector3<S>,
40    /// max point
41    pub max: Vector3<S>,
42}
43
44#[repr(C)]
45#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
46pub struct Sphere<S> {
47    /// min point
48    pub center: Vector3<S>,
49    /// max point
50    pub radius: S,
51}
52
53impl<S: BaseFloat> Sphere<S> {
54    #[inline]
55    pub fn from_params(center: Vector3<S>, radius: S) -> Self {
56        Self { center, radius }
57    }
58
59    #[inline]
60    pub fn new() -> Self {
61        Self {
62            center: Vector3::zero(),
63            radius: S::zero(),
64        }
65    }
66}
67
68#[derive(Debug, PartialEq, Eq, Copy, Clone)]
69pub enum Intersection {
70    /// fully inside the frustum
71    Inside,
72    /// Partially inside the frustum
73    Partial,
74    /// Fully outside the frustum
75    Outside,
76}
77
78impl<S: BaseFloat> BoundingBox<S> {
79    #[inline]
80    pub fn from_params(min: Vector3<S>, max: Vector3<S>) -> Self {
81        Self { min, max }
82    }
83
84    #[inline]
85    pub fn new() -> Self {
86        Self::from_params(Vector3::zero(), Vector3::zero())
87    }
88}
89
90impl<S: BaseFloat> FrustumCuller<S> {
91    /// Creates an identity frustum culler. This is equivalent to calling the `from_matrix` method
92    /// passing an identity matrix.
93    pub fn new() -> Self {
94        Self::from_matrix(Matrix4::identity())
95    }
96
97    /// Creates a frustum culler from a given perspective frustum configuration.
98    #[inline]
99    pub fn from_perspective(perspective: Perspective<S>) -> Self {
100        Self::from_matrix(perspective.into())
101    }
102
103    /// Creates a frustum culler from a given `PerspectiveFov` configuration.
104    #[inline]
105    pub fn from_perspective_fov(perspective: PerspectiveFov<S>) -> Self {
106        Self::from_matrix(perspective.into())
107    }
108
109    #[inline]
110    pub fn from_ortho(ortho: Ortho<S>) -> Self {
111        Self::from_matrix(ortho.into())
112    }
113
114    /// Creates a `FrustumCuller` from an arbitrary matrix, from which the frustum planes are
115    /// computed.
116    pub fn from_matrix(m: Matrix4<S>) -> Self {
117        let mut culler: Self = unsafe { mem::zeroed() };
118
119        culler.nx_x = m.x.w + m.x.x;
120        culler.nx_y = m.y.w + m.y.x;
121        culler.nx_z = m.z.w + m.z.x;
122        culler.nx_w = m.w.w + m.w.x;
123        //if (allow_test_spheres) {
124        let invl = (culler.nx_x * culler.nx_x + culler.nx_y * culler.nx_y
125            + culler.nx_z * culler.nx_z)
126            .sqrt()
127            .recip();
128        culler.nx_x *= invl;
129        culler.nx_y *= invl;
130        culler.nx_z *= invl;
131        culler.nx_w *= invl;
132        //}
133        culler.px_x = m.x.w - m.x.x;
134        culler.px_y = m.y.w - m.y.x;
135        culler.px_z = m.z.w - m.z.x;
136        culler.px_w = m.w.w - m.w.x;
137        //if (allow_test_spheres) {
138        let invl = (culler.px_x * culler.px_x + culler.px_y * culler.px_y
139            + culler.px_z * culler.px_z)
140            .sqrt()
141            .recip();
142        culler.px_x *= invl;
143        culler.px_y *= invl;
144        culler.px_z *= invl;
145        culler.px_w *= invl;
146        //}
147        culler.ny_x = m.x.w + m.x.y;
148        culler.ny_y = m.y.w + m.y.y;
149        culler.ny_z = m.z.w + m.z.y;
150        culler.ny_w = m.w.w + m.w.y;
151        //if (allow_test_spheres) {
152        let invl = (culler.ny_x * culler.ny_x + culler.ny_y * culler.ny_y
153            + culler.ny_z * culler.ny_z)
154            .sqrt()
155            .recip();
156        culler.ny_x *= invl;
157        culler.ny_y *= invl;
158        culler.ny_z *= invl;
159        culler.ny_w *= invl;
160        //}
161        culler.py_x = m.x.w - m.x.y;
162        culler.py_y = m.y.w - m.y.y;
163        culler.py_z = m.z.w - m.z.y;
164        culler.py_w = m.w.w - m.w.y;
165        //if (allow_test_spheres) {
166        let invl = (culler.py_x * culler.py_x + culler.py_y * culler.py_y
167            + culler.py_z * culler.py_z)
168            .sqrt()
169            .recip();
170        culler.py_x *= invl;
171        culler.py_y *= invl;
172        culler.py_z *= invl;
173        culler.py_w *= invl;
174        //}
175        culler.nz_x = m.x.w + m.x.z;
176        culler.nz_y = m.y.w + m.y.z;
177        culler.nz_z = m.z.w + m.z.z;
178        culler.nz_w = m.w.w + m.w.z;
179        //if (allow_test_spheres) {
180        let invl = (culler.nz_x * culler.nz_x + culler.nz_y * culler.nz_y
181            + culler.nz_z * culler.nz_z)
182            .sqrt()
183            .recip();
184        culler.nz_x *= invl;
185        culler.nz_y *= invl;
186        culler.nz_z *= invl;
187        culler.nz_w *= invl;
188        //}
189        culler.pz_x = m.x.w - m.x.z;
190        culler.pz_y = m.y.w - m.y.z;
191        culler.pz_z = m.z.w - m.z.z;
192        culler.pz_w = m.w.w - m.w.z;
193        //if (allow_test_spheres) {
194        let invl = (culler.pz_x * culler.pz_x + culler.pz_y * culler.pz_y
195            + culler.pz_z * culler.pz_z)
196            .sqrt()
197            .recip();
198        culler.pz_x *= invl;
199        culler.pz_y *= invl;
200        culler.pz_z *= invl;
201        culler.pz_w *= invl;
202        //}
203
204        culler
205    }
206
207    /// Test wether a 3D point lies inside of the frustum
208    pub fn test_point(&self, point: Vector3<S>) -> Intersection {
209        if self.nx_x * point.x + self.nx_y * point.y + self.nx_z * point.z + self.nx_w >= S::zero()
210            && self.px_x * point.x + self.px_y * point.y + self.px_z * point.z + self.px_w
211                >= S::zero()
212            && self.ny_x * point.x + self.ny_y * point.y + self.ny_z * point.z + self.ny_w
213                >= S::zero()
214            && self.py_x * point.x + self.py_y * point.y + self.py_z * point.z + self.py_w
215                >= S::zero()
216            && self.nz_x * point.x + self.nz_y * point.y + self.nz_z * point.z + self.nz_w
217                >= S::zero()
218            && self.pz_x * point.x + self.pz_y * point.y + self.pz_z * point.z + self.pz_w
219                >= S::zero()
220        {
221            Intersection::Inside
222        } else {
223            Intersection::Outside
224        }
225    }
226
227    /// Returns the result of testing the intersection of the frustum with a sphere, defined by a
228    /// center point (`center`) and a radius (`radius`).
229    ///
230    /// This method will distinguish between a partial intersection and a total intersection.
231    pub fn test_sphere<T>(&self, sphere: T) -> Intersection
232    where
233        T: Into<Sphere<S>>,
234    {
235        let sphere = sphere.into();
236
237        let mut inside = true;
238        let mut dist;
239        dist = self.nx_x * sphere.center.x + self.nx_y * sphere.center.y
240            + self.nx_z * sphere.center.z + self.nx_w;
241        if dist >= -sphere.radius {
242            inside &= dist >= sphere.radius;
243            dist = self.px_x * sphere.center.x + self.px_y * sphere.center.y
244                + self.px_z * sphere.center.z + self.px_w;
245            if dist >= -sphere.radius {
246                inside &= dist >= sphere.radius;
247                dist = self.ny_x * sphere.center.x + self.ny_y * sphere.center.y
248                    + self.ny_z * sphere.center.z + self.ny_w;
249                if dist >= -sphere.radius {
250                    inside &= dist >= sphere.radius;
251                    dist = self.py_x * sphere.center.x + self.py_y * sphere.center.y
252                        + self.py_z * sphere.center.z + self.py_w;
253                    if dist >= -sphere.radius {
254                        inside &= dist >= sphere.radius;
255                        dist = self.nz_x * sphere.center.x + self.nz_y * sphere.center.y
256                            + self.nz_z * sphere.center.z
257                            + self.nz_w;
258                        if dist >= -sphere.radius {
259                            inside &= dist >= sphere.radius;
260                            dist = self.pz_x * sphere.center.x + self.pz_y * sphere.center.y
261                                + self.pz_z * sphere.center.z
262                                + self.pz_w;
263                            if dist >= -sphere.radius {
264                                inside &= dist >= sphere.radius;
265                                return if inside {
266                                    Intersection::Inside
267                                } else {
268                                    Intersection::Partial
269                                };
270                            }
271                        }
272                    }
273                }
274            }
275        }
276
277        Intersection::Outside
278    }
279
280    /// Tests wether a given axis aligned bounding box intersects with the Frustum. There is a
281    /// distinction between partial intersection and full intersection, which is given by the
282    /// values of the `Intersection` enum.
283    pub fn test_bounding_box<T>(&self, aab: T) -> Intersection
284    where
285        T: Into<BoundingBox<S>>,
286    {
287        let aab = aab.into();
288        let mut inside = true;
289        if self.nx_x * if self.nx_x < S::zero() {
290            aab.min.x
291        } else {
292            aab.max.x
293        } + self.nx_y * if self.nx_y < S::zero() {
294            aab.min.y
295        } else {
296            aab.max.y
297        } + self.nx_z * if self.nx_z < S::zero() {
298            aab.min.z
299        } else {
300            aab.max.z
301        } >= -self.nx_w
302        {
303            inside &= self.nx_x * if self.nx_x < S::zero() {
304                aab.max.x
305            } else {
306                aab.min.x
307            } + self.nx_y * if self.nx_y < S::zero() {
308                aab.max.y
309            } else {
310                aab.min.y
311            } + self.nx_z * if self.nx_z < S::zero() {
312                aab.max.z
313            } else {
314                aab.min.z
315            } >= -self.nx_w;
316            if self.px_x * if self.px_x < S::zero() {
317                aab.min.x
318            } else {
319                aab.max.x
320            } + self.px_y * if self.px_y < S::zero() {
321                aab.min.y
322            } else {
323                aab.max.y
324            } + self.px_z * if self.px_z < S::zero() {
325                aab.min.z
326            } else {
327                aab.max.z
328            } >= -self.px_w
329            {
330                inside &= self.px_x * if self.px_x < S::zero() {
331                    aab.max.x
332                } else {
333                    aab.min.x
334                } + self.px_y * if self.px_y < S::zero() {
335                    aab.max.y
336                } else {
337                    aab.min.y
338                } + self.px_z * if self.px_z < S::zero() {
339                    aab.max.z
340                } else {
341                    aab.min.z
342                } >= -self.px_w;
343                if self.ny_x * if self.ny_x < S::zero() {
344                    aab.min.x
345                } else {
346                    aab.max.x
347                } + self.ny_y * if self.ny_y < S::zero() {
348                    aab.min.y
349                } else {
350                    aab.max.y
351                } + self.ny_z * if self.ny_z < S::zero() {
352                    aab.min.z
353                } else {
354                    aab.max.z
355                } >= -self.ny_w
356                {
357                    inside &= self.ny_x * if self.ny_x < S::zero() {
358                        aab.max.x
359                    } else {
360                        aab.min.x
361                    } + self.ny_y * if self.ny_y < S::zero() {
362                        aab.max.y
363                    } else {
364                        aab.min.y
365                    } + self.ny_z * if self.ny_z < S::zero() {
366                        aab.max.z
367                    } else {
368                        aab.min.z
369                    } >= -self.ny_w;
370                    if self.py_x * if self.py_x < S::zero() {
371                        aab.min.x
372                    } else {
373                        aab.max.x
374                    } + self.py_y * if self.py_y < S::zero() {
375                        aab.min.y
376                    } else {
377                        aab.max.y
378                    } + self.py_z * if self.py_z < S::zero() {
379                        aab.min.z
380                    } else {
381                        aab.max.z
382                    } >= -self.py_w
383                    {
384                        inside &= self.py_x * if self.py_x < S::zero() {
385                            aab.max.x
386                        } else {
387                            aab.min.x
388                        } + self.py_y * if self.py_y < S::zero() {
389                            aab.max.y
390                        } else {
391                            aab.min.y
392                        } + self.py_z * if self.py_z < S::zero() {
393                            aab.max.z
394                        } else {
395                            aab.min.z
396                        } >= -self.py_w;
397                        if self.nz_x * if self.nz_x < S::zero() {
398                            aab.min.x
399                        } else {
400                            aab.max.x
401                        } + self.nz_y * if self.nz_y < S::zero() {
402                            aab.min.y
403                        } else {
404                            aab.max.y
405                        } + self.nz_z * if self.nz_z < S::zero() {
406                            aab.min.z
407                        } else {
408                            aab.max.z
409                        } >= -self.nz_w
410                        {
411                            inside &= self.nz_x * if self.nz_x < S::zero() {
412                                aab.max.x
413                            } else {
414                                aab.min.x
415                            }
416                                + self.nz_y * if self.nz_y < S::zero() {
417                                    aab.max.y
418                                } else {
419                                    aab.min.y
420                                }
421                                + self.nz_z * if self.nz_z < S::zero() {
422                                    aab.max.z
423                                } else {
424                                    aab.min.z
425                                } >= -self.nz_w;
426                            if self.pz_x * if self.pz_x < S::zero() {
427                                aab.min.x
428                            } else {
429                                aab.max.x
430                            }
431                                + self.pz_y * if self.pz_y < S::zero() {
432                                    aab.min.y
433                                } else {
434                                    aab.max.y
435                                }
436                                + self.pz_z * if self.pz_z < S::zero() {
437                                    aab.min.z
438                                } else {
439                                    aab.max.z
440                                } >= -self.pz_w
441                            {
442                                inside &= self.pz_x * if self.pz_x < S::zero() {
443                                    aab.max.x
444                                } else {
445                                    aab.min.x
446                                }
447                                    + self.pz_y * if self.pz_y < S::zero() {
448                                        aab.max.y
449                                    } else {
450                                        aab.min.y
451                                    }
452                                    + self.pz_z * if self.pz_z < S::zero() {
453                                        aab.max.z
454                                    } else {
455                                        aab.min.z
456                                    } >= -self.pz_w;
457                                return if inside {
458                                    Intersection::Inside
459                                } else {
460                                    Intersection::Partial
461                                };
462                            }
463                        }
464                    }
465                }
466            }
467        }
468
469        Intersection::Outside
470    }
471}
472
473impl<S> From<(Vector3<S>, Vector3<S>)> for BoundingBox<S> {
474    #[inline]
475    fn from((min, max): (Vector3<S>, Vector3<S>)) -> Self {
476        Self { min, max }
477    }
478}
479
480impl<S> From<(Vector3<S>, S)> for Sphere<S> {
481    #[inline]
482    fn from((center, radius): (Vector3<S>, S)) -> Self {
483        Self { center, radius }
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use {BoundingBox, FrustumCuller, Intersection, Sphere};
490
491    use cgmath::{Matrix4, Ortho, PerspectiveFov, Rad, Vector3, prelude::*};
492
493    #[test]
494    fn sphere_in_frustum_ortho() {
495        let frustum_culling = FrustumCuller::from_matrix(
496            Ortho {
497                left: -1.0,
498                right: 1.0,
499                bottom: -1.0,
500                top: 1.0,
501                near: -1.0,
502                far: 1.0,
503            }.into(),
504        );
505
506        assert_eq!(
507            Intersection::Inside,
508            frustum_culling.test_sphere(Sphere::from_params(Vector3::new(0.0, 0.0, 0.0), 0.1))
509        );
510        assert_eq!(
511            Intersection::Partial,
512            frustum_culling.test_sphere(Sphere::from_params(Vector3::new(1.0, 0.0, 0.0), 0.1))
513        );
514        assert_eq!(
515            Intersection::Outside,
516            frustum_culling.test_sphere(Sphere::from_params(Vector3::new(1.2, 0.0, 0.0), 0.1))
517        );
518    }
519
520    #[test]
521    fn sphere_in_frustum_perspective() {
522        let frustum_culling = FrustumCuller::from_matrix(
523            PerspectiveFov {
524                fovy: Rad(3.14159265 / 2.0),
525                aspect: 1.0,
526                near: 0.1,
527                far: 100.0,
528            }.into(),
529        );
530
531        assert_eq!(
532            Intersection::Inside,
533            frustum_culling.test_sphere(Sphere::from_params(Vector3::new(1.0, 0.0, -2.0), 0.1))
534        );
535        assert_eq!(
536            Intersection::Outside,
537            frustum_culling.test_sphere(Sphere::from_params(Vector3::new(4.0, 0.0, -2.0), 0.1))
538        );
539    }
540
541    #[test]
542    fn test_point_in_perspective() {
543        let frustum_culling = FrustumCuller::from_matrix(
544            PerspectiveFov {
545                fovy: Rad(3.14159265 / 2.0),
546                aspect: 1.0,
547                near: 0.1,
548                far: 100.0,
549            }.into(),
550        );
551
552        assert_eq!(
553            Intersection::Inside,
554            frustum_culling.test_point(Vector3::new(0.0, 0.0, -5.0))
555        );
556        assert_eq!(
557            Intersection::Outside,
558            frustum_culling.test_point(Vector3::new(0.0, 6.0, -5.0))
559        );
560    }
561
562    #[test]
563    fn test_aab_in_ortho() {
564        let mut c = FrustumCuller::from_matrix(
565            Ortho {
566                left: -1.0,
567                right: 1.0,
568                bottom: -1.0,
569                top: 1.0,
570                near: -1.0,
571                far: 1.0,
572            }.into(),
573        );
574
575        assert_eq!(
576            Intersection::Partial,
577            c.test_bounding_box(BoundingBox::from_params(
578                Vector3::new(-20.0, -2.0, 0.0),
579                Vector3::new(20.0, 2.0, 0.0)
580            ))
581        );
582        assert_eq!(
583            Intersection::Inside,
584            c.test_bounding_box(BoundingBox::from_params(
585                Vector3::new(-0.5, -0.5, -0.5),
586                Vector3::new(0.5, 0.5, 0.5)
587            ))
588        );
589        assert_eq!(
590            Intersection::Outside,
591            c.test_bounding_box(BoundingBox::from_params(
592                Vector3::new(1.1, 0.0, 0.0),
593                Vector3::new(2.0, 2.0, 2.0)
594            ))
595        );
596
597        c = FrustumCuller::from_ortho(Ortho {
598            left: -1.0,
599            right: 1.0,
600            bottom: -1.0,
601            top: 1.0,
602            near: -1.0,
603            far: 1.0,
604        });
605
606        assert_eq!(
607            Intersection::Partial,
608            c.test_bounding_box(BoundingBox::from_params(
609                Vector3::new(0.0, 0.0, 0.0),
610                Vector3::new(2.0, 2.0, 2.0)
611            ))
612        );
613        assert_eq!(
614            Intersection::Outside,
615            c.test_bounding_box(BoundingBox::from_params(
616                Vector3::new(1.1, 0.0, 0.0),
617                Vector3::new(2.0, 2.0, 2.0)
618            ))
619        );
620
621        c = FrustumCuller::from_matrix(Matrix4::identity());
622
623        assert_eq!(
624            Intersection::Partial,
625            c.test_bounding_box(BoundingBox::from_params(
626                Vector3::new(0.5, 0.5, 0.5),
627                Vector3::new(2.0, 2.0, 2.0)
628            ))
629        );
630        assert_eq!(
631            Intersection::Outside,
632            c.test_bounding_box(BoundingBox::from_params(
633                Vector3::new(1.5, 0.5, 0.5),
634                Vector3::new(2.0, 2.0, 2.0)
635            ))
636        );
637        assert_eq!(
638            Intersection::Outside,
639            c.test_bounding_box(BoundingBox::from_params(
640                Vector3::new(-2.5, 0.5, 0.5),
641                Vector3::new(-1.5, 2.0, 2.0)
642            ))
643        );
644        assert_eq!(
645            Intersection::Outside,
646            c.test_bounding_box(BoundingBox::from_params(
647                Vector3::new(-0.5, -2.5, 0.5),
648                Vector3::new(1.5, -2.0, 2.0)
649            ))
650        );
651    }
652
653    #[test]
654    fn test_aab_in_perspective() {
655        let c = FrustumCuller::from_perspective_fov(PerspectiveFov {
656            fovy: Rad(3.14159265 / 2.0),
657            aspect: 1.0,
658            near: 0.1,
659            far: 100.0,
660        });
661
662        assert_eq!(
663            Intersection::Inside,
664            c.test_bounding_box(BoundingBox::from_params(
665                Vector3::new(0.0, 0.0, -7.0),
666                Vector3::new(1.0, 1.0, -5.0)
667            ))
668        );
669        assert_eq!(
670            Intersection::Outside,
671            c.test_bounding_box(BoundingBox::from_params(
672                Vector3::new(1.1, 0.0, 0.0),
673                Vector3::new(2.0, 2.0, 2.0)
674            ))
675        );
676        assert_eq!(
677            Intersection::Outside,
678            c.test_bounding_box(BoundingBox::from_params(
679                Vector3::new(4.0, 4.0, -3.0),
680                Vector3::new(5.0, 5.0, -5.0)
681            ))
682        );
683        assert_eq!(
684            Intersection::Outside,
685            c.test_bounding_box(BoundingBox::from_params(
686                Vector3::new(-6.0, -6.0, -2.0),
687                Vector3::new(-1.0, -4.0, -4.0)
688            ))
689        );
690    }
691}