1use 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
21pub 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
33pub 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
45pub 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
50pub 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
71pub 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#[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 pub fn width(&self) -> f32 {
115 self.max.x - self.min.x
116 }
117
118 pub fn height(&self) -> f32 {
120 self.max.y - self.min.y
121 }
122
123 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 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 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 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 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#[cfg_attr(not(target_arch = "spirv"), derive(Debug))]
216#[derive(Clone, Copy, Default, PartialEq, SlabItem)]
217pub struct Frustum {
218 pub planes: [Vec4; 6],
222 pub points: [Vec3; 8],
224 pub center: Vec3,
226}
227
228impl Frustum {
229 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 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 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 pub fn depth(&self) -> f32 {
343 (self.planes[0].w - self.planes[5].w).abs()
344 }
345}
346
347#[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 pub fn get_mesh(&self) -> [(Vec3, Vec3); 36] {
395 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 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 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 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#[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 pub fn new(center: impl Into<Vec3>, radius: f32) -> BoundingSphere {
459 BoundingSphere {
460 center: center.into(),
461 radius,
462 }
463 }
464
465 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 pub fn project_by(&self, view_projection: &Mat4) -> Self {
483 let center = self.center;
484 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 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 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
537pub trait BVol {
539 fn get_aabb(&self) -> Aabb;
541
542 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 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 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 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}