valence_spatial/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use vek::{Aabb, Vec3};
4
5pub mod bvh;
6
7pub trait SpatialIndex<N = f64> {
8    type Object: Bounded3D<N>;
9
10    /// Invokes `f` with every object in the spatial index considered
11    /// colliding according to `collides` in an arbitrary order.
12    ///
13    /// `collides` takes an AABB and returns whether or not a collision
14    /// occurred with the given AABB.
15    ///
16    /// `f` is called with every object considered colliding. If `f` returns
17    /// with `Some(x)`, then `query` exits early with `Some(x)`. If `f` never
18    /// returns with `Some`, then query returns `None`.
19    fn query<C, F, T>(&self, collides: C, f: F) -> Option<T>
20    where
21        C: FnMut(Aabb<N>) -> bool,
22        F: FnMut(&Self::Object) -> Option<T>;
23
24    /// Casts a ray defined by `origin` and `direction` through object AABBs
25    /// and returns the closest intersection for which `f` returns `true`.
26    ///
27    /// `f` is a predicate used to filter intersections. For instance, if a ray
28    /// is shot from a player's eye position, you probably don't want the
29    /// ray to intersect with the player's own hitbox.
30    ///
31    /// If no intersections are found or if `f` never returns `true` then `None`
32    /// is returned. Additionally, the given ray direction must be
33    /// normalized.
34    fn raycast<F>(
35        &self,
36        origin: Vec3<f64>,
37        direction: Vec3<f64>,
38        f: F,
39    ) -> Option<RaycastHit<Self::Object, N>>
40    where
41        F: FnMut(RaycastHit<Self::Object, N>) -> bool;
42}
43
44pub trait Bounded3D<N = f64> {
45    fn aabb(&self) -> Aabb<N>;
46}
47
48/// Represents an intersection between a ray and an entity's axis-aligned
49/// bounding box (hitbox).
50#[derive(PartialEq, Eq, Debug)]
51pub struct RaycastHit<'a, O, N = f64> {
52    /// The object that was hit by the ray.
53    pub object: &'a O,
54    /// The distance from the ray origin to the closest intersection point.
55    /// If the origin of the ray is inside the bounding box, then this will be
56    /// zero.
57    pub near: N,
58    /// The distance from the ray origin to the second intersection point. This
59    /// represents the point at which the ray exits the bounding box.
60    pub far: N,
61}
62
63impl<O, N: Clone> Clone for RaycastHit<'_, O, N> {
64    fn clone(&self) -> Self {
65        Self {
66            object: self.object,
67            near: self.near.clone(),
68            far: self.far.clone(),
69        }
70    }
71}
72
73impl<O, N: Copy> Copy for RaycastHit<'_, O, N> {}
74
75impl<N: Clone> Bounded3D<N> for Aabb<N> {
76    fn aabb(&self) -> Aabb<N> {
77        self.clone()
78    }
79}
80
81#[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)]
82pub struct WithAabb<O, N = f64> {
83    pub object: O,
84    pub aabb: Aabb<N>,
85}
86
87impl<O, N> WithAabb<O, N> {
88    pub fn new(object: O, aabb: Aabb<N>) -> Self {
89        Self { object, aabb }
90    }
91}
92
93impl<O, N: Clone> Bounded3D<N> for WithAabb<O, N> {
94    fn aabb(&self) -> Aabb<N> {
95        self.aabb.clone()
96    }
97}
98
99/// Calculates the intersection between an axis-aligned bounding box and a ray
100/// defined by its origin `ro` and direction `rd`.
101///
102/// If an intersection occurs, `Some((near, far))` is returned. `near` and `far`
103/// are the distance from the origin to the closest and furthest intersection
104/// points respectively. If the intersection occurs inside the bounding box,
105/// then `near` is zero.
106pub fn ray_box_intersect(ro: Vec3<f64>, rd: Vec3<f64>, bb: Aabb<f64>) -> Option<(f64, f64)> {
107    let mut near = -f64::INFINITY;
108    let mut far = f64::INFINITY;
109
110    for i in 0..3 {
111        // Rust's definition of min and max properly handle the NaNs that these
112        // computations might produce.
113        let t0 = (bb.min[i] - ro[i]) / rd[i];
114        let t1 = (bb.max[i] - ro[i]) / rd[i];
115
116        near = near.max(t0.min(t1));
117        far = far.min(t0.max(t1));
118    }
119
120    if near <= far && far >= 0.0 {
121        Some((near.max(0.0), far))
122    } else {
123        None
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn ray_box_edge_cases() {
133        let bb = Aabb {
134            min: Vec3::new(0.0, 0.0, 0.0),
135            max: Vec3::new(1.0, 1.0, 1.0),
136        };
137
138        let ros = [
139            // On a corner
140            Vec3::new(0.0, 0.0, 0.0),
141            // Outside
142            Vec3::new(-0.5, 0.5, -0.5),
143            // In the center
144            Vec3::new(0.5, 0.5, 0.5),
145            // On an edge
146            Vec3::new(0.0, 0.5, 0.0),
147            // On a face
148            Vec3::new(0.0, 0.5, 0.5),
149            // Outside slabs
150            Vec3::new(-2.0, -2.0, -2.0),
151        ];
152
153        let rds = [
154            Vec3::new(1.0, 0.0, 0.0),
155            Vec3::new(-1.0, 0.0, 0.0),
156            Vec3::new(0.0, 1.0, 0.0),
157            Vec3::new(0.0, -1.0, 0.0),
158            Vec3::new(0.0, 0.0, 1.0),
159            Vec3::new(0.0, 0.0, -1.0),
160        ];
161
162        assert!(rds.iter().all(|d| d.is_normalized()));
163
164        for ro in ros {
165            for rd in rds {
166                if let Some((near, far)) = ray_box_intersect(ro, rd, bb) {
167                    assert!(near.is_finite());
168                    assert!(far.is_finite());
169                    assert!(near <= far);
170                    assert!(near >= 0.0);
171                    assert!(far >= 0.0);
172                }
173            }
174        }
175    }
176}