crayon/math/
frustum.rs

1//! View frustum for visibility determination
2
3use crate::math::prelude::{Aabb3, Plane, PlaneBound, PlaneRelation};
4
5use cgmath::num_traits::cast;
6use cgmath::prelude::*;
7use cgmath::{BaseFloat, Matrix, Matrix4, Point3, Rad};
8
9/// Projections.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum Projection<S: BaseFloat> {
12    /// Orthographic projection.
13    Ortho {
14        /// The width of orthographic window.
15        width: S,
16        /// The height of orthographic window.
17        height: S,
18        /// The near clip plane.
19        near: S,
20        /// The far clip plane.
21        far: S,
22    },
23
24    /// Perspective projection.
25    Perspective {
26        /// Field of view in vertical.
27        fovy: Rad<S>,
28        /// The aspect of width / height.
29        aspect: S,
30        /// The near clip plane.
31        near: S,
32        /// The far clip plane.
33        far: S,
34    },
35}
36
37impl<S: BaseFloat> Projection<S> {
38    pub fn ortho(width: S, height: S, near: S, far: S) -> Self {
39        Projection::Ortho {
40            width,
41            height,
42            near,
43            far,
44        }
45    }
46
47    pub fn perspective(fovy: Rad<S>, aspect: S, near: S, far: S) -> Self {
48        Projection::Perspective {
49            fovy,
50            aspect,
51            near,
52            far,
53        }
54    }
55
56    pub fn to_matrix(&self) -> Matrix4<S> {
57        Self::matrix(*self)
58    }
59
60    pub fn validate(&self) {
61        match *self {
62            Projection::Perspective {
63                fovy,
64                aspect,
65                near,
66                far,
67            } => {
68                assert!(
69                    fovy > Rad::zero(),
70                    "The vertical field of view cannot be below zero, found: {:?}",
71                    fovy
72                );
73
74                assert!(
75                    fovy < Rad::turn_div_2(),
76                    "The vertical field of view cannot be greater than a half turn, found: {:?}",
77                    fovy
78                );
79
80                assert!(
81                    aspect > S::zero(),
82                    "The aspect ratio cannot be below zero, found: {:?}",
83                    aspect
84                );
85
86                assert!(
87                    near > S::zero(),
88                    "The near plane distance cannot be below zero, found: {:?}",
89                    near
90                );
91
92                assert!(
93                    far > S::zero(),
94                    "The far plane distance cannot be below zero, found: {:?}",
95                    far
96                );
97
98                assert!(
99                    far > near,
100                    "The far plane cannot be closer than the near plane, found: far: {:?}, near: {:?}",
101                    far,
102                    near
103                );
104            }
105            Projection::Ortho {
106                width,
107                height,
108                near,
109                far,
110            } => {
111                assert!(
112                    width > S::zero(),
113                    "The width cannot be below zero, found: {:?}",
114                    width
115                );
116
117                assert!(
118                    height > S::zero(),
119                    "The height cannot be below zero, found: {:?}",
120                    height
121                );
122
123                assert!(
124                    far > near,
125                    "The far plane cannot be closer than the near plane, found: far: {:?}, near: {:?}",
126                    far,
127                    near
128                );
129            }
130        }
131    }
132
133    /// Gets the projection matrix in left hand coordinates.
134    pub fn matrix(projection: Projection<S>) -> Matrix4<S> {
135        match projection {
136            Projection::Ortho {
137                width,
138                height,
139                near,
140                far,
141            } => Self::ortho_matrix(width, height, near, far),
142            Projection::Perspective {
143                fovy,
144                aspect,
145                near,
146                far,
147            } => Self::perspective_matrix(fovy, aspect, near, far),
148        }
149    }
150
151    /// Gets the orthographic projection matrix in left hand coordinates.
152    pub fn ortho_matrix(w: S, h: S, n: S, f: S) -> Matrix4<S> {
153        let half: S = cast(0.5).unwrap();
154        let two: S = cast(2.0).unwrap();
155        let zero = S::zero();
156        let one = S::one();
157
158        let (hw, hh) = (w * half, h * half);
159
160        let (l0, r0) = (-hw, hw);
161        let (b0, t0) = (-hh, hh);
162
163        let c0 = [two / (r0 - l0), zero, zero, zero];
164        let c1 = [zero, two / (t0 - b0), zero, zero];
165        let c2 = [zero, zero, two / (f - n), zero];
166        let c3 = [
167            (r0 + l0) / (l0 - r0),
168            (t0 + b0) / (b0 - t0),
169            (f + n) / (n - f),
170            one,
171        ];
172        Matrix4::from_cols(c0.into(), c1.into(), c2.into(), c3.into())
173    }
174
175    /// Gets the perspective projection matrix in left hand coordinates.
176    pub fn perspective_matrix(fovy: Rad<S>, aspect: S, n: S, f: S) -> Matrix4<S> {
177        let half: S = cast(0.5).unwrap();
178        let two: S = cast(2.0).unwrap();
179        let zero = S::zero();
180        let one = S::one();
181
182        let fc = Rad::cot(fovy * half);
183        let c0 = [fc / aspect, zero, zero, zero];
184        let c1 = [zero, fc, zero, zero];
185        let c2 = [zero, zero, (f + n) / (f - n), one];
186        let c3 = [zero, zero, (two * f * n) / (n - f), zero];
187        Matrix4::from_cols(c0.into(), c1.into(), c2.into(), c3.into())
188    }
189}
190
191/// View frustum, used for frustum culling
192#[derive(Copy, Clone, Debug, PartialEq)]
193pub struct Frustum<S: BaseFloat> {
194    projection: Projection<S>,
195
196    /// Left plane
197    pub left: Plane<S>,
198    /// Right plane
199    pub right: Plane<S>,
200    /// Bottom plane
201    pub bottom: Plane<S>,
202    /// Top plane
203    pub top: Plane<S>,
204    /// Near plane
205    pub near: Plane<S>,
206    /// Far plane
207    pub far: Plane<S>,
208}
209
210impl<S: BaseFloat> Frustum<S> {
211    /// Construct a frustum.
212    pub fn new(projection: Projection<S>) -> Frustum<S> {
213        projection.validate();
214        let mat = Projection::matrix(projection);
215
216        Frustum {
217            projection,
218
219            left: Plane::from_vector4_alt(mat.row(3) + mat.row(0))
220                .normalize()
221                .unwrap(),
222
223            right: Plane::from_vector4_alt(mat.row(3) - mat.row(0))
224                .normalize()
225                .unwrap(),
226
227            bottom: Plane::from_vector4_alt(mat.row(3) + mat.row(1))
228                .normalize()
229                .unwrap(),
230
231            top: Plane::from_vector4_alt(mat.row(3) - mat.row(1))
232                .normalize()
233                .unwrap(),
234
235            near: Plane::from_vector4_alt(mat.row(3) + mat.row(2))
236                .normalize()
237                .unwrap(),
238
239            far: Plane::from_vector4_alt(mat.row(3) - mat.row(2))
240                .normalize()
241                .unwrap(),
242        }
243    }
244
245    pub fn projection(&self) -> Projection<S> {
246        self.projection
247    }
248
249    /// Find the spatial relation of a bound inside this frustum.
250    pub fn contains<B: PlaneBound<S>>(&self, bound: &B) -> PlaneRelation {
251        [
252            self.left,
253            self.right,
254            self.top,
255            self.bottom,
256            self.near,
257            self.far,
258        ]
259            .iter()
260            .fold(PlaneRelation::In, |cur, p| {
261                use std::cmp::max;
262                let r = bound.relate(*p);
263                // If any of the planes are `Out`, the bound is outside.
264                // Otherwise, if any are `Cross`, the bound is crossing.
265                // Otherwise, the bound is fully inside.
266                max(cur, r)
267            })
268    }
269
270    pub fn to_matrix(&self) -> Matrix4<S> {
271        self.projection.to_matrix()
272    }
273}
274
275/// View frustum corner points
276#[derive(Copy, Clone, PartialEq, Debug)]
277pub struct FrustumPoints<S: BaseFloat> {
278    /// Near top left point
279    pub near_top_left: Point3<S>,
280    /// Near top right point
281    pub near_top_right: Point3<S>,
282    /// Near bottom left point
283    pub near_bottom_left: Point3<S>,
284    /// Near bottom right point
285    pub near_bottom_right: Point3<S>,
286    /// Far top left point
287    pub far_top_left: Point3<S>,
288    /// Far top right point
289    pub far_top_right: Point3<S>,
290    /// Far bottom left point
291    pub far_bottom_left: Point3<S>,
292    /// Far bottom right point
293    pub far_bottom_right: Point3<S>,
294}
295
296impl<S: BaseFloat> FrustumPoints<S> {
297    /// Apply an arbitrary transform to the corners of this frustum, return a new
298    /// conservative frustum.
299    #[inline]
300    pub fn transform<T>(&self, transform: &T) -> Self
301    where
302        T: Transform<Point3<S>>,
303    {
304        FrustumPoints {
305            near_top_left: transform.transform_point(self.near_top_left),
306            near_top_right: transform.transform_point(self.near_top_right),
307            near_bottom_left: transform.transform_point(self.near_bottom_left),
308            near_bottom_right: transform.transform_point(self.near_bottom_right),
309            far_top_left: transform.transform_point(self.far_top_left),
310            far_top_right: transform.transform_point(self.far_top_right),
311            far_bottom_left: transform.transform_point(self.far_bottom_left),
312            far_bottom_right: transform.transform_point(self.far_bottom_right),
313        }
314    }
315
316    /// Compute corners.
317    #[inline]
318    pub fn to_corners(&self) -> [Point3<S>; 8] {
319        [
320            self.near_top_left,
321            self.near_top_right,
322            self.near_bottom_left,
323            self.near_bottom_right,
324            self.far_top_left,
325            self.far_top_right,
326            self.far_bottom_left,
327            self.far_bottom_right,
328        ]
329    }
330
331    /// Compute aabb.
332    #[inline]
333    pub fn aabb(&self) -> Aabb3<S> {
334        let aabb = Aabb3::zero();
335        self.to_corners()[..]
336            .iter()
337            .fold(aabb, |u, &corner| u.grow(corner))
338    }
339}
340
341impl<S: BaseFloat> Into<FrustumPoints<S>> for Frustum<S> {
342    fn into(self) -> FrustumPoints<S> {
343        match self.projection {
344            Projection::Ortho {
345                width,
346                height,
347                near,
348                far,
349            } => {
350                let half: S = cast(0.5).unwrap();
351                let (hw, hh) = (width * half, height * half);
352                let (l, r) = (-hw, hw);
353                let (b, t) = (-hh, hh);
354
355                FrustumPoints {
356                    near_top_left: Point3::new(near, t, l),
357                    near_top_right: Point3::new(near, t, r),
358                    near_bottom_left: Point3::new(near, b, l),
359                    near_bottom_right: Point3::new(near, b, r),
360                    far_top_left: Point3::new(far, t, l),
361                    far_top_right: Point3::new(far, t, r),
362                    far_bottom_left: Point3::new(far, b, l),
363                    far_bottom_right: Point3::new(far, b, r),
364                }
365            }
366            Projection::Perspective {
367                fovy,
368                aspect,
369                near,
370                far,
371            } => {
372                let m = Projection::perspective_matrix(fovy, aspect, near, far);
373                let im = m.invert().unwrap();
374                let one = S::one();
375
376                let points = FrustumPoints {
377                    near_top_left: Point3::new(-one, one, -one),
378                    near_top_right: Point3::new(-one, one, one),
379                    near_bottom_left: Point3::new(-one, -one, -one),
380                    near_bottom_right: Point3::new(-one, -one, one),
381                    far_top_left: Point3::new(one, one, -one),
382                    far_top_right: Point3::new(one, one, one),
383                    far_bottom_left: Point3::new(one, -one, -one),
384                    far_bottom_right: Point3::new(one, -one, one),
385                };
386
387                points.transform(&im)
388            }
389        }
390    }
391}
392
393impl<S: BaseFloat> Into<Matrix4<S>> for Frustum<S> {
394    fn into(self) -> Matrix4<S> {
395        self.projection.to_matrix()
396    }
397}