From 7885b58947999bce54bff7ca24299c34c4d18fa7 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Wed, 1 Feb 2023 17:12:30 -0600 Subject: [PATCH] Add parallel projection support --- assignment-1/.gitignore | 1 + assignment-1/Makefile | 17 +++++++-- assignment-1/README.md | 9 ++++- assignment-1/examples/objects.txt | 21 +++++++++++ assignment-1/src/input_file.rs | 45 ++++++++++++++++++------ assignment-1/src/main.rs | 58 ++++++++++++++++++++++--------- assignment-1/src/ray.rs | 19 ++++++---- assignment-1/src/scene_data.rs | 37 ++++++++++++-------- assignment-1/writeup.md | 9 +++++ flake.nix | 1 + 10 files changed, 166 insertions(+), 51 deletions(-) create mode 100644 assignment-1/examples/objects.txt diff --git a/assignment-1/.gitignore b/assignment-1/.gitignore index 58e5cde..44d187f 100644 --- a/assignment-1/.gitignore +++ b/assignment-1/.gitignore @@ -1,5 +1,6 @@ /target /assignment-1 +/examples/*.png *.ppm *.zip *.pdf diff --git a/assignment-1/Makefile b/assignment-1/Makefile index c13743c..deffaa9 100644 --- a/assignment-1/Makefile +++ b/assignment-1/Makefile @@ -3,12 +3,17 @@ DOCKER := docker ZIP := zip PANDOC := pandoc +CONVERT := convert HANDIN := hw1a.michael.zhang.zip -BINARY := assignment-1 +BINARY := ./assignment-1 WRITEUP := writeup.pdf SOURCES := $(shell find -name "*.rs") +EXAMPLES := $(shell find examples -name "*.txt") +EXAMPLES_PPM := $(patsubst %.txt,%.ppm,$(EXAMPLES)) +EXAMPLES_PNG := $(patsubst %.txt,%.png,$(EXAMPLES)) + all: $(HANDIN) $(BINARY): $(SOURCES) @@ -22,8 +27,14 @@ $(BINARY): $(SOURCES) cargo build --release mv target/release/assignment-1 $@ -$(HANDIN): $(BINARY) $(WRITEUP) Cargo.toml Cargo.lock README.md - $(ZIP) -r $@ src $^ +$(HANDIN): $(BINARY) $(WRITEUP) Cargo.toml Cargo.lock README.md $(EXAMPLES_PNG) $(EXAMPLES_PPM) + $(ZIP) -r $@ src examples $^ + +examples/%.ppm: examples/%.txt + cargo run -- -o $@ $< + +examples/%.png: examples/%.ppm + convert $< $@ writeup.pdf: writeup.md $(PANDOC) -o $@ $< diff --git a/assignment-1/README.md b/assignment-1/README.md index 096d536..9d6e0ac 100644 --- a/assignment-1/README.md +++ b/assignment-1/README.md @@ -1,11 +1,18 @@ # Raycaster +## Bundle contents + Writeup is located at `/writeup.pdf`. The binary can be found at `/assignment-1`. Run `./assignment-1 --help` to see how to use it. The binary has been built using the Rust Docker image, which should have an environment similar to CSELabs. If there is trouble running the -binary, try building form source: +binary, try building from source, as documented below. + +Examples are found in the `examples` directory. The text files are the input +sources, and the ppm files are the corresponding outputs. They have been +generated by running this program. For convenience, pngs have also been provided +using imagemagick. ## Building from source diff --git a/assignment-1/examples/objects.txt b/assignment-1/examples/objects.txt new file mode 100644 index 000000000..38fa760 --- /dev/null +++ b/assignment-1/examples/objects.txt @@ -0,0 +1,21 @@ +imsize 640 480 +eye 0 0 3 +viewdir 0 0 -1 +hfov 120 +updir 0 1 0 +bkgcolor 0.1 0.1 0.1 + +mtlcolor 0 0.5 0.5 +sphere -1 -2 -5 2 +sphere 3 -5 -1 0.5 + +mtlcolor 0.5 0.5 1 +sphere 1 2 -3 3 +sphere -6 3 -4 1 + +mtlcolor 0.5 0 0.5 +sphere 5 5 -1 1 +sphere -6 -4 -8 7 + +mtlcolor 0.5 1 0.5 +cylinder 2 1 -2 1 -2 1 1 2 diff --git a/assignment-1/src/input_file.rs b/assignment-1/src/input_file.rs index 772898c..7a85504 100644 --- a/assignment-1/src/input_file.rs +++ b/assignment-1/src/input_file.rs @@ -4,7 +4,7 @@ use anyhow::Result; use crate::{ image::Color, - scene_data::{Object, Scene, Sphere}, + scene_data::{Cylinder, Object, ObjectKind, Scene, Sphere}, vec3::Vec3, }; @@ -35,6 +35,10 @@ pub fn parse_input_file(path: impl AsRef) -> Result { scene.image_width = width; scene.image_height = height; } + } else if keyword == "projection" { + if let Some("parallel") = parts.next() { + scene.parallel_projection = true; + } } // Do float parsing instead else { @@ -42,11 +46,11 @@ pub fn parse_input_file(path: impl AsRef) -> Result { .map(|s| s.parse::().map_err(|e| e.into())) .collect::>>()?; - let read_vec3 = || { + let read_vec3 = |start: usize| { if parts.len() < 3 { bail!("Vec3 requires 3 components."); } - Ok(Vec3::new(parts[0], parts[1], parts[2])) + Ok(Vec3::new(parts[start], parts[start + 1], parts[start + 2])) }; let read_color = || { @@ -57,9 +61,9 @@ pub fn parse_input_file(path: impl AsRef) -> Result { }; match keyword { - "eye" => scene.eye_pos = read_vec3()?, - "viewdir" => scene.view_dir = read_vec3()?, - "updir" => scene.up_dir = read_vec3()?, + "eye" => scene.eye_pos = read_vec3(0)?, + "viewdir" => scene.view_dir = read_vec3(0)?, + "updir" => scene.up_dir = read_vec3(0)?, "hfov" => scene.hfov = parts[0], "bkgcolor" => scene.bkg_color = read_color()?, @@ -70,11 +74,30 @@ pub fn parse_input_file(path: impl AsRef) -> Result { scene.material_colors.push(read_color()?); } - "sphere" => scene.objects.push(Object::Sphere(Sphere { - center: read_vec3()?, - radius: parts[3], - material: material_color.unwrap(), - })), + "sphere" => scene.objects.push(Object { + kind: ObjectKind::Sphere(Sphere { + center: read_vec3(0)?, + radius: parts[3], + }), + material: match material_color { + Some(v) => v, + None => bail!("Each sphere must be preceded by a `mtlcolor` line"), + }, + }), + + "cylinder" => scene.objects.push(Object { + kind: ObjectKind::Cylinder(Cylinder { + center: read_vec3(0)?, + direction: read_vec3(3)?, + radius: parts[6], + length: parts[7], + }), + material: match material_color { + Some(v) => v, + None => bail!("Each sphere must be preceded by a `mtlcolor` line"), + }, + }), + _ => bail!("Unknown keyword {keyword}"), } } diff --git a/assignment-1/src/main.rs b/assignment-1/src/main.rs index b1b536e..264f2b1 100644 --- a/assignment-1/src/main.rs +++ b/assignment-1/src/main.rs @@ -14,14 +14,11 @@ use anyhow::Result; use clap::Parser; use ordered_float::NotNan; use rayon::prelude::{IntoParallelIterator, ParallelIterator}; +use scene_data::ObjectKind; use crate::image::Image; use crate::input_file::parse_input_file; use crate::ray::Ray; -use crate::scene_data::Object; - -/// Viewing distance -const ARBITRARY_D: f64 = 1.0; /// Simple raycaster. #[derive(Parser)] @@ -35,6 +32,14 @@ struct Opt { /// with an extension of .ppm) #[clap(short = 'o', long = "output")] output_path: Option, + + /// Force parallel projection to be used + #[clap(long = "parallel")] + force_parallel: bool, + + /// Override distance from eye + #[clap(long = "distance", default_value = "1.0")] + distance: f64, } fn main() -> Result<()> { @@ -43,10 +48,15 @@ fn main() -> Result<()> { .output_path .unwrap_or_else(|| opt.input_path.with_extension("ppm")); - let scene = parse_input_file(&opt.input_path)?; + let mut scene = parse_input_file(&opt.input_path)?; + let distance = opt.distance; + + if opt.force_parallel { + scene.parallel_projection = true; + } // Compute the viewing window - let view_window = scene.compute_viewing_window(); + let view_window = scene.compute_viewing_window(distance); // Translate image pixels to real-world 3d coords let translate_pixel = { @@ -79,34 +89,50 @@ fn main() -> Result<()> { let pixels = pixels_iter .map(|(px, py)| { let pixel_in_space = translate_pixel(px, py); - let ray = Ray::from_endpoints(scene.eye_pos, pixel_in_space); + + let ray_start = if scene.parallel_projection { + // For a parallel projection, we'll just take the view direction and + // subtract it from the target point. This means every single + // ray will be viewed from a point at infinity, rather than a single eye + // position. + let n = scene.view_dir.unit(); + let view_dir = n * distance; + pixel_in_space - view_dir + } else { + scene.eye_pos + }; + + let ray = Ray::from_endpoints(ray_start, 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 */ + use ObjectKind::*; + let intersection_point_opt = match &object.kind { + Sphere(sphere) => ray.intersects_sphere_at(&sphere), + Cylinder(cylinder) => ray.intersects_cylinder_at(&cylinder), }; - // Return both the t and the sphere, because we want to sort on the t - // but later retrieve attributes from the sphere - ray.intersects_at(sphere).and_then(|t| { + intersection_point_opt.and_then(|t| { // 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 let t = NotNan::new(t); - t.ok().map(|t| (t, sphere)) + + // Return both the t and the sphere, because we want to sort on the + // t but later retrieve attributes from the sphere + t.ok().map(|t| (t, object)) }) }) // Sort the list of intersection times by the lowest one. .min_by_key(|(t, _)| *t); let pixel_color = match earliest_intersection { - Some((_, sphere)) => scene.material_colors[sphere.material], + // Take the object's material color + Some((_, object)) => scene.material_colors[object.material], + // There was no intersection, so this should default to the scene's // background color None => scene.bkg_color, diff --git a/assignment-1/src/ray.rs b/assignment-1/src/ray.rs index fb7023a..4cffd94 100644 --- a/assignment-1/src/ray.rs +++ b/assignment-1/src/ray.rs @@ -1,4 +1,4 @@ -use crate::scene_data::Sphere; +use crate::scene_data::{Cylinder, Sphere}; use crate::vec3::Vec3; /// A normalized parametric Ray of the form (origin + direction * time) @@ -30,7 +30,7 @@ impl Ray { /// sphere. /// /// If there is no intersection point, returns None. - pub fn intersects_at(&self, sphere: &Sphere) -> Option { + pub fn intersects_sphere_at(&self, sphere: &Sphere) -> Option { let a = self.direction.x.powi(2) + self.direction.y.powi(2) + self.direction.z.powi(2); @@ -64,6 +64,15 @@ impl Ray { _ => unreachable!("Invalid determinant value: {discriminant}"), } } + + /// Given a cylinder, returns the first time at which this ray intersects the + /// cylinder. + /// + /// If there is no intersection point, returns None. + pub fn intersects_cylinder_at(&self, cylinder: &Cylinder) -> Option { + // TODO: Implement + None + } } #[cfg(test)] @@ -82,10 +91,9 @@ mod tests { let sphere = Sphere { center: Vec3::new(0.0, 0.0, -10.0), radius: 4.0, - material: 0, }; - let point = ray.intersects_at(&sphere).map(|t| ray.eval(t)); + let point = ray.intersects_sphere_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))); @@ -100,10 +108,9 @@ mod tests { let sphere = Sphere { center: Vec3::new(0.0, 0.0, -10.0), radius: 4.0, - material: 0, }; // oops! In this case, the ray does not intersect the sphere. - assert_eq!(ray.intersects_at(&sphere), None); + assert_eq!(ray.intersects_sphere_at(&sphere), None); } } diff --git a/assignment-1/src/scene_data.rs b/assignment-1/src/scene_data.rs index 891765b..5e092f2 100644 --- a/assignment-1/src/scene_data.rs +++ b/assignment-1/src/scene_data.rs @@ -1,23 +1,31 @@ -use crate::{image::Color, vec3::Vec3, ARBITRARY_D}; +use crate::{image::Color, vec3::Vec3}; #[derive(Debug)] pub struct Sphere { pub center: Vec3, pub radius: f64, +} + +#[derive(Debug)] +pub struct Cylinder { + pub center: Vec3, + pub direction: Vec3, + pub radius: f64, + pub length: f64, +} + +#[derive(Debug)] +pub struct Object { + pub kind: ObjectKind, /// Index into the scene's material color list pub material: usize, } #[derive(Debug)] -pub enum Object { +pub enum ObjectKind { Sphere(Sphere), - Cylinder { - center: Vec3, - direction: Vec3, - radius: f64, - length: f64, - }, + Cylinder(Cylinder), } #[derive(Debug)] @@ -36,6 +44,7 @@ pub struct Scene { /// Horizontal field of view (in degrees) pub hfov: f64, + pub parallel_projection: bool, pub image_width: usize, pub image_height: usize, @@ -49,7 +58,7 @@ pub struct Scene { impl Scene { /// Determine the boundaries of the viewing window in world coordinates - pub fn compute_viewing_window(&self) -> Rect { + pub fn compute_viewing_window(&self, distance: f64) -> Rect { // Compute viewing directions let u = Vec3::cross(self.view_dir, self.up_dir).unit(); let v = Vec3::cross(u, self.view_dir).unit(); @@ -64,7 +73,7 @@ impl Scene { 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 + w_over_2d * 2.0 * distance }; let aspect_ratio = self.image_width as f64 / self.image_height as f64; @@ -74,10 +83,10 @@ impl Scene { let n = self.view_dir.unit(); #[rustfmt::skip] // Otherwise this line wraps over let view_window = Rect { - upper_left: self.eye_pos + n * ARBITRARY_D - u * (viewing_width / 2.0) + v * (viewing_height / 2.0), - upper_right: self.eye_pos + n * ARBITRARY_D + u * (viewing_width / 2.0) + v * (viewing_height / 2.0), - lower_left: self.eye_pos + n * ARBITRARY_D - u * (viewing_width / 2.0) - v * (viewing_height / 2.0), - lower_right: self.eye_pos + n * ARBITRARY_D + u * (viewing_width / 2.0) - v * (viewing_height / 2.0), + 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 diff --git a/assignment-1/writeup.md b/assignment-1/writeup.md index d11db0b..5611976 100644 --- a/assignment-1/writeup.md +++ b/assignment-1/writeup.md @@ -71,3 +71,12 @@ location for any pixel. (Technically really we would want the middle of the pixel, so just add $\frac{\Delta x + \Delta y}{2}$ to the point to get that) + +### Cylinder Intersection Notes + +First, we will transform the current point into the vector space of the +cylinder, so that the cylinder location is $(0, 0, 0)$ and the direction vector +is normalized into $(0, 0, 1)$. + +Then it's a matter of determining if the $x$ and $y$ coordinates fall into the +space constrained by the equation $x^2 + y^2 = r^2$ and if $z \le L$. diff --git a/flake.nix b/flake.nix index 4ab03ee..ff4f3e2 100644 --- a/flake.nix +++ b/flake.nix @@ -20,6 +20,7 @@ zip unzip texlive.combined.scheme-full + imagemagick ]) ++ (with toolchain; [ cargo rustc