crystal_ball 0.3.0

A path tracing library written in Rust.
Documentation
use std::ops::{Index, IndexMut};

use crate::math::{Point3, Ray, Vec3, XYZEnum};

/// A 3-dimensional bounding box represented by its minimum and maximum corner.
///
/// Defaults to [`f64::INFINITY`] for `min` and [`f64::NEG_INFINITY`] for `max`.
/// This way, including *any* point will result in the correct bounding volume.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Bounds3 {
    pub min: Point3,
    pub max: Point3,
}

impl Default for Bounds3 {
    fn default() -> Self {
        Bounds3 {
            min: Point3::splat(f64::INFINITY),
            max: Point3::splat(f64::NEG_INFINITY),
        }
    }
}

impl Bounds3 {
    /// Create a new [`Bounds3`] using the minimum and maximum point coordinate values, respectively.
    pub fn new(point_a: Point3, point_b: Point3) -> Self {
        Bounds3 {
            min: Point3::new(
                point_a.x.min(point_b.x),
                point_a.y.min(point_b.y),
                point_a.z.min(point_b.z),
            ),
            max: Point3::new(
                point_a.x.max(point_b.x),
                point_a.y.max(point_b.y),
                point_a.z.max(point_b.z),
            ),
        }
    }

    /// Expand `self` to include the given [`Point3`].
    pub fn include_point(self, point: Point3) -> Self {
        Bounds3 {
            min: Point3::new(
                self.min.x.min(point.x),
                self.min.y.min(point.y),
                self.min.z.min(point.z),
            ),
            max: Point3::new(
                self.max.x.max(point.x),
                self.max.y.max(point.y),
                self.max.z.max(point.z),
            ),
        }
    }

    /// Expand `self` to include the given [`Bounds3`].
    pub fn include_bounds(self, bounds: Bounds3) -> Self {
        Bounds3 {
            min: Point3::new(
                self.min.x.min(bounds.min.x),
                self.min.y.min(bounds.min.y),
                self.min.z.min(bounds.min.z),
            ),
            max: Point3::new(
                self.max.x.max(bounds.max.x),
                self.max.y.max(bounds.max.y),
                self.max.z.max(bounds.max.z),
            ),
        }
    }

    /// Calculates the intersection of two [`Bounds3`].
    pub fn intersection(bounds_a: Bounds3, bounds_b: Bounds3) -> Self {
        Bounds3 {
            min: Point3::new(
                bounds_a.min.x.max(bounds_b.min.x),
                bounds_a.min.y.max(bounds_b.min.y),
                bounds_a.min.z.max(bounds_b.min.z),
            ),
            max: Point3::new(
                bounds_a.max.x.min(bounds_b.max.x),
                bounds_a.max.y.min(bounds_b.max.y),
                bounds_a.max.z.min(bounds_b.max.z),
            ),
        }
    }

    /// Checks whether `self` intersects the given [`Bounds3`].
    pub fn overlaps(&self, bounds: Bounds3) -> bool {
        let overlaps_x = (self.max.x >= bounds.min.x) && (self.min.x <= bounds.max.x);
        let overlaps_y = (self.max.y >= bounds.min.y) && (self.min.y <= bounds.max.y);
        let overlaps_z = (self.max.z >= bounds.min.z) && (self.min.z <= bounds.max.z);

        overlaps_x && overlaps_y && overlaps_z
    }

    /// Checks whether `self` includes the given [`Point3`].
    pub fn includes_point(&self, point: Point3) -> bool {
        point.x >= self.min.x
            && point.x <= self.max.x
            && point.y >= self.min.y
            && point.y <= self.max.y
            && point.z >= self.min.z
            && point.z <= self.max.z
    }

    /// Calculate the diagonal from `min` to `max`.
    pub fn diagonal(&self) -> Vec3 {
        self.max - self.min
    }

    /// Calculate the [`Bounds3`]'s surface area.
    pub fn surface_area(&self) -> f64 {
        let diagonal = self.diagonal();

        2.0 * (diagonal.x * diagonal.y + diagonal.x * diagonal.z + diagonal.y * diagonal.z)
    }

    /// Returns the direction of the longest edge.
    pub fn maximum_extent(&self) -> XYZEnum {
        let diagonal = self.diagonal();

        if diagonal.x > diagonal.y && diagonal.x > diagonal.z {
            XYZEnum::X
        } else if diagonal.y > diagonal.z {
            XYZEnum::Y
        } else {
            XYZEnum::Z
        }
    }

    /// Calculate the position of a [`Point3`] relative to the corners.
    ///
    /// `(0.0, 0.0, 0.0)` represents the `min` and `(1.0, 1.0, 1.0)` represents `max`.
    pub fn offset(&self, point: Point3) -> Vec3 {
        let mut offset = point - self.min;

        if self.max.x > self.min.x {
            offset = Vec3::new(offset.x / (self.max.x - self.min.x), offset.y, offset.z);
        }
        if self.max.y > self.min.y {
            offset = Vec3::new(offset.x, offset.y / (self.max.y - self.min.y), offset.z);
        }
        if self.max.z > self.min.z {
            offset = Vec3::new(offset.x, offset.y, offset.z / (self.max.z - self.min.z));
        }

        offset
    }

