#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Position3 {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Position3 {
pub const fn new(x: f32, y: f32, z: f32) -> Self {
Self { x, y, z }
}
pub fn distance_squared(self, other: Self) -> f32 {
let dx = self.x - other.x;
let dy = self.y - other.y;
let dz = self.z - other.z;
dx.mul_add(dx, dy.mul_add(dy, dz * dz))
}
pub const fn to_vec3(self) -> Vec3 {
Vec3::new(self.x, self.y, self.z)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Vec3 {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Vec3 {
pub const fn new(x: f32, y: f32, z: f32) -> Self {
Self { x, y, z }
}
pub fn dot(self, other: Self) -> f32 {
self.x
.mul_add(other.x, self.y.mul_add(other.y, self.z * other.z))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Aabb3 {
pub min: Position3,
pub max: Position3,
}
impl Aabb3 {
pub const fn new(min: Position3, max: Position3) -> Self {
Self { min, max }
}
pub fn from_center_half_extents(center: Position3, half_extents: Vec3) -> Self {
Self {
min: Position3::new(
center.x - half_extents.x,
center.y - half_extents.y,
center.z - half_extents.z,
),
max: Position3::new(
center.x + half_extents.x,
center.y + half_extents.y,
center.z + half_extents.z,
),
}
}
pub fn overlaps(self, other: Self) -> bool {
self.min.x <= other.max.x
&& self.max.x >= other.min.x
&& self.min.y <= other.max.y
&& self.max.y >= other.min.y
&& self.min.z <= other.max.z
&& self.max.z >= other.min.z
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Plane3 {
pub normal: Vec3,
pub distance: f32,
}
impl Plane3 {
pub const fn new(normal: Vec3, distance: f32) -> Self {
Self { normal, distance }
}
pub fn from_normal_and_point(normal: Vec3, point: Position3) -> Self {
Self {
normal,
distance: -normal.dot(point.to_vec3()),
}
}
pub fn signed_distance_to_position(self, position: Position3) -> f32 {
self.normal.dot(position.to_vec3()) + self.distance
}
pub fn intersects_aabb(self, aabb: Aabb3) -> bool {
let positive = Position3::new(
if self.normal.x >= 0.0 {
aabb.max.x
} else {
aabb.min.x
},
if self.normal.y >= 0.0 {
aabb.max.y
} else {
aabb.min.y
},
if self.normal.z >= 0.0 {
aabb.max.z
} else {
aabb.min.z
},
);
self.signed_distance_to_position(positive) >= 0.0
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Frustum3 {
pub planes: [Plane3; 6],
}
impl Frustum3 {
pub const fn from_planes(planes: [Plane3; 6]) -> Self {
Self { planes }
}
pub fn from_aabb(aabb: Aabb3) -> Self {
Self::from_planes([
Plane3::new(Vec3::new(1.0, 0.0, 0.0), -aabb.min.x),
Plane3::new(Vec3::new(-1.0, 0.0, 0.0), aabb.max.x),
Plane3::new(Vec3::new(0.0, 1.0, 0.0), -aabb.min.y),
Plane3::new(Vec3::new(0.0, -1.0, 0.0), aabb.max.y),
Plane3::new(Vec3::new(0.0, 0.0, 1.0), -aabb.min.z),
Plane3::new(Vec3::new(0.0, 0.0, -1.0), aabb.max.z),
])
}
pub fn intersects_aabb(self, aabb: Aabb3) -> bool {
self.planes.iter().all(|plane| plane.intersects_aabb(aabb))
}
pub fn intersects_bounds(self, position: Position3, bounds: Bounds) -> bool {
self.intersects_aabb(bounds.to_aabb(position))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Bounds {
#[default]
Point,
Sphere {
radius: f32,
},
Aabb {
half_extents: Vec3,
},
}
impl Bounds {
pub fn to_aabb(self, position: Position3) -> Aabb3 {
match self {
Self::Point => Aabb3::new(position, position),
Self::Sphere { radius } => {
Aabb3::from_center_half_extents(position, Vec3::new(radius, radius, radius))
}
Self::Aabb { half_extents } => Aabb3::from_center_half_extents(position, half_extents),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CellCoord3 {
pub x: i32,
pub y: i32,
pub z: i32,
}
impl CellCoord3 {
pub const fn new(x: i32, y: i32, z: i32) -> Self {
Self { x, y, z }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GridSpec {
cell_size: f32,
}
impl GridSpec {
pub fn new(cell_size: f32) -> Result<Self, GridSpecError> {
if cell_size.is_finite() && cell_size > 0.0 {
Ok(Self { cell_size })
} else {
Err(GridSpecError::InvalidCellSize)
}
}
pub const fn cell_size(self) -> f32 {
self.cell_size
}
pub fn cell_at(self, position: Position3) -> CellCoord3 {
let inv = 1.0 / self.cell_size;
CellCoord3::new(
floor_to_cell(position.x * inv),
floor_to_cell(position.y * inv),
floor_to_cell(position.z * inv),
)
}
pub fn cells_for_aabb(self, aabb: Aabb3) -> Vec<CellCoord3> {
let min = self.cell_at(aabb.min);
let max = self.cell_at(aabb.max);
let capacity = axis_cell_count(min.x, max.x)
.saturating_mul(axis_cell_count(min.y, max.y))
.saturating_mul(axis_cell_count(min.z, max.z));
let mut cells = Vec::with_capacity(capacity);
for x in min.x..=max.x {
for y in min.y..=max.y {
for z in min.z..=max.z {
cells.push(CellCoord3::new(x, y, z));
}
}
}
cells
}
pub fn cells_for_bounds(self, position: Position3, bounds: Bounds) -> Vec<CellCoord3> {
self.cells_for_aabb(bounds.to_aabb(position))
}
}
#[allow(clippy::cast_possible_truncation)]
fn floor_to_cell(value: f32) -> i32 {
value.floor() as i32
}
fn axis_cell_count(min: i32, max: i32) -> usize {
if max < min {
return 0;
}
usize::try_from(i64::from(max) - i64::from(min) + 1).unwrap_or(usize::MAX)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GridSpecError {
InvalidCellSize,
}
impl core::fmt::Display for GridSpecError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidCellSize => f.write_str("cell size must be finite and positive"),
}
}
}
impl std::error::Error for GridSpecError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frustum_from_aabb_accepts_intersecting_bounds_and_rejects_outside() {
let frustum = Frustum3::from_aabb(Aabb3::new(
Position3::new(0.0, -10.0, -10.0),
Position3::new(50.0, 10.0, 10.0),
));
assert!(frustum.intersects_bounds(Position3::new(25.0, 0.0, 0.0), Bounds::Point));
assert!(frustum.intersects_bounds(
Position3::new(55.0, 0.0, 0.0),
Bounds::Sphere { radius: 8.0 }
));
assert!(!frustum.intersects_bounds(Position3::new(-5.0, 0.0, 0.0), Bounds::Point));
assert!(!frustum.intersects_bounds(Position3::new(25.0, 20.0, 0.0), Bounds::Point));
}
}