diff --git a/assignment-1/.gitignore b/assignment-1/.gitignore index ea8c4bf..a6ff48c 100644 --- a/assignment-1/.gitignore +++ b/assignment-1/.gitignore @@ -1 +1,2 @@ /target +*.ppm diff --git a/assignment-1/Cargo.lock b/assignment-1/Cargo.lock index 15567d1..5da292b 100644 --- a/assignment-1/Cargo.lock +++ b/assignment-1/Cargo.lock @@ -14,7 +14,9 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "itertools", "num", + "ordered-float", ] [[package]] @@ -72,6 +74,12 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "errno" version = "0.2.8" @@ -130,6 +138,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "libc" version = "0.2.139" @@ -227,6 +244,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +[[package]] +name = "ordered-float" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d84eb1409416d254e4a9c8fa56cc24701755025b458f0fcd8e59e1f5f40c23bf" +dependencies = [ + "num-traits", +] + [[package]] name = "os_str_bytes" version = "6.4.1" diff --git a/assignment-1/Cargo.toml b/assignment-1/Cargo.toml index eb235bb..8ddbc92 100644 --- a/assignment-1/Cargo.toml +++ b/assignment-1/Cargo.toml @@ -8,4 +8,6 @@ edition = "2021" [dependencies] anyhow = "1.0.68" clap = { version = "4.1.4", features = ["derive"] } +itertools = "0.10.5" num = { version = "0.4.0", features = ["serde"] } +ordered-float = "3.4.0" diff --git a/assignment-1/src/image.rs b/assignment-1/src/image.rs new file mode 100644 index 000000000..15329f4 --- /dev/null +++ b/assignment-1/src/image.rs @@ -0,0 +1,35 @@ +use std::io::{Result, Write}; + +/// A 24-bit pixel represented by a red, green, and blue value. +#[derive(Clone, Copy, Default, Debug)] +pub struct Pixel(pub u8, pub u8, pub u8); + +/// A representation of an image +pub struct Image { + /// Width in pixels + pub(crate) width: usize, + + /// Height in pixels + pub(crate) height: usize, + + /// Pixel data in row-major form. + pub(crate) data: Vec, +} + +impl Image { + /// Write the image in PPM format to a file. + pub fn write(&self, mut w: impl Write) -> Result<()> { + // Header + let header = format!("P3 {} {} 255\n", self.width, self.height); + w.write_all(header.as_bytes())?; + + // Pixel data + for pixel in self.data.iter() { + let Pixel(red, green, blue) = pixel; + let pixel = format!("{red} {green} {blue}\n"); + w.write_all(pixel.as_bytes())?; + } + + Ok(()) + } +} diff --git a/assignment-1/src/input_file.rs b/assignment-1/src/input_file.rs index 6eb7fca..7eb1312 100644 --- a/assignment-1/src/input_file.rs +++ b/assignment-1/src/input_file.rs @@ -54,11 +54,17 @@ pub fn parse_input_file(path: impl AsRef) -> Result { "hfov" => scene.hfov = parts[0], "bkgcolor" => scene.bkg_color = read_vec3()?, - "mtlcolor" => material_color = Some(read_vec3()?), + + "mtlcolor" => { + let idx = scene.material_colors.len(); + material_color = Some(idx); + scene.material_colors.push(read_vec3()?); + }, "sphere" => scene.objects.push(Object::Sphere(Sphere { center: read_vec3()?, radius: parts[3], + material: material_color.unwrap(), })), _ => bail!("Unknown keyword {keyword}"), } diff --git a/assignment-1/src/main.rs b/assignment-1/src/main.rs index a41aa25..32fc938 100644 --- a/assignment-1/src/main.rs +++ b/assignment-1/src/main.rs @@ -1,21 +1,30 @@ #[macro_use] extern crate anyhow; +mod image; mod input_file; mod ray; mod scene_data; mod vec3; mod view; +use std::fs::File; use std::path::PathBuf; use anyhow::Result; use clap::Parser; +use itertools::Itertools; +use ordered_float::NotNan; +use crate::image::{Image, Pixel}; use crate::input_file::parse_input_file; +use crate::ray::Ray; +use crate::scene_data::Object; use crate::vec3::Vec3; use crate::view::Rect; +const ARBITRARY_D: f64 = 2.0; + /// Simple raycaster. #[derive(Parser)] #[clap(author, version, about, long_about = None)] @@ -26,14 +35,20 @@ struct Opt { /// /// imsize [width] [height] /// - /// Where `imsize' is a keyword, and `width' and `height' are integer values - /// denoting the desired size of the image to be generated. + /// Where `imsize' is a keyword, and `width' and `height' are integer values denoting the desired + /// size of the image to be generated. #[clap()] input_path: PathBuf, + + #[clap()] + output_path: Option, } fn main() -> Result<()> { let opt = Opt::parse(); + let out_file = opt + .output_path + .unwrap_or_else(|| opt.input_path.with_extension("ppm")); let scene = parse_input_file(&opt.input_path)?; println!("Scene: {scene:?}"); @@ -42,20 +57,88 @@ fn main() -> Result<()> { let u = Vec3::cross(scene.view_dir, scene.up_dir).unit(); let v = Vec3::cross(u, scene.view_dir).unit(); + // 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 = scene.hfov / 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 * ARBITRARY_D + }; + let aspect_ratio = scene.image_width as f64 / scene.image_height as f64; + let viewing_height = viewing_width / aspect_ratio; + // Compute viewing window corners // TODO: See slide 101 - // Also need to reverse calculation for d based on hfov let n = scene.view_dir.unit(); - let d = 1.0; + #[rustfmt::skip] // Otherwise this line wraps over let view_window = Rect { - upper_left: scene.eye_pos + n * d, // + ... - upper_right: scene.eye_pos + n * d, - lower_left: scene.eye_pos + n * d, - lower_right: scene.eye_pos + n * d, + upper_left: scene.eye_pos + n * ARBITRARY_D - u * (viewing_width / 2.0) + v * (viewing_height / 2.0), + upper_right: scene.eye_pos + n * ARBITRARY_D + u * (viewing_width / 2.0) + v * (viewing_height / 2.0), + lower_left: scene.eye_pos + n * ARBITRARY_D - u * (viewing_width / 2.0) - v * (viewing_height / 2.0), + lower_right: scene.eye_pos + n * ARBITRARY_D + u * (viewing_width / 2.0) - v * (viewing_height / 2.0), + }; + + // Translate image pixels to real-world 3d coords + let pixel_translation = { + let dx = view_window.upper_right - view_window.upper_left; + let pixel_base_x = dx / scene.image_width as f64; + + let dy = view_window.lower_left - view_window.upper_left; + let pixel_base_y = dy / scene.image_height as f64; + + move |px: usize, py: usize| { + let x_component = view_window.upper_left + pixel_base_x * px as f64; + let y_component = view_window.upper_left + pixel_base_y * py as f64; + x_component + y_component + } }; // Loop through every single pixel of the output file - for (px, py) in (0..scene.image_width).zip(0..scene.image_height) {} + let mut pixels = + vec![Pixel::default(); scene.image_width * scene.image_width]; + for (px, py) in (0..scene.image_width).cartesian_product(0..scene.image_height) { + let pixel_in_space = pixel_translation(px, py); + let ray = Ray::from_endpoints(scene.eye_pos, pixel_in_space); + + let earliest_intersection = scene + .objects + .iter() + .filter_map(|object| { + let sphere = match object { + Object::Sphere(v) => v, + _ => return None, // TODO: Handle other object types for intersection as well + }; + + ray.intersects_at(sphere).map(|t| (t, sphere)) + }) + .min_by_key(|(t, _)| NotNan::new(*t).unwrap()); + + let pixel_color = match earliest_intersection { + Some((_, sphere)) => scene.material_colors[sphere.material], + // There was no intersection, so this should default to the background color + None => scene.bkg_color, + }; + + // println!("({px}, {py}): {intersection:?}\t{ray:?} {sphere:?}"); + + pixels[py * scene.image_height + px] = pixel_color.to_pixel(); + } + + let image = Image { + width: scene.image_width, + height: scene.image_height, + data: pixels, + }; + + { + let file = File::create(&out_file)?; + image.write(file)?; + } Ok(()) } diff --git a/assignment-1/src/ray.rs b/assignment-1/src/ray.rs index 0adfef6..64b1d65 100644 --- a/assignment-1/src/ray.rs +++ b/assignment-1/src/ray.rs @@ -5,52 +5,63 @@ use crate::vec3::Vec3; /// /// That means at any time t: f64, the point represented by origin + direction * time occurs on the /// ray. +#[derive(Debug)] pub struct Ray { origin: Vec3, direction: Vec3, } impl Ray { + /// Construct a ray from endpoints + pub fn from_endpoints(start: Vec3, end: Vec3) -> Self { + let delta = (end - start).unit(); + Ray { + origin: start, + direction: delta, + } + } + /// Evaluate the ray at a certain point in time, yielding a point pub fn eval(&self, time: f64) -> Vec3 { self.origin + self.direction * time } -} -/// Given a ray and a sphere, returns the first time at which this ray intersects the sphere. -/// -/// If there is no intersection point, returns None. -pub fn ray_intersection_time(ray: &Ray, sphere: &Sphere) -> Option { - let a = - ray.direction.x.powi(2) + ray.direction.y.powi(2) + ray.direction.z.powi(2); - let b = 2.0 - * (ray.direction.x * (ray.origin.x - sphere.center.x) - + ray.direction.y * (ray.origin.y - sphere.center.y) - + ray.direction.z * (ray.origin.z - sphere.center.z)); - let c = (ray.origin.x - sphere.center.x).powi(2) - + (ray.origin.y - sphere.center.y).powi(2) - + (ray.origin.z - sphere.center.z).powi(2) - - sphere.radius.powi(2); - let discriminant = b * b - 4.0 * a * c; + /// Given a sphere, returns the first time at which this ray intersects the sphere. + /// + /// If there is no intersection point, returns None. + pub fn intersects_at(&self, sphere: &Sphere) -> Option { + let a = self.direction.x.powi(2) + + self.direction.y.powi(2) + + self.direction.z.powi(2); + let b = 2.0 + * (self.direction.x * (self.origin.x - sphere.center.x) + + self.direction.y * (self.origin.y - sphere.center.y) + + self.direction.z * (self.origin.z - sphere.center.z)); + let c = (self.origin.x - sphere.center.x).powi(2) + + (self.origin.y - sphere.center.y).powi(2) + + (self.origin.z - sphere.center.z).powi(2) + - sphere.radius.powi(2); + let discriminant = b * b - 4.0 * a * c; - match discriminant { - // Discriminant < 0, means the equation has no solutions. - d if d < 0.0 => return None, + match discriminant { + // Discriminant < 0, means the equation has no solutions. + d if d < 0.0 => return None, - // Discriminant == 0 - d if d == 0.0 => { - return Some(-b / (2.0 * a)); + // Discriminant == 0 + d if d == 0.0 => { + return Some(-b / (2.0 * a)); + } + + d if d > 0.0 => { + let solution_1 = (-b + discriminant.sqrt()) / (2.0 * a); + let solution_2 = (-b - discriminant.sqrt()) / (2.0 * a); + + return Some(solution_1.min(solution_2)); + } + + // Probably hit some NaN or Infinity value due to faulty inputs... + _ => unreachable!("Invalid determinant value: {discriminant}"), } - - d if d > 0.0 => { - let solution_1 = (-b + discriminant.sqrt()) / (2.0 * a); - let solution_2 = (-b - discriminant.sqrt()) / (2.0 * a); - - return Some(solution_1.min(solution_2)); - } - - // Probably hit some NaN or Infinity value due to faulty inputs... - _ => unreachable!("Invalid determinant value: {discriminant}"), } } @@ -59,7 +70,7 @@ mod tests { use crate::scene_data::Sphere; use crate::vec3::Vec3; - use super::{ray_intersection_time, Ray}; + use super::Ray; #[test] fn practice_problem_slide_154() { @@ -72,7 +83,7 @@ mod tests { radius: 4.0, }; - let point = ray_intersection_time(&ray, &sphere).map(|t| ray.eval(t)); + let point = ray.intersects_at(&sphere).map(|t| ray.eval(t)); // the intersection point in this case is (0, 0, -6) assert_eq!(point, Some(Vec3::new(0.0, 0.0, -6.0))); @@ -90,6 +101,6 @@ mod tests { }; // oops! In this case, the ray does not intersect the sphere. - assert_eq!(ray_intersection_time(&ray, &sphere), None); + assert_eq!(ray.intersects_at(&sphere), None); } } diff --git a/assignment-1/src/scene_data.rs b/assignment-1/src/scene_data.rs index 547856e..3cc91e0 100644 --- a/assignment-1/src/scene_data.rs +++ b/assignment-1/src/scene_data.rs @@ -4,6 +4,9 @@ use crate::vec3::Vec3; pub struct Sphere { pub center: Vec3, pub radius: f64, + + /// Index into the scene's material color list + pub material: usize, } #[derive(Debug)] @@ -32,8 +35,6 @@ pub struct Scene { /// Background color pub bkg_color: Vec3, - /// Material color - pub mtl_color: Vec3, - + pub material_colors: Vec, pub objects: Vec, } diff --git a/assignment-1/src/vec3.rs b/assignment-1/src/vec3.rs index bceef2b..6663dad 100644 --- a/assignment-1/src/vec3.rs +++ b/assignment-1/src/vec3.rs @@ -1,7 +1,9 @@ -use std::ops::{Add, Mul}; +use std::ops::{Add, Div, Mul, Sub}; use num::Float; +use crate::image::Pixel; + #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] pub struct Vec3 { pub x: T, @@ -37,6 +39,16 @@ impl Vec3 { } } +impl Vec3 { + /// Convert into an RGB color + pub fn to_pixel(&self) -> Pixel { + let r = (self.x * 256.0) as u8; + let g = (self.y * 256.0) as u8; + let b = (self.z * 256.0) as u8; + Pixel(r, g, b) + } +} + /// Vector addition impl Add> for Vec3 where @@ -49,6 +61,18 @@ where } } +/// Vector subtraction +impl Sub> for Vec3 +where + T: Sub, +{ + type Output = Vec3; + + fn sub(self, rhs: Vec3) -> Self::Output { + Vec3::new(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z) + } +} + /// Scalar multiplication impl Mul for Vec3 where @@ -62,6 +86,19 @@ where } } +/// Scalar division +impl Div for Vec3 +where + T: Div, + U: Copy, +{ + type Output = Vec3; + + fn div(self, rhs: U) -> Self::Output { + Vec3::new(self.x / rhs, self.y / rhs, self.z / rhs) + } +} + #[cfg(test)] mod tests { use super::Vec3; diff --git a/assignment-1/src/view.rs b/assignment-1/src/view.rs index 4e4dd04..ea4ad38 100644 --- a/assignment-1/src/view.rs +++ b/assignment-1/src/view.rs @@ -8,6 +8,4 @@ pub struct Rect { pub lower_right: Vec3, } -pub fn compute_viewing_rect() { - -} +pub fn compute_viewing_rect() {}