    /// Performs a ray intersection against `self`.
    ///
    /// This only determines whether the [`Ray`] intersects `self` and ***not*** where they intersect.
    pub fn intersects(
        &self,
        ray: Ray,
        max_intersection_distance: f64,
        inverse_direction: Vec3,
        direction_is_negative: [bool; 3],
    ) -> bool {
        let direction_is_negative_0 = direction_is_negative[0] as usize;
        let direction_is_not_negative_0 = !direction_is_negative[0] as usize;
        let direction_is_negative_1 = direction_is_negative[1] as usize;
        let direction_is_not_negative_1 = !direction_is_negative[1] as usize;
        let direction_is_negative_2 = direction_is_negative[2] as usize;
        let direction_is_not_negative_2 = !direction_is_negative[2] as usize;

        let mut t_min = (self[direction_is_negative_0].x - ray.origin.x) * inverse_direction.x;
        let mut t_max = (self[direction_is_not_negative_0].x - ray.origin.x) * inverse_direction.x;
        let ty_min = (self[direction_is_negative_1].y - ray.origin.y) * inverse_direction.y;
        let ty_max = (self[direction_is_not_negative_1].y - ray.origin.y) * inverse_direction.y;

        if t_min > ty_max || ty_min > t_max {
            return false;
        }
        if ty_min > t_min {
            t_min = ty_min;
        }
        if ty_max < t_max {
            t_max = ty_max;
        }

        let tz_min = (self[direction_is_negative_2].z - ray.origin.z) * inverse_direction.z;
        let tz_max = (self[direction_is_not_negative_2].z - ray.origin.z) * inverse_direction.z;

        if t_min > tz_max || tz_min > t_max {
            return false;
        }
        if tz_min > t_min {
            t_min = tz_min;
        }
        if tz_max < t_max {
            t_max = tz_max;
        }

        t_min < max_intersection_distance && t_max > 0.0
    }
}

impl Index<usize> for Bounds3 {
    type Output = Point3;

    fn index(&self, index: usize) -> &Self::Output {
        match index {
            0 => &self.min,
            1 => &self.max,
            _ => panic!(
                "index out of bounds: the len is 2 but the index is {}",
                index
            ),
        }
    }
}

impl IndexMut<usize> for Bounds3 {
    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
        match index {
            0 => &mut self.min,
            1 => &mut self.max,
            _ => panic!(
                "index out of bounds: the len is 2 but the index is {}",
                index
            ),
        }
    }
}

#[cfg(test)]
mod tests {
    use assert_approx_eq::assert_approx_eq;

    use crate::math::Point3;
    use crate::util::EPSILON_F64;

    use super::Bounds3;

    #[test]
    fn bounds_include_point() {
        let mut bounds = Bounds3::new(Point3::splat(-1.0), Point3::splat(1.0));
        assert!(!bounds.includes_point(Point3::new(-1.2, 0.6, 6.3)));

        bounds = bounds.include_point(Point3::new(-1.2, 0.6, 6.3));
        assert!(bounds.includes_point(Point3::new(-1.2, 0.6, 6.3)));
        assert_approx_eq!(bounds.min, Point3::new(-1.2, -1.0, -1.0), EPSILON_F64);
        assert_approx_eq!(bounds.max, Point3::new(1.0, 1.0, 6.3), EPSILON_F64);
    }

    #[test]
    fn bounds_include_bounds() {
        let mut bounds = Bounds3::default();
        assert!(!bounds.includes_point(Point3::new(3.8, -8.42, -0.3)));
        assert!(!bounds.includes_point(Point3::new(-6.7, -2.0, 0.4)));

        bounds = Bounds3::new(Point3::new(3.8, -8.42, -0.3), Point3::new(-6.7, -2.0, 0.4));
        assert_approx_eq!(bounds.min, Point3::new(-6.7, -8.42, -0.3));
        assert_approx_eq!(bounds.max, Point3::new(3.8, -2.0, 0.4));
        assert!(bounds.includes_point(Point3::new(3.8, -8.42, -0.3)));
        assert!(bounds.includes_point(Point3::new(-6.7, -2.0, 0.4)));
    }

    #[test]
    fn bounds_index() {
        let mut bounds = Bounds3::new(Point3::new(3.8, -8.42, -0.3), Point3::new(-6.7, -2.0, 0.4));
        assert_approx_eq!(bounds.min, bounds[0]);
        assert_approx_eq!(bounds.max, bounds[1]);

        bounds[0] = Point3::new(-5.987, -8.7, -1.2);
        bounds[1] = Point3::new(8.7, 1.2, 5.987);

        assert_approx_eq!(bounds.min, Point3::new(-5.987, -8.7, -1.2));
        assert_approx_eq!(bounds.max, Point3::new(8.7, 1.2, 5.987));
    }

    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn bounds_index_panic() {
        let bounds = Bounds3::default();
        let _x = bounds[4];
    }

    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn bounds_index_mut_panic() {
        let mut bounds = Bounds3::default();
        bounds[2] = Point3::new(-5.987, -8.7, -1.2);
    }
}