crystal_ball 0.3.0

A path tracing library written in Rust.
Documentation
use std::f64::consts::FRAC_1_PI;

use crystal_ball_derive::Transformable;

use crate::math::{Bounds3, Hit, Point2, Point3, Ray, Transform, Vec3};
use crate::shapes::Shape;

/// A geometrically perfect sphere.
#[derive(Copy, Clone, Default, Debug, PartialEq, Transformable)]
#[internal]
pub struct Sphere {
    #[transform]
    pub transform: Transform,
}

impl Sphere {
    /// Create a new [`Sphere`].
    pub fn new() -> Self {
        Sphere {
            transform: Transform::default(),
        }
    }

    /// Map a point on the sphere to UV coordinates.
    pub fn uv_map(&self, point: Point3) -> Point2 {
        let u = 0.5 + point.z.atan2(point.x) * 0.5 * FRAC_1_PI;
        let v = 0.5 - point.y.asin() * FRAC_1_PI;

        Point2::new(u, v)
    }
}

impl Shape for Sphere {
    fn intersects(&self, ray: Ray) -> Option<Hit> {
        let ray = self.transform.inverse_transform_ray(ray);

        // If the ray origin is outside of the sphere,
        // tangent_squared is the square of the vector from the ray origin to a tangent point on the sphere.
        // If it is inside, tangent_squared will be < 0, but the formulas below still hold.
        let tangent_squared = ray.origin.to_vec3().magnitude_squared() - 1.0;
        let projection = Vec3::dot(ray.direction, ray.origin.into());
        let discriminant = projection.powi(2) - tangent_squared;

        if discriminant <= 0.0 {
            return None;
        }

        let mut intersection_distance = if tangent_squared > 0.0 {
            // Ray origin is outside the sphere.
            -projection - discriminant.sqrt()
        } else {
            // Ray origin is inside the sphere.
            -projection + discriminant.sqrt()
        };

        if intersection_distance < 0.0 {
            return None;
        }

        let mut intersection_point = ray.get(intersection_distance);

        let mut normal = intersection_point.to_vec3();
        let uv = self.uv_map((-normal).into());

        let ray = self.transform.transform_ray(ray);

        intersection_point = self.transform.mat4 * intersection_point;
        intersection_distance = (intersection_point - ray.origin).magnitude();

        normal = (self.transform.mat4_inverse.transpose() * normal).normalize();

        let mat4 = self.transform.mat4;
        let origin = Point3::new(mat4[0][3], mat4[1][3], mat4[2][3]);
        let mut tangent = Vec3::cross(Vec3::Y, intersection_point - origin)
            .normalize()
            .extend(1.0);

        if Vec3::dot(ray.direction, normal) > 0.0 {
            normal = -normal;
            tangent = -tangent;
        }

        Some(Hit::new(
            intersection_point,
            normal,
            Some(tangent),
            intersection_distance,
            Point2::new(1.0 - uv.x, 1.0 - uv.y),
        ))
    }

    fn bounds(&self) -> Bounds3 {
        let min = Point3::splat(-1.0);
        let max = Point3::splat(1.0);

        self.transform.transform_bounds(Bounds3::new(min, max))
    }
}