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#[derive(Clone, Copy, Default, PartialEq, SlabItem, core::fmt::Debug)]
216pub struct Frustum {
217 pub planes: [Vec4; 6],
221 pub points: [Vec3; 8],
223 pub center: Vec3,
225}
226
227impl Frustum {
228 pub fn from_camera(camera: &CameraDescriptor) -> Self {
230 let viewprojection = camera.view_projection();
231 let mvp = viewprojection.to_cols_array_2d();
232
233 let left = normalize_plane(Vec4::new(
234 mvp[0][0] + mvp[0][3],
235 mvp[1][0] + mvp[1][3],
236 mvp[2][0] + mvp[2][3],
237 mvp[3][0] + mvp[3][3],
238 ));
239 let right = normalize_plane(Vec4::new(
240 -mvp[0][0] + mvp[0][3],
241 -mvp[1][0] + mvp[1][3],
242 -mvp[2][0] + mvp[2][3],
243 -mvp[3][0] + mvp[3][3],
244 ));
245 let bottom = normalize_plane(Vec4::new(
246 mvp[0][1] + mvp[0][3],
247 mvp[1][1] + mvp[1][3],
248 mvp[2][1] + mvp[2][3],
249 mvp[3][1] + mvp[3][3],
250 ));
251 let top = normalize_plane(Vec4::new(
252 -mvp[0][1] + mvp[0][3],
253 -mvp[1][1] + mvp[1][3],
254 -mvp[2][1] + mvp[2][3],
255 -mvp[3][1] + mvp[3][3],
256 ));
257 let near = normalize_plane(Vec4::new(mvp[0][2], mvp[1][2], mvp[2][2], mvp[3][2]));
260 let far = normalize_plane(Vec4::new(
261 -mvp[0][2] + mvp[0][3],
262 -mvp[1][2] + mvp[1][3],
263 -mvp[2][2] + mvp[2][3],
264 -mvp[3][2] + mvp[3][3],
265 ));
266
267 let far = (-1.0 * near.xyz()).extend(far.w);
272
273 let flt = intersect_planes(&far, &left, &top);
274 let frt = intersect_planes(&far, &right, &top);
275 let flb = intersect_planes(&far, &left, &bottom);
276 let frb = intersect_planes(&far, &right, &bottom);
277 let nlt = intersect_planes(&near, &left, &top);
278 let nrt = intersect_planes(&near, &right, &top);
279 let nlb = intersect_planes(&near, &left, &bottom);
280 let nrb = intersect_planes(&near, &right, &bottom);
281
282 Self {
283 center: (nlt + nrt + nlb + nrb) / 4.0,
284 planes: [near, left, right, bottom, top, far],
285 points: [nlt, nrt, nlb, nrb, flt, frt, flb, frb],
286 }
287 }
288
289 #[cfg(not(target_arch = "spirv"))]
290 pub fn get_mesh(&self) -> Vec<(Vec3, Vec3)> {
292 let [nlt, nrt, nlb, nrb, flt, frt, flb, frb] = self.points;
293 let p0 = nlt;
294 let p1 = flt;
295 let p2 = frt;
296 let p3 = nrt;
297 let p4 = nlb;
298 let p5 = nrb;
299 let p6 = frb;
300 let p7 = flb;
301 crate::math::convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7])
302 .chunks_exact(3)
303 .flat_map(|chunk| match chunk {
304 [a, b, c] => {
305 let n = crate::math::triangle_face_normal(*a, *b, *c);
306 [(*a, n), (*b, n), (*c, n)]
307 }
308 _ => unreachable!(),
309 })
310 .collect()
311 }
312
313 pub fn test_against_aabb(&self, aabb: &Aabb) -> bool {
314 for i in 0..3 {
315 let mut out = 0;
316 for j in 0..8 {
317 if self.points[j].to_array()[i] < aabb.min.to_array()[i] {
318 out += 1;
319 }
320 }
321 if out == 8 {
322 return false;
323 }
324 out = 0;
325 for j in 0..8 {
326 if self.points[j].to_array()[i] > aabb.max.to_array()[i] {
327 out += 1;
328 }
329 }
330 if out == 8 {
331 return false;
332 }
333 }
334 true
335 }
336
337 pub fn depth(&self) -> f32 {
339 (self.planes[0].w - self.planes[5].w).abs()
340 }
341}
342
343#[derive(Clone, Copy, Default, PartialEq, SlabItem, core::fmt::Debug)]
350pub struct BoundingBox {
351 pub center: Vec3,
352 pub half_extent: Vec3,
353}
354
355impl BoundingBox {
356 pub fn from_min_max(min: Vec3, max: Vec3) -> Self {
357 let center = (min + max) / 2.0;
358 let half_extent = max - center;
359 Self {
360 center,
361 half_extent,
362 }
363 }
364
365 pub fn distance(&self, point: Vec3) -> f32 {
366 let p = point - self.center;
367 let component_edge_distance = p.abs() - self.half_extent;
368 let outside = component_edge_distance.max(Vec3::ZERO).length();
369 let inside = component_edge_distance
370 .x
371 .max(component_edge_distance.y)
372 .min(0.0);
373 inside + outside
374 }
375
376 #[cfg(cpu)]
377 pub fn get_mesh(&self) -> [(Vec3, Vec3); 36] {
390 let p0 = Vec3::new(-self.half_extent.x, self.half_extent.y, self.half_extent.z);
393 let p1 = Vec3::new(-self.half_extent.x, self.half_extent.y, -self.half_extent.z);
394 let p2 = Vec3::new(self.half_extent.x, self.half_extent.y, -self.half_extent.z);
395 let p3 = self.half_extent;
396 let p4 = Vec3::new(-self.half_extent.x, -self.half_extent.y, self.half_extent.z);
397 let p5 = Vec3::new(self.half_extent.x, -self.half_extent.y, self.half_extent.z);
398 let p6 = Vec3::new(self.half_extent.x, -self.half_extent.y, -self.half_extent.z);
399 let p7 = -self.half_extent;
401
402 let positions =
403 crate::math::convex_mesh([p0, p1, p2, p3, p4, p5, p6, p7].map(|p| p + self.center));
404
405 let vertices: Vec<(Vec3, Vec3)> = positions
407 .chunks_exact(3)
408 .flat_map(|chunk| match chunk {
409 [a, b, c] => {
410 let n = crate::math::triangle_face_normal(*a, *b, *c);
411 [(*a, n), (*b, n), (*c, n)]
412 }
413 _ => unreachable!(),
414 })
415 .collect();
416
417 vertices
419 .try_into()
420 .unwrap_or_else(|v: Vec<(Vec3, Vec3)>| panic!("expected 36 vertices, got {}", v.len()))
421 }
422
423 pub fn contains_point(&self, point: Vec3) -> bool {
424 let delta = (point - self.center).abs();
425 let extent = self.half_extent.abs();
426 delta.x <= extent.x && delta.y <= extent.y && delta.z <= extent.z
427 }
428}
429
430#[derive(Clone, Copy, Debug, Default, PartialEq, SlabItem)]
432pub struct BoundingSphere {
433 pub center: Vec3,
434 pub radius: f32,
435}
436
437impl From<(Vec3, Vec3)> for BoundingSphere {
438 fn from((min, max): (Vec3, Vec3)) -> Self {
439 let center = (min + max) * 0.5;
440 let radius = center.distance(max);
441 BoundingSphere { center, radius }
442 }
443}
444
445impl From<Aabb> for BoundingSphere {
446 fn from(value: Aabb) -> Self {
447 (value.min, value.max).into()
448 }
449}
450
451impl BoundingSphere {
452 pub fn new(center: impl Into<Vec3>, radius: f32) -> BoundingSphere {
454 BoundingSphere {
455 center: center.into(),
456 radius,
457 }
458 }
459
460 pub fn is_inside_camera_view(
463 &self,
464 camera: &CameraDescriptor,
465 transform: TransformDescriptor,
466 ) -> (bool, BoundingSphere) {
467 let center = Mat4::from(transform).transform_point3(self.center);
468 let scale = Vec3::splat(transform.scale.max_element());
469 let radius = Mat4::from_scale(scale)
470 .transform_point3(Vec3::new(self.radius, 0.0, 0.0))
471 .distance(Vec3::ZERO);
472 let sphere = BoundingSphere::new(center, radius);
473 (sphere.is_inside_frustum(camera.frustum()), sphere)
474 }
475
476 pub fn project_by(&self, view_projection: &Mat4) -> Self {
478 let center = self.center;
479 let surface_point = self.center + self.radius * Vec3::Z;
481 let new_center = view_projection.project_point3(center);
482 let new_surface_point = view_projection.project_point3(surface_point);
483 let new_radius = new_center.distance(new_surface_point);
484 Self {
485 center: new_center,
486 radius: new_radius,
487 }
488 }
489
490 pub fn project_onto_viewport(&self, camera: &CameraDescriptor, viewport: Vec2) -> Aabb {
493 fn ndc_to_pixel(viewport: Vec2, ndc: Vec3) -> Vec2 {
494 let screen = Vec3::new((ndc.x + 1.0) * 0.5, 1.0 - (ndc.y + 1.0) * 0.5, ndc.z);
495 (screen * viewport.extend(1.0)).xy()
496 }
497
498 let viewproj = camera.view_projection();
499 let frustum = camera.frustum();
500
501 let center_clip = viewproj * self.center.extend(1.0);
505 let front_center_ndc =
506 viewproj.project_point3(self.center + self.radius * frustum.planes[5].xyz());
507 let back_center_ndc =
508 viewproj.project_point3(self.center + self.radius * frustum.planes[0].xyz());
509 let center_ndc = center_clip.xyz() / center_clip.w;
510 let center_pixels = ndc_to_pixel(viewport, center_ndc);
511 let radius_pixels = viewport.x * (self.radius / center_clip.w);
512 Aabb::new(
513 (center_pixels - radius_pixels).extend(front_center_ndc.z),
514 (center_pixels + radius_pixels).extend(back_center_ndc.z),
515 )
516 }
517}
518
519impl BVol for BoundingSphere {
520 fn get_aabb(&self) -> Aabb {
521 Aabb {
522 min: self.center - Vec3::splat(self.radius),
523 max: self.center + Vec3::splat(self.radius),
524 }
525 }
526
527 fn culls_this_plane(&self, plane: &Vec4) -> bool {
528 dist_bpp(plane, self.center) < -self.radius
529 }
530}
531
532pub trait BVol {
534 fn get_aabb(&self) -> Aabb;
536
537 fn culls_this_plane(&self, plane: &Vec4) -> bool;
541
542 fn is_inside_frustum(&self, frustum: Frustum) -> bool {
543 let (inside, _) = self.coherent_test_is_volume_outside_frustum(&frustum, 0);
544 !inside
545 }
546
547 fn coherent_test_is_volume_outside_frustum(
561 &self,
562 frustum: &Frustum,
563 lpindex: u32,
564 ) -> (bool, u32) {
565 if self.culls_this_plane(&frustum.planes[lpindex as usize]) {
566 return (true, lpindex);
567 }
568
569 for i in 0..6 {
570 if (i != lpindex) && self.culls_this_plane(&frustum.planes[i as usize]) {
571 return (true, i);
572 }
573 }
574
575 if !frustum.test_against_aabb(&self.get_aabb()) {
576 return (true, lpindex);
577 }
578
579 (false, lpindex)
580 }
581}
582
583impl BVol for Aabb {
584 fn get_aabb(&self) -> Aabb {
585 *self
586 }
587
588 fn culls_this_plane(&self, plane: &Vec4) -> bool {
589 dist_bpp(plane, mi_vertex(plane, self)) < 0.0
590 }
591}
592
593#[cfg(test)]
594mod test {
595 use glam::{Mat4, Quat};
596
597 use crate::{context::Context, geometry::Vertex, test::BlockOnFuture};
598
599 use super::*;
600
601 #[test]
602 fn bvol_frustum_is_in_world_space_sanity() {
603 let (p, v) = crate::camera::default_perspective(800.0, 600.0);
604 let camera = CameraDescriptor::new(p, v);
605 let aabb_outside = Aabb {
606 min: Vec3::new(-10.0, -12.0, 20.0),
607 max: Vec3::new(10.0, 12.0, 40.0),
608 };
609 assert!(!aabb_outside.is_inside_frustum(camera.frustum()));
610
611 let aabb_inside = Aabb {
612 min: Vec3::new(-3.0, -3.0, -3.0),
613 max: Vec3::new(3.0, 3.0, 3.0),
614 };
615 assert!(aabb_inside.is_inside_frustum(camera.frustum()));
616 }
617
618 #[test]
619 fn frustum_culling_debug_corner_case() {
620 let camera = {
623 let aspect = 1.0;
624 let fovy = core::f32::consts::FRAC_PI_4;
625 let znear = 4.0;
626 let zfar = 1000.0;
627 let projection = Mat4::perspective_rh(fovy, aspect, znear, zfar);
628 let eye = Vec3::new(0.0, 0.0, 10.0);
629 let target = Vec3::ZERO;
630 let up = Vec3::Y;
631 let view = Mat4::look_at_rh(eye, target, up);
632 CameraDescriptor::new(projection, view)
633 };
634 let aabb = Aabb {
635 min: Vec3::new(-3.2869213, -3.0652206, -3.8715153),
636 max: Vec3::new(3.2869213, 3.0652206, 3.8715153),
637 };
638 let transform = TransformDescriptor {
639 translation: Vec3::new(7.5131035, -9.947085, -5.001645),
640 rotation: Quat::from_xyzw(0.4700742, 0.34307128, 0.6853008, -0.43783003),
641 scale: Vec3::new(1.0, 1.0, 1.0),
642 };
643 assert!(
644 !aabb.is_outside_camera_view(&camera, transform),
645 "aabb should be inside the frustum"
646 );
647 }
648
649 #[test]
650 fn bounding_box_from_min_max() {
651 let ctx = Context::headless(256, 256).block();
652 let stage = ctx
653 .new_stage()
654 .with_background_color(Vec4::ZERO)
655 .with_msaa_sample_count(4)
656 .with_lighting(true);
657 let _camera = stage.new_camera().with_projection_and_view(
658 Mat4::orthographic_rh(-3.0, 3.0, -3.0, 3.0, 0.1, 20.0),
661 Mat4::look_at_rh(Vec3::new(-3.0, 3.0, 5.0), Vec3::ZERO, Vec3::Y),
662 );
663 let _lights = crate::test::make_two_directional_light_setup(&stage);
664
665 let white = stage.new_material();
666 let red = stage
667 .new_material()
668 .with_albedo_factor(Vec4::new(1.0, 0.0, 0.0, 1.0));
669
670 let _w = stage.new_primitive().with_material(&white).with_vertices(
671 stage.new_vertices(
672 crate::math::unit_cube()
673 .into_iter()
674 .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)),
675 ),
676 );
677
678 let mut corners = vec![];
679 for x in [-1.0, 1.0] {
680 for y in [-1.0, 1.0] {
681 for z in [-1.0, 1.0] {
682 corners.push(Vec3::new(x, y, z));
683 }
684 }
685 }
686 let mut rs = vec![];
687 for corner in corners.iter() {
688 let bb = BoundingBox {
689 center: Vec3::new(0.5, 0.5, 0.5) * corner,
690 half_extent: Vec3::splat(0.25),
691 };
692 assert!(
693 bb.contains_point(bb.center),
694 "BoundingBox {bb:?} does not contain center"
695 );
696
697 rs.push(
698 stage.new_primitive().with_material(&red).with_vertices(
699 stage.new_vertices(
700 bb.get_mesh()
701 .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)),
702 ),
703 ),
704 );
705 }
706
707 let frame = ctx.get_next_frame().unwrap();
708 stage.render(&frame.view());
709 let img = frame.read_image().block().unwrap();
710 img_diff::assert_img_eq("bvol/bounding_box/get_mesh.png", img);
711 }
712
713 #[test]
723 fn orthographic_projection_lighting() {
724 let ctx = Context::headless(256, 256).block();
725 let view = Mat4::look_at_rh(Vec3::new(-3.0, 3.0, 5.0) * 0.5, Vec3::ZERO, Vec3::Y);
726
727 let render = |projection: Mat4, label: &str| -> usize {
730 let stage = ctx
731 .new_stage()
732 .with_background_color(Vec4::ZERO)
733 .with_msaa_sample_count(4)
734 .with_lighting(true);
735 let _camera = stage
736 .new_camera()
737 .with_projection_and_view(projection, view);
738 let _lights = crate::test::make_two_directional_light_setup(&stage);
739 let white = stage.new_material().with_albedo_factor(Vec4::ONE);
740 let _prim = stage.new_primitive().with_material(&white).with_vertices(
741 stage.new_vertices(
742 crate::math::unit_cube()
743 .into_iter()
744 .map(|(p, n)| Vertex::default().with_position(p).with_normal(n)),
745 ),
746 );
747 let frame = ctx.get_next_frame().unwrap();
748 stage.render(&frame.view());
749 let img = frame.read_image().block().unwrap();
750 img_diff::save(&format!("bvol/ortho_lighting_{label}.png"), img.clone());
751 let bright = img
752 .pixels()
753 .filter(|p| p.0[0] > 10 || p.0[1] > 10 || p.0[2] > 10)
754 .count();
755 bright
756 };
757
758 let ortho_bright = render(
760 Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, -10.0, 10.0),
761 "ortho_correct",
762 );
763
764 let persp_bright = render(crate::camera::perspective(256.0, 256.0), "perspective");
766
767 assert!(
768 persp_bright > 100,
769 "Sanity: perspective should have bright pixels, got {persp_bright}"
770 );
771 assert!(
772 ortho_bright > 100,
773 "Orthographic projection (near=-10, far=10) should produce visible lit output, got \
774 only {ortho_bright} bright pixels (perspective had {persp_bright})"
775 );
776 }
777
778 #[test]
779 fn aabb_intersection() {
780 let a = Aabb::new(Vec3::ZERO, Vec3::ONE);
781 let b = Aabb::new(Vec3::splat(0.9), Vec3::splat(1.9));
782 assert!(a.intersects_aabb(&b));
783 assert!(b.intersects_aabb(&a));
784 }
785
786 #[test]
787 fn aabb_union() {
788 let a = Aabb::new(Vec3::splat(4.0), Vec3::splat(5.0));
789 let b = Aabb::new(Vec3::ZERO, Vec3::ONE);
790 let c = Aabb::union(a, b);
791 assert_eq!(
792 Aabb {
793 min: Vec3::ZERO,
794 max: Vec3::splat(5.0)
795 },
796 c
797 );
798 }
799}