renderling/
bvol.rs

1//! Bounding volumes and culling primitives.
2//!
3//! The initial implementation here was gleaned from `treeculler`, which
4//! unfortunately cannot compile to SPIR-V because of its use of `u8`.
5//!
6//! Also, here we use `glam`, whereas `treeculler` uses its own internal
7//! primitives.
8//!
9//! More resources:
10//! * <https://fgiesen.wordpress.com/2010/10/17/view-frustum-culling/>
11//! * <http://old.cescg.org/CESCG-2002/DSykoraJJelinek/>
12//! * <https://iquilezles.org/www/articles/frustumcorrect/frustumcorrect.htm>
13
14use crabslab::SlabItem;
15use glam::{Mat4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
16#[cfg(gpu)]
17use spirv_std::num_traits::Float;
18
19use crate::{camera::shader::CameraDescriptor, transform::shader::TransformDescriptor};
20
21/// Normalize a plane.
22pub fn normalize_plane(mut plane: Vec4) -> Vec4 {
23    let normal_magnitude = (plane.x.powi(2) + plane.y.powi(2) + plane.z.powi(2))
24        .sqrt()
25        .max(f32::EPSILON);
26    plane.x /= normal_magnitude;
27    plane.y /= normal_magnitude;
28    plane.z /= normal_magnitude;
29    plane.w /= normal_magnitude;
30    plane
31}
32
33/// Find the intersection point of three planes.
34///
35/// # Notes
36/// This assumes that the planes will not intersect in a line.
37pub fn intersect_planes(p0: &Vec4, p1: &Vec4, p2: &Vec4) -> Vec3 {
38    let bxc = p1.xyz().cross(p2.xyz());
39    let cxa = p2.xyz().cross(p0.xyz());
40    let axb = p0.xyz().cross(p1.xyz());
41    let r = -bxc * p0.w - cxa * p1.w - axb * p2.w;
42    r * (1.0 / bxc.dot(p0.xyz()))
43}
44
45/// Calculates distance between plane and point
46pub fn dist_bpp(plane: &Vec4, point: Vec3) -> f32 {
47    plane.x * point.x + plane.y * point.y + plane.z * point.z + plane.w
48}
49
50/// Calculates the most inside vertex of an AABB.
51pub fn mi_vertex(plane: &Vec4, aabb: &Aabb) -> Vec3 {
52    Vec3::new(
53        if plane.x >= 0.0 {
54            aabb.max.x
55        } else {
56            aabb.min.x
57        },
58        if plane.y >= 0.0 {
59            aabb.max.y
60        } else {
61            aabb.min.y
62        },
63        if plane.z >= 0.0 {
64            aabb.max.z
65        } else {
66            aabb.min.z
67        },
68    )
69}
70
71/// Calculates the most outside vertex of an AABB.
72pub fn mo_vertex(plane: &Vec4, aabb: &Aabb) -> Vec3 {
73    Vec3::new(
74        if plane.x >= 0.0 {
75            aabb.min.x
76        } else {
77            aabb.max.x
78        },
79        if plane.y >= 0.0 {
80            aabb.min.y
81        } else {
82            aabb.max.y
83        },
84        if plane.z >= 0.0 {
85            aabb.min.z
86        } else {
87            aabb.max.z
88        },
89    )
90}
91
92/// Axis aligned bounding box.
93#[derive(Clone, Copy, Debug, Default, PartialEq, SlabItem)]
94pub struct Aabb {
95    pub min: Vec3,
96    pub max: Vec3,
97}
98
99impl From<(Vec3, Vec3)> for Aabb {
100    fn from((a, b): (Vec3, Vec3)) -> Self {
101        Aabb::new(a, b)
102    }
103}
104
105impl Aabb {
106    pub fn new(a: Vec3, b: Vec3) -> Self {
107        Self {
108            min: a.min(b),
109            max: a.max(b),
110        }
111    }
112
113    /// Return the length along the x axis.
114    pub fn width(&self) -> f32 {
115        self.max.x - self.min.x
116    }
117
118    /// Return the length along the y axis.
119    pub fn height(&self) -> f32 {
120        self.max.y - self.min.y
121    }
122
123    /// Return the length along the z axis.
124    pub fn depth(&self) -> f32 {
125        self.max.z - self.min.z
126    }
127
128    pub fn center(&self) -> Vec3 {
129        (self.min + self.max) * 0.5
130    }
131
132    pub fn extents(&self) -> Vec3 {
133        self.max - self.center()
134    }
135
136    pub fn diagonal_length(&self) -> f32 {
137        self.min.distance(self.max)
138    }
139
140    pub fn is_zero(&self) -> bool {
141        self.min == self.max
142    }
143
144    /// Returns the union of the two [`Aabb`]s.
145    pub fn union(a: Self, b: Self) -> Self {
146        Aabb {
147            min: a.min.min(a.max).min(b.min).min(b.max),
148            max: a.max.max(a.min).max(b.max).max(b.min),
149        }
150    }
151
152    /// Determines whether this `Aabb` can be seen by `camera` after being
153    /// transformed by `transform`.
154    pub fn is_outside_camera_view(
155        &self,
156        camera: &CameraDescriptor,
157        transform: TransformDescriptor,
158    ) -> bool {
159        let transform = Mat4::from(transform);
160        let min = transform.transform_point3(self.min);
161        let max = transform.transform_point3(self.max);
162        Aabb::new(min, max).is_inside_frustum(camera.frustum())
163    }
164
165    #[cfg(not(target_arch = "spirv"))]
166    /// Return a triangle mesh connecting this `Aabb`'s corners.
167    ///
168    /// ```ignore
169    ///    y           1_____2     _____
170    ///    |           /    /|    /|    |  (same box, left and front sides removed)
171    ///    |___x     0/___3/ |   /7|____|6
172    ///   /           |    | /   | /    /
173    /// z/            |____|/   4|/____/5
174    ///
175    /// 7 is min
176    /// 3 is max
177    /// ```
178    pub fn get_mesh(&self) -> Vec<(Vec3, Vec3)> {
179        let p0 = Vec3::new(self.min.x, self.max.y, self.max.z);
180        let p1 = Vec3::new(self.min.x, self.max.y, self.min.z);
181        let p2 = Vec3::new(self.max.x, self.max.y, self.min.z);
182        let p3 = Vec3::new(self.max.x, self.max.y, self.max.z);
183        let p4 = Vec3::new(self.min.x, self.min.y, self.max.z);
184        let p7 = Vec3::new(self.min.x, self.min.y, self.min.z);
185        let p6 = Vec3::new(self.max.x, self.min.y, self.min.z);
186        let p5 = Vec3::new(self.max.x, self.min.y, self.max.z);
187
188        let positions = crate::math::convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7]);
189        positions
190            .chunks_exact(3)
191            .flat_map(|chunk| match chunk {
192                [a, b, c] => {
193                    let n = crate::math::triangle_face_normal(*a, *b, *c);
194                    [(*a, n), (*b, n), (*c, n)]
195                }
196                _ => unreachable!(),
197            })
198            .collect()
199    }
200
201    /// Returns whether this `Aabb` intersects another `Aabb`.
202    ///
203    /// Returns `false` if the two are touching, but not overlapping.
204    pub fn intersects_aabb(&self, other: &Aabb) -> bool {
205        self.min.x < other.max.x
206            && self.max.x > other.min.x
207            && self.min.y < other.max.y
208            && self.max.y > other.min.y
209            && self.min.z < other.max.z
210            && self.max.z > other.min.z
211    }
212}
213
214/// Six planes of a view frustum.
215#[cfg_attr(not(target_arch = "spirv"), derive(Debug))]
216#[derive(Clone, Copy, Default, PartialEq, SlabItem)]
217pub struct Frustum {
218    /// Planes constructing the sides of the frustum,
219    /// each expressed as a normal vector (xyz) and the distance (w)
220    /// from the origin along that vector.
221    pub planes: [Vec4; 6],
222    /// Points representing the corners of the frustum
223    pub points: [Vec3; 8],
224    /// Centroid of the corners of the frustum
225    pub center: Vec3,
226}
227
228impl Frustum {
229    /// Compute a frustum in world space from the given [`CameraDescriptor`].
230    pub fn from_camera(camera: &CameraDescriptor) -> Self {
231        let viewprojection = camera.view_projection();
232        let mvp = viewprojection.to_cols_array_2d();
233
234        let left = normalize_plane(Vec4::new(
235            mvp[0][0] + mvp[0][3],
236            mvp[1][0] + mvp[1][3],
237            mvp[2][0] + mvp[2][3],
238            mvp[3][0] + mvp[3][3],
239        ));
240        let right = normalize_plane(Vec4::new(
241            -mvp[0][0] + mvp[0][3],
242            -mvp[1][0] + mvp[1][3],
243            -mvp[2][0] + mvp[2][3],
244            -mvp[3][0] + mvp[3][3],
245        ));
246        let bottom = normalize_plane(Vec4::new(
247            mvp[0][1] + mvp[0][3],
248            mvp[1][1] + mvp[1][3],
249            mvp[2][1] + mvp[2][3],
250            mvp[3][1] + mvp[3][3],
251        ));
252        let top = normalize_plane(Vec4::new(
253            -mvp[0][1] + mvp[0][3],
254            -mvp[1][1] + mvp[1][3],
255            -mvp[2][1] + mvp[2][3],
256            -mvp[3][1] + mvp[3][3],
257        ));
258        let near = normalize_plane(Vec4::new(
259            mvp[0][2] + mvp[0][3],
260            mvp[1][2] + mvp[1][3],
261            mvp[2][2] + mvp[2][3],
262            mvp[3][2] + mvp[3][3],
263        ));
264        let far = normalize_plane(Vec4::new(
265            -mvp[0][2] + mvp[0][3],
266            -mvp[1][2] + mvp[1][3],
267            -mvp[2][2] + mvp[2][3],
268            -mvp[3][2] + mvp[3][3],
269        ));
270
271        // Account for the possibility that the projection is infinite.
272        //
273        // See <https://renderling.xyz/devlog/index.html#actual_frustum_culling>
274        // for more details.
275        let far = (-1.0 * near.xyz()).extend(far.w);
276
277        let flt = intersect_planes(&far, &left, &top);
278        let frt = intersect_planes(&far, &right, &top);
279        let flb = intersect_planes(&far, &left, &bottom);
280        let frb = intersect_planes(&far, &right, &bottom);
281        let nlt = intersect_planes(&near, &left, &top);
282        let nrt = intersect_planes(&near, &right, &top);
283        let nlb = intersect_planes(&near, &left, &bottom);
284        let nrb = intersect_planes(&near, &right, &bottom);
285
286        Self {
287            center: (nlt + nrt + nlb + nrb) / 4.0,
288            planes: [near, left, right, bottom, top, far],
289            points: [nlt, nrt, nlb, nrb, flt, frt, flb, frb],
290        }
291    }
292
293    #[cfg(not(target_arch = "spirv"))]
294    /// Return a triangle mesh connecting this `Frustum`'s corners.
295    pub fn get_mesh(&self) -> Vec<(Vec3, Vec3)> {
296        let [nlt, nrt, nlb, nrb, flt, frt, flb, frb] = self.points;
297        let p0 = nlt;
298        let p1 = flt;
299        let p2 = frt;
300        let p3 = nrt;
301        let p4 = nlb;
302        let p5 = nrb;
303        let p6 = frb;
304        let p7 = flb;
305        crate::math::convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7])
306            .chunks_exact(3)
307            .flat_map(|chunk| match chunk {
308                [a, b, c] => {
309                    let n = crate::math::triangle_face_normal(*a, *b, *c);
310                    [(*a, n), (*b, n), (*c, n)]
311                }
312                _ => unreachable!(),
313            })
314            .collect()
315    }
316
317    pub fn test_against_aabb(&self, aabb: &Aabb) -> bool {
318        for i in 0..3 {
319            let mut out = 0;
320            for j in 0..8 {
321                if self.points[j].to_array()[i] < aabb.min.to_array()[i] {
322                    out += 1;
323                }
324            }
325            if out == 8 {
326                return false;
327            }
328            out = 0;
329            for j in 0..8 {
330                if self.points[j].to_array()[i] > aabb.max.to_array()[i] {
331                    out += 1;
332                }
333            }
334            if out == 8 {
335                return false;
336            }
337        }
338        true
339    }
340
341    /// Returns the depth of the frustum.
342    pub fn depth(&self) -> f32 {
343        (self.planes[0].w - self.planes[5].w).abs()
344    }
345}
346
347/// Bounding box consisting of a center and three half extents.
348///
349/// Essentially a point at the center and a vector pointing from
350/// the center to the corner.
351///
352/// This is _not_ an axis aligned bounding box.
353#[cfg_attr(not(target_arch = "spirv"), derive(Debug))]
354#[derive(Clone, Copy, Default, PartialEq, SlabItem)]
355pub struct BoundingBox {
356    pub center: Vec3,
357    pub half_extent: Vec3,
358}
359
360impl BoundingBox {
361    pub fn from_min_max(min: Vec3, max: Vec3) -> Self {
362        let center = (min + max) / 2.0;
363        let half_extent = max - center;
364        Self {
365            center,
366            half_extent,
367        }
368    }
369
370    pub fn distance(&self, point: Vec3) -> f32 {
371        let p = point - self.center;
372        let component_edge_distance = p.abs() - self.half_extent;
373        let outside = component_edge_distance.max(Vec3::ZERO).length();
374        let inside = component_edge_distance
375            .x
376            .max(component_edge_distance.y)
377            .min(0.0);
378        inside + outside
379    }
380
381    #[cfg(cpu)]
382    /// Return a triangle mesh connecting this `Aabb`'s corners.
383    ///
384    /// ```ignore
385    ///    y           1_____2     _____
386    ///    |           /    /|    /|    |  (same box, left and front sides removed)
387    ///    |___x     0/___3/ |   /7|____|6
388    ///   /           |    | /   | /    /
389    /// z/            |____|/   4|/____/5
390    ///
391    /// 7 is min
392    /// 3 is max
393    /// ```
394    pub fn get_mesh(&self) -> [(Vec3, Vec3); 36] {
395        // Deriving the corner positions from centre and half-extent,
396
397        let p0 = Vec3::new(-self.half_extent.x, self.half_extent.y, self.half_extent.z);
398        let p1 = Vec3::new(-self.half_extent.x, self.half_extent.y, -self.half_extent.z);
399        let p2 = Vec3::new(self.half_extent.x, self.half_extent.y, -self.half_extent.z);
400        let p3 = self.half_extent;
401        let p4 = Vec3::new(-self.half_extent.x, -self.half_extent.y, self.half_extent.z);
402        let p5 = Vec3::new(self.half_extent.x, -self.half_extent.y, self.half_extent.z);
403        let p6 = Vec3::new(self.half_extent.x, -self.half_extent.y, -self.half_extent.z);
404        // min
405        let p7 = -self.half_extent;
406
407        let positions =
408            crate::math::convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7].map(|p| p + self.center));
409
410        // Attach per-triangle face normals.
411        let vertices: Vec<(Vec3, Vec3)> = positions
412            .chunks_exact(3)
413            .flat_map(|chunk| match chunk {
414                [a, b, c] => {
415                    let n = crate::math::triangle_face_normal(*a, *b, *c);
416                    [(*a, n), (*b, n), (*c, n)]
417                }
418                _ => unreachable!(),
419            })
420            .collect();
421
422        // Convert into fixed-size array (12 triangles × 3 vertices).
423        vertices
424            .try_into()
425            .unwrap_or_else(|v: Vec<(Vec3, Vec3)>| panic!("expected 36 vertices, got {}", v.len()))
426    }
427
428    pub fn contains_point(&self, point: Vec3) -> bool {
429        let delta = (point - self.center).abs();
430        let extent = self.half_extent.abs();
431        delta.x <= extent.x && delta.y <= extent.y && delta.z <= extent.z
432    }
433}
434
435/// Bounding sphere consisting of a center and radius.
436#[derive(Clone, Copy, Debug, Default, PartialEq, SlabItem)]
437pub struct BoundingSphere {
438    pub center: Vec3,
439    pub radius: f32,
440}
441
442impl From<(Vec3, Vec3)> for BoundingSphere {
443    fn from((min, max): (Vec3, Vec3)) -> Self {
444        let center = (min + max) * 0.5;
445        let radius = center.distance(max);
446        BoundingSphere { center, radius }
447    }
448}
449
450impl From<Aabb> for BoundingSphere {
451    fn from(value: Aabb) -> Self {
452        (value.min, value.max).into()
453    }
454}
455
456impl BoundingSphere {
457    /// Creates a new bounding sphere.
458    pub fn new(center: impl Into<Vec3>, radius: f32) -> BoundingSphere {
459        BoundingSphere {
460            center: center.into(),
461            radius,
462        }
463    }
464
465    /// Determine whether this sphere is inside the camera's view frustum after
466    /// being transformed by `transform`.  
467    pub fn is_inside_camera_view(
468        &self,
469        camera: &CameraDescriptor,
470        transform: TransformDescriptor,
471    ) -> (bool, BoundingSphere) {
472        let center = Mat4::from(transform).transform_point3(self.center);
473        let scale = Vec3::splat(transform.scale.max_element());
474        let radius = Mat4::from_scale(scale)
475            .transform_point3(Vec3::new(self.radius, 0.0, 0.0))
476            .distance(Vec3::ZERO);
477        let sphere = BoundingSphere::new(center, radius);
478        (sphere.is_inside_frustum(camera.frustum()), sphere)
479    }
480
481    /// Transform this `BoundingSphere` by the given view projection matrix.
482    pub fn project_by(&self, view_projection: &Mat4) -> Self {
483        let center = self.center;
484        // Pick any direction to find a point on the surface.
485        let surface_point = self.center + self.radius * Vec3::Z;
486        let new_center = view_projection.project_point3(center);
487        let new_surface_point = view_projection.project_point3(surface_point);
488        let new_radius = new_center.distance(new_surface_point);
489        Self {
490            center: new_center,
491            radius: new_radius,
492        }
493    }
494
495    /// Returns an [`Aabb`] with x and y coordinates in viewport pixels and z coordinate
496    /// in NDC depth.
497    pub fn project_onto_viewport(&self, camera: &CameraDescriptor, viewport: Vec2) -> Aabb {
498        fn ndc_to_pixel(viewport: Vec2, ndc: Vec3) -> Vec2 {
499            let screen = Vec3::new((ndc.x + 1.0) * 0.5, 1.0 - (ndc.y + 1.0) * 0.5, ndc.z);
500            (screen * viewport.extend(1.0)).xy()
501        }
502
503        let viewproj = camera.view_projection();
504        let frustum = camera.frustum();
505
506        // Find the center and radius of the bounding sphere in pixel space.
507        // By pixel space, I mean where (0, 0) is the top-left of the screen
508        // and (w, h) is is the bottom-left.
509        let center_clip = viewproj * self.center.extend(1.0);
510        let front_center_ndc =
511            viewproj.project_point3(self.center + self.radius * frustum.planes[5].xyz());
512        let back_center_ndc =
513            viewproj.project_point3(self.center + self.radius * frustum.planes[0].xyz());
514        let center_ndc = center_clip.xyz() / center_clip.w;
515        let center_pixels = ndc_to_pixel(viewport, center_ndc);
516        let radius_pixels = viewport.x * (self.radius / center_clip.w);
517        Aabb::new(
518            (center_pixels - radius_pixels).extend(front_center_ndc.z),
519            (center_pixels + radius_pixels).extend(back_center_ndc.z),
520        )
521    }
522}
523
524impl BVol for BoundingSphere {
525    fn get_aabb(&self) -> Aabb {
526        Aabb {
527            min: self.center - Vec3::splat(self.radius),
528            max: self.center + Vec3::splat(self.radius),
529        }
530    }
531
532    fn culls_this_plane(&self, plane: &Vec4) -> bool {
533        dist_bpp(plane, self.center) < -self.radius
534    }
535}
536
537/// Bounding volume trait.
538pub trait BVol {
539    /// Returns an AABB that contains the bounding volume.
540    fn get_aabb(&self) -> Aabb;
541
542    /// Checks if the given bounding volume is culled by this plane.
543    ///
544    /// Returns true if it does, false otherwise.
545    fn culls_this_plane(&self, plane: &Vec4) -> bool;
546
547    fn is_inside_frustum(&self, frustum: Frustum) -> bool {
548        let (inside, _) = self.coherent_test_is_volume_outside_frustum(&frustum, 0);
549        !inside
550    }
551
552    /// Checks if bounding volume is outside the frustum "coherently".
553    ///
554    /// In order for a bounding volume to be inside the frustum, it must not be
555    /// culled by any plane.
556    ///
557    /// Coherence is provided by the `lpindex` argument, which should be the
558    /// index of the first plane found that culls this volume, given as part
559    /// of the return value of this function.
560    ///
561    /// Returns `true` if the volume is outside the frustum, `false` otherwise.
562    ///
563    /// Returns the index of first plane found that culls this volume, to cache
564    /// and use later as a short circuit.
565    fn coherent_test_is_volume_outside_frustum(
566        &self,
567        frustum: &Frustum,
568        lpindex: u32,
569    ) -> (bool, u32) {
570        if self.culls_this_plane(&frustum.planes[lpindex as usize]) {
571            return (true, lpindex);
572        }
573
574        for i in 0..6 {
575            if (i != lpindex) && self.culls_this_plane(&frustum.planes[i as usize]) {
576                return (true, i);
577            }
578        }
579
580        if !frustum.test_against_aabb(&self.get_aabb()) {
581            return (true, lpindex);
582        }
583
584        (false, lpindex)
585    }
586}
587
588impl BVol for Aabb {
589    fn get_aabb(&self) -> Aabb {
590        *self
591    }
592
593    fn culls_this_plane(&self, plane: &Vec4) -> bool {
594        dist_bpp(plane, mi_vertex(plane, self)) < 0.0
595    }
596}
597
598#[cfg(test)]
599mod test {
600    use glam::{Mat4, Quat};
601
602    use crate::{context::Context, geometry::Vertex, test::BlockOnFuture};
603
604    use super::*;
605
606    #[test]
607    fn bvol_frustum_is_in_world_space_sanity() {
608        let (p, v) = crate::camera::default_perspective(800.0, 600.0);
609        let camera = CameraDescriptor::new(p, v);
610        let aabb_outside = Aabb {
611            min: Vec3::new(-10.0, -12.0, 20.0),
612            max: Vec3::new(10.0, 12.0, 40.0),
613        };
614        assert!(!aabb_outside.is_inside_frustum(camera.frustum()));
615
616        let aabb_inside = Aabb {
617            min: Vec3::new(-3.0, -3.0, -3.0),
618            max: Vec3::new(3.0, 3.0, 3.0),
619        };
620        assert!(aabb_inside.is_inside_frustum(camera.frustum()));
621    }
622
623    #[test]
624    fn frustum_culling_debug_corner_case() {
625        // https://github.com/schell/renderling/issues/131
626        // https://renderling.xyz/devlog/index.html#frustum_culling_last_debugging__aabb_vs_frustum_corner_case
627        let camera = {
628            let aspect = 1.0;
629            let fovy = core::f32::consts::FRAC_PI_4;
630            let znear = 4.0;
631            let zfar = 1000.0;
632            let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar);
633            let eye = Vec3::new(0.0, 0.0, 10.0);
634            let target = Vec3::ZERO;
635            let up = Vec3::Y;
636            let view = Mat4::look_at_rh(eye, target, up);
637            CameraDescriptor::new(projection, view)
638        };
639        let aabb = Aabb {
640            min: Vec3::new(-3.2869213, -3.0652206, -3.8715153),
641            max: Vec3::new(3.2869213, 3.0652206, 3.8715153),
642        };
643        let transform = TransformDescriptor {
644            translation: Vec3::new(7.5131035, -9.947085, -5.001645),
645            rotation: Quat::from_xyzw(0.4700742, 0.34307128, 0.6853008, -0.43783003),
646            scale: Vec3::new(1.0, 1.0, 1.0),
647        };
648        assert!(
649            !aabb.is_outside_camera_view(&camera, transform),
650            "aabb should be inside the frustum"
651        );
652    }
653
654    #[test]
655    fn bounding_box_from_min_max() {
656        let ctx = Context::headless(256, 256).block();
657        let stage = ctx
658            .new_stage()
659            .with_background_color(Vec4::ZERO)
660            .with_msaa_sample_count(4)
661            .with_lighting(true);
662        let _camera = stage.new_camera().with_projection_and_view(
663            // TODO: BUG - using orthographic here renderes nothing
664            // Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, 10.0, -10.0),
665            crate::camera::perspective(256.0, 256.0),
666            Mat4::look_at_rh(Vec3::new(-3.0, 3.0, 5.0) * 0.5, Vec3::ZERO, Vec3::Y),
667        );
668        let _lights = crate::test::make_two_directional_light_setup(&stage);
669
670        let white = stage.new_material();
671        let red = stage
672            .new_material()
673            .with_albedo_factor(Vec4::new(1.0, 0.0, 0.0, 1.0));
674
675        let _w = stage.new_primitive().with_material(&white).with_vertices(
676            stage.new_vertices(
677                crate::math::unit_cube()
678                    .into_iter()
679                    .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)),
680            ),
681        );
682
683        let mut corners = vec![];
684        for x in [-1.0, 1.0] {
685            for y in [-1.0, 1.0] {
686                for z in [-1.0, 1.0] {
687                    corners.push(Vec3::new(x, y, z));
688                }
689            }
690        }
691        let mut rs = vec![];
692        for corner in corners.iter() {
693            let bb = BoundingBox {
694                center: Vec3::new(0.5, 0.5, 0.5) * corner,
695                half_extent: Vec3::splat(0.25),
696            };
697            assert!(
698                bb.contains_point(bb.center),
699                "BoundingBox {bb:?} does not contain center"
700            );
701
702            rs.push(
703                stage.new_primitive().with_material(&red).with_vertices(
704                    stage.new_vertices(
705                        bb.get_mesh()
706                            .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)),
707                    ),
708                ),
709            );
710        }
711
712        let frame = ctx.get_next_frame().unwrap();
713        stage.render(&frame.view());
714        let img = frame.read_image().block().unwrap();
715        img_diff::assert_img_eq("bvol/bounding_box/get_mesh.png", img);
716    }
717
718    #[test]
719    fn aabb_intersection() {
720        let a = Aabb::new(Vec3::ZERO, Vec3::ONE);
721        let b = Aabb::new(Vec3::splat(0.9), Vec3::splat(1.9));
722        assert!(a.intersects_aabb(&b));
723        assert!(b.intersects_aabb(&a));
724    }
725
726    #[test]
727    fn aabb_union() {
728        let a = Aabb::new(Vec3::splat(4.0), Vec3::splat(5.0));
729        let b = Aabb::new(Vec3::ZERO, Vec3::ONE);
730        let c = Aabb::union(a, b);
731        assert_eq!(
732            Aabb {
733                min: Vec3::ZERO,
734                max: Vec3::splat(5.0)
735            },
736            c
737        );
738    }
739}