use std::fmt::Debug; use anyhow::Result; use nalgebra::Vector3; use ordered_float::NotNan; use crate::image::Color; use crate::ray::Ray; pub trait ObjectKind: Debug + Send + Sync { /// Determine where the ray intersects this object, returning the earliest /// time this happens. Returns None if no intersection occurs. /// /// Also known as Trace_Ray in the slides, except not the part where it calls /// Shade_Ray. fn intersects_ray_at(&self, ray: &Ray) -> Result>; } /// An object in the scene #[derive(Debug)] pub struct Object { pub kind: Box, /// Index into the scene's material color list pub material: usize, } #[derive(Debug)] pub struct Rect { pub upper_left: Vector3, pub upper_right: Vector3, pub lower_left: Vector3, pub lower_right: Vector3, } #[derive(Debug)] pub struct Material { pub diffuse_color: Vector3, pub specular_color: Vector3, pub k_a: f64, pub k_d: f64, pub k_s: f64, pub exponent: f64, } #[derive(Debug)] pub enum LightKind { /// A point light source exists at a point and emits light in all directions Point { location: Vector3, }, Directional { direction: Vector3, }, } #[derive(Debug)] pub struct Light { pub kind: LightKind, pub color: Vector3, } #[derive(Debug, Default)] pub struct Scene { pub eye_pos: Vector3, pub view_dir: Vector3, pub up_dir: Vector3, /// Horizontal field of view (in degrees) pub hfov: f64, pub parallel_projection: bool, pub image_width: usize, pub image_height: usize, /// Background color pub bkg_color: Color, pub materials: Vec, pub lights: Vec, pub objects: Vec, } #[derive(Derivative)] #[derivative(Debug, PartialEq, PartialOrd, Ord)] pub struct IntersectionContext { /// The time of the intersection in the parametric ray /// /// Unfortunately, IEEE floats in Rust don't have total ordering, because /// NaNs violate ordering properties. The way to remedy this is to ensure we /// don't have NaNs by wrapping it into this type, which then implements /// total ordering. pub time: NotNan, /// The intersection point. #[derivative(PartialEq = "ignore", Ord = "ignore")] pub point: Vector3, /// The normal vector protruding from the surface of the object at the /// intersection point #[derivative(PartialEq = "ignore", Ord = "ignore")] pub normal: Vector3, } impl Eq for IntersectionContext {} impl Scene { /// Determine the color that should be used to fill this pixel. /// /// - material_idx is the index into the materials list. /// - intersection_context contains information on vectors where the /// intersection occurred /// /// Also known as Shade_Ray in the slides. pub fn compute_pixel_color( &self, material_idx: usize, intersection_context: IntersectionContext, ) -> Color { // TODO: Does it make sense to make this function fallible from an API // design standpoint? let material = match self.materials.get(material_idx) { Some(v) => v, None => return self.bkg_color, }; let ambient_component = material.k_a * material.diffuse_color; let diffuse_and_specular: Vector3 = self .lights .iter() .map(|light| { // The vector pointing in the direction of the light let light_direction = match light.kind { LightKind::Point { location } => { location - intersection_context.point } LightKind::Directional { direction } => direction, } .normalize(); let normal = intersection_context.normal.normalize(); let viewer_direction = self.eye_pos - intersection_context.point; let halfway_direction = ((light_direction + viewer_direction) / 2.0).normalize(); let diffuse_component = material.k_d * material.diffuse_color * normal.dot(&light_direction).max(0.0); let specular_component = material.k_s * material.specular_color * normal .dot(&halfway_direction) .max(0.0) .powf(material.exponent); diffuse_component + specular_component }) .sum(); ambient_component + diffuse_and_specular } /// Determine the boundaries of the viewing window in world coordinates pub fn compute_viewing_window(&self, distance: f64) -> Rect { // Compute viewing directions let u = self.view_dir.cross(&self.up_dir).normalize(); let v = u.cross(&self.view_dir).normalize(); // Compute dimensions of viewing window based on field of view let viewing_width = { // Divide the angle in 2 since we are trying to use trig rules so we must // get it from a right triangle let half_hfov = self.hfov.to_radians() / 2.0; // tan(hfov / 2) = w / 2d let w_over_2d = half_hfov.tan(); // To find the viewing width we must multiply by 2d now w_over_2d * 2.0 * distance }; let aspect_ratio = self.image_width as f64 / self.image_height as f64; let viewing_height = viewing_width / aspect_ratio; // Compute viewing window corners let n = self.view_dir.normalize(); #[rustfmt::skip] // Otherwise this line wraps over let view_window = Rect { upper_left: self.eye_pos + n * distance - u * (viewing_width / 2.0) + v * (viewing_height / 2.0), upper_right: self.eye_pos + n * distance + u * (viewing_width / 2.0) + v * (viewing_height / 2.0), lower_left: self.eye_pos + n * distance - u * (viewing_width / 2.0) - v * (viewing_height / 2.0), lower_right: self.eye_pos + n * distance + u * (viewing_width / 2.0) - v * (viewing_height / 2.0), }; view_window } }