Skip to main content

molgfx_math/bounds/
source.rs

1//! Where a hierarchy reads its primitive bounds from.
2//!
3//! Building over a `&[Aabb]` forces the caller to materialise one box per
4//! primitive first. At twenty-four bytes each that is a temporary the size of
5//! the structure — twenty-four gigabytes for a billion atoms — allocated only
6//! to be read once and dropped. A source computes each box on demand from the
7//! columns the scene already owns, so the temporary never exists.
8//!
9//! Implementations must be pure and cheap: the builder reads every bound twice
10//! (once for the scene bound, once for the leaf it lands in) and reads them
11//! from several threads at once.
12
13use crate::{Aabb, Vec3};
14
15/// A random-access supply of primitive bounds.
16pub trait BvhSource: Sync {
17    /// Number of primitives addressable by [`Self::bound`].
18    fn len(&self) -> usize;
19
20    /// Bound of one primitive. Rows outside `0..len()` return the empty bound,
21    /// which the builder skips.
22    fn bound(&self, index: u32) -> Aabb;
23
24    /// Whether the source supplies no primitives.
25    fn is_empty(&self) -> bool {
26        self.len() == 0
27    }
28}
29
30impl BvhSource for [Aabb] {
31    #[inline]
32    fn len(&self) -> usize {
33        <[Aabb]>::len(self)
34    }
35
36    #[inline]
37    fn bound(&self, index: u32) -> Aabb {
38        match self.get(index as usize) {
39            Some(bound) => *bound,
40            None => Aabb::EMPTY,
41        }
42    }
43}
44
45impl BvhSource for Vec<Aabb> {
46    #[inline]
47    fn len(&self) -> usize {
48        self.as_slice().len()
49    }
50
51    #[inline]
52    fn bound(&self, index: u32) -> Aabb {
53        self.as_slice().bound(index)
54    }
55}
56
57impl<const N: usize> BvhSource for [Aabb; N] {
58    #[inline]
59    fn len(&self) -> usize {
60        N
61    }
62
63    #[inline]
64    fn bound(&self, index: u32) -> Aabb {
65        self.as_slice().bound(index)
66    }
67}
68
69impl<T: BvhSource + ?Sized> BvhSource for &T {
70    #[inline]
71    fn len(&self) -> usize {
72        (**self).len()
73    }
74
75    #[inline]
76    fn bound(&self, index: u32) -> Aabb {
77        (**self).bound(index)
78    }
79}
80
81/// Bounds derived from a position column and a radius column.
82///
83/// This is the atom case, and the reason the trait exists: a scene already
84/// holds both columns, so the boxes are arithmetic rather than storage. Rows
85/// past the shorter of the two columns are empty.
86#[derive(Clone, Copy, Debug)]
87pub struct SphereBounds<'a> {
88    centers: &'a [[f32; 3]],
89    radii: &'a [f32],
90}
91
92impl<'a> SphereBounds<'a> {
93    /// Pairs a position column with a radius column.
94    #[must_use]
95    pub fn new(centers: &'a [[f32; 3]], radii: &'a [f32]) -> Self {
96        Self { centers, radii }
97    }
98}
99
100impl BvhSource for SphereBounds<'_> {
101    #[inline]
102    fn len(&self) -> usize {
103        self.centers.len().min(self.radii.len())
104    }
105
106    #[inline]
107    fn bound(&self, index: u32) -> Aabb {
108        let index = index as usize;
109        let (Some(center), Some(radius)) = (self.centers.get(index), self.radii.get(index)) else {
110            return Aabb::EMPTY;
111        };
112        let center = Vec3::from_array(*center);
113        let extent = Vec3::splat(radius.abs());
114        Aabb::new(center - extent, center + extent)
115    }
116}
117
118/// Bounds swept between two position columns sharing one radius column.
119///
120/// A trajectory interpolates between two frames, so the primitive a ray can hit
121/// is the box enclosing both endpoints. Building over this keeps the moving
122/// hierarchy conservative without materialising the swept boxes.
123#[derive(Clone, Copy, Debug)]
124pub struct SweptSphereBounds<'a> {
125    start: &'a [[f32; 3]],
126    end: &'a [[f32; 3]],
127    radii: &'a [f32],
128}
129
130impl<'a> SweptSphereBounds<'a> {
131    /// Pairs two position columns with the radius column they share.
132    #[must_use]
133    pub fn new(start: &'a [[f32; 3]], end: &'a [[f32; 3]], radii: &'a [f32]) -> Self {
134        Self { start, end, radii }
135    }
136}
137
138impl BvhSource for SweptSphereBounds<'_> {
139    #[inline]
140    fn len(&self) -> usize {
141        self.start.len().min(self.end.len()).min(self.radii.len())
142    }
143
144    #[inline]
145    fn bound(&self, index: u32) -> Aabb {
146        let index = index as usize;
147        let (Some(start), Some(end), Some(radius)) = (
148            self.start.get(index),
149            self.end.get(index),
150            self.radii.get(index),
151        ) else {
152            return Aabb::EMPTY;
153        };
154        let start = Vec3::from_array(*start);
155        let end = Vec3::from_array(*end);
156        let extent = Vec3::splat(radius.abs());
157        Aabb::new(start.min(end) - extent, start.max(end) + extent)
158    }
159}
160
161#[cfg(test)]
162#[path = "source_tests.rs"]
163mod tests;