#![doc = include_str!("../README.md")] #![deny( rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links, rustdoc::missing_crate_level_docs, rustdoc::invalid_codeblock_attributes, rustdoc::invalid_rust_codeblocks, rustdoc::bare_urls, rustdoc::invalid_html_tags )] #![warn( trivial_casts, trivial_numeric_casts, unused_lifetimes, unused_import_braces, unreachable_pub, clippy::dbg_macro )] use vek::{Aabb, Vec3}; pub mod bvh; pub trait SpatialIndex { type Object: Bounded3D; /// Invokes `f` with every object in the spatial index considered /// colliding according to `collides` in an arbitrary order. /// /// `collides` takes an AABB and returns whether or not a collision /// occurred with the given AABB. /// /// `f` is called with every object considered colliding. If `f` returns /// with `Some(x)`, then `query` exits early with `Some(x)`. If `f` never /// returns with `Some`, then query returns `None`. fn query(&self, collides: C, f: F) -> Option where C: FnMut(Aabb) -> bool, F: FnMut(&Self::Object) -> Option; /// Casts a ray defined by `origin` and `direction` through object AABBs /// and returns the closest intersection for which `f` returns `true`. /// /// `f` is a predicate used to filter intersections. For instance, if a ray /// is shot from a player's eye position, you probably don't want the /// ray to intersect with the player's own hitbox. /// /// If no intersections are found or if `f` never returns `true` then `None` /// is returned. Additionally, the given ray direction must be /// normalized. fn raycast( &self, origin: Vec3, direction: Vec3, f: F, ) -> Option> where F: FnMut(RaycastHit) -> bool; } pub trait Bounded3D { fn aabb(&self) -> Aabb; } /// Represents an intersection between a ray and an entity's axis-aligned /// bounding box (hitbox). #[derive(PartialEq, Eq, Debug)] pub struct RaycastHit<'a, O, N = f64> { /// The object that was hit by the ray. pub object: &'a O, /// The distance from the ray origin to the closest intersection point. /// If the origin of the ray is inside the bounding box, then this will be /// zero. pub near: N, /// The distance from the ray origin to the second intersection point. This /// represents the point at which the ray exits the bounding box. pub far: N, } impl Clone for RaycastHit<'_, O, N> { fn clone(&self) -> Self { Self { object: self.object, near: self.near.clone(), far: self.far.clone(), } } } impl Copy for RaycastHit<'_, O, N> {} impl Bounded3D for Aabb { fn aabb(&self) -> Aabb { self.clone() } } #[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)] pub struct WithAabb { pub object: O, pub aabb: Aabb, } impl WithAabb { pub fn new(object: O, aabb: Aabb) -> Self { Self { object, aabb } } } impl Bounded3D for WithAabb { fn aabb(&self) -> Aabb { self.aabb.clone() } } /// Calculates the intersection between an axis-aligned bounding box and a ray /// defined by its origin `ro` and direction `rd`. /// /// If an intersection occurs, `Some((near, far))` is returned. `near` and `far` /// are the distance from the origin to the closest and furthest intersection /// points respectively. If the intersection occurs inside the bounding box, /// then `near` is zero. pub fn ray_box_intersect(ro: Vec3, rd: Vec3, bb: Aabb) -> Option<(f64, f64)> { let mut near = -f64::INFINITY; let mut far = f64::INFINITY; for i in 0..3 { // Rust's definition of min and max properly handle the NaNs that these // computations might produce. let t0 = (bb.min[i] - ro[i]) / rd[i]; let t1 = (bb.max[i] - ro[i]) / rd[i]; near = near.max(t0.min(t1)); far = far.min(t0.max(t1)); } if near <= far && far >= 0.0 { Some((near.max(0.0), far)) } else { None } } #[cfg(test)] mod tests { use super::*; #[test] fn ray_box_edge_cases() { let bb = Aabb { min: Vec3::new(0.0, 0.0, 0.0), max: Vec3::new(1.0, 1.0, 1.0), }; let ros = [ // On a corner Vec3::new(0.0, 0.0, 0.0), // Outside Vec3::new(-0.5, 0.5, -0.5), // In the center Vec3::new(0.5, 0.5, 0.5), // On an edge Vec3::new(0.0, 0.5, 0.0), // On a face Vec3::new(0.0, 0.5, 0.5), // Outside slabs Vec3::new(-2.0, -2.0, -2.0), ]; let rds = [ Vec3::new(1.0, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, -1.0, 0.0), Vec3::new(0.0, 0.0, 1.0), Vec3::new(0.0, 0.0, -1.0), ]; assert!(rds.iter().all(|d| d.is_normalized())); for ro in ros { for rd in rds { if let Some((near, far)) = ray_box_intersect(ro, rd, bb) { assert!(near.is_finite()); assert!(far.is_finite()); assert!(near <= far); assert!(near >= 0.0); assert!(far >= 0.0); } } } } }