frustum 0.2.1

Simple library to set up a frustum, such as a camera.
Documentation
use frustum::{Frustum, Point3, Vec3, WorldSpace};
use palette::*;
use rayon::prelude::*;

pub struct Sphere {
    origin: Point3<WorldSpace>,
    radius: f64,
    ambient: palette::Lab,
    diffuse: palette::Lab,
    specular: palette::Lab,
}

impl Sphere {
    pub fn intersect(
        &self,
        ro: &Point3<WorldSpace>,
        rd: &Vec3<WorldSpace>,
    ) -> Option<(Point3<WorldSpace>, f64)> {
        let diff = self.origin - *ro;
        let t0 = diff.dot(*rd);
        let d_squared = diff.dot(diff) - t0 * t0;
        if d_squared > (self.radius * self.radius) {
            return None;
        }

        let t1 = (self.radius * self.radius) - d_squared;

        let t = if t0 > t1 + std::f64::EPSILON {
            t0 - t1
        } else {
            t0 + t1
        };

        if t > std::f64::EPSILON {
            return Some((*ro + *rd * t, t));
        }
        None
    }

    pub fn normal(&self, target: &Point3<WorldSpace>) -> Vec3<WorldSpace> {
        (*target - self.origin).normalize()
    }

    pub fn color(
        &self,
        target: &Point3<WorldSpace>,
        light: &Point3<WorldSpace>,
        origin: &Point3<WorldSpace>,
    ) -> palette::Lab {
        let normal = self.normal(target).normalize();
        let light_dir = (*light - *target).normalize();
        let view_dir = (*origin - *target).normalize();
        let half_dir = (light_dir + view_dir).normalize();

        let lambertian = normal.dot(light_dir).max(0.).min(1.) as f32;
        let specular = if lambertian > 0.0 {
            half_dir.dot(normal).max(0.).powi(128).min(1.) as f32
        } else {
            0.0
        };

        self.ambient
            .mix(&self.diffuse, lambertian * 0.7)
            .mix(&self.specular, specular * 1.5)
            .clamp()
    }
}

fn main() {
    let light = Point3::<WorldSpace>::new(-5., 5., 20.);

    let sphere = Sphere {
        origin: Point3::<WorldSpace>::new(0., 0., 0.),
        radius: 2.5,
        ambient: palette::Lab::new(20., -6., -11.),
        diffuse: palette::Lab::new(86., -9., 14.),
        specular: palette::Lab::new(100., -3., 4.),
    };

    let camera = Frustum {
        origin: Point3::<WorldSpace>::new(0.0, 0.0, 10.0),
        target: Point3::<WorldSpace>::new(0.0, 0.0, 0.0),
        fovy: 45.0,
        ncp: 1.0,
        fcp: 20.0,
        width: 500,
        height: 500,
    };

    let data = camera
        .iter()
        .collect::<Vec<_>>()
        .par_iter()
        .map(|(_x, _y, ro, rd)| {
            let color: Srgb = match sphere.intersect(&ro, &rd) {
                Some((target, _)) => {
                    sphere.color(&target, &light, &camera.origin)
                }
                None => palette::Lab::new(100., 0., 0.),
            }
            .into();

            color.clamp().into_raw::<[f32; 3]>()
        })
        .collect::<Vec<[f32; 3]>>()
        .iter()
        .flatten()
        .map(|v| (*v * 255.0) as u8)
        .collect::<Vec<u8>>();

    image::save_buffer(
        "raytracing_result.png",
        &data,
        camera.width as u32,
        camera.height as u32,
        image::ColorType::Rgb8,
    )
    .unwrap_or_else(|err| {
        println!("Cannot save photorealistic image: {}", err);
        std::process::exit(4);
    });
}