Add parallel projection support

This commit is contained in:
Michael Zhang 2023-02-01 17:12:30 -06:00
parent 00000190c4
commit 00000200ae
10 changed files with 166 additions and 51 deletions

View file

@ -1,5 +1,6 @@
/target /target
/assignment-1 /assignment-1
/examples/*.png
*.ppm *.ppm
*.zip *.zip
*.pdf *.pdf

View file

@ -3,12 +3,17 @@
DOCKER := docker DOCKER := docker
ZIP := zip ZIP := zip
PANDOC := pandoc PANDOC := pandoc
CONVERT := convert
HANDIN := hw1a.michael.zhang.zip HANDIN := hw1a.michael.zhang.zip
BINARY := assignment-1 BINARY := ./assignment-1
WRITEUP := writeup.pdf WRITEUP := writeup.pdf
SOURCES := $(shell find -name "*.rs") 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) all: $(HANDIN)
$(BINARY): $(SOURCES) $(BINARY): $(SOURCES)
@ -22,8 +27,14 @@ $(BINARY): $(SOURCES)
cargo build --release cargo build --release
mv target/release/assignment-1 $@ mv target/release/assignment-1 $@
$(HANDIN): $(BINARY) $(WRITEUP) Cargo.toml Cargo.lock README.md $(HANDIN): $(BINARY) $(WRITEUP) Cargo.toml Cargo.lock README.md $(EXAMPLES_PNG) $(EXAMPLES_PPM)
$(ZIP) -r $@ src $^ $(ZIP) -r $@ src examples $^
examples/%.ppm: examples/%.txt
cargo run -- -o $@ $<
examples/%.png: examples/%.ppm
convert $< $@
writeup.pdf: writeup.md writeup.pdf: writeup.md
$(PANDOC) -o $@ $< $(PANDOC) -o $@ $<

View file

@ -1,11 +1,18 @@
# Raycaster # Raycaster
## Bundle contents
Writeup is located at `/writeup.pdf`. Writeup is located at `/writeup.pdf`.
The binary can be found at `/assignment-1`. Run `./assignment-1 --help` to see 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 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 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 ## Building from source

View file

@ -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

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use crate::{ use crate::{
image::Color, image::Color,
scene_data::{Object, Scene, Sphere}, scene_data::{Cylinder, Object, ObjectKind, Scene, Sphere},
vec3::Vec3, vec3::Vec3,
}; };
@ -35,6 +35,10 @@ pub fn parse_input_file(path: impl AsRef<Path>) -> Result<Scene> {
scene.image_width = width; scene.image_width = width;
scene.image_height = height; scene.image_height = height;
} }
} else if keyword == "projection" {
if let Some("parallel") = parts.next() {
scene.parallel_projection = true;
}
} }
// Do float parsing instead // Do float parsing instead
else { else {
@ -42,11 +46,11 @@ pub fn parse_input_file(path: impl AsRef<Path>) -> Result<Scene> {
.map(|s| s.parse::<f64>().map_err(|e| e.into())) .map(|s| s.parse::<f64>().map_err(|e| e.into()))
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
let read_vec3 = || { let read_vec3 = |start: usize| {
if parts.len() < 3 { if parts.len() < 3 {
bail!("Vec3 requires 3 components."); 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 = || { let read_color = || {
@ -57,9 +61,9 @@ pub fn parse_input_file(path: impl AsRef<Path>) -> Result<Scene> {
}; };
match keyword { match keyword {
"eye" => scene.eye_pos = read_vec3()?, "eye" => scene.eye_pos = read_vec3(0)?,
"viewdir" => scene.view_dir = read_vec3()?, "viewdir" => scene.view_dir = read_vec3(0)?,
"updir" => scene.up_dir = read_vec3()?, "updir" => scene.up_dir = read_vec3(0)?,
"hfov" => scene.hfov = parts[0], "hfov" => scene.hfov = parts[0],
"bkgcolor" => scene.bkg_color = read_color()?, "bkgcolor" => scene.bkg_color = read_color()?,
@ -70,11 +74,30 @@ pub fn parse_input_file(path: impl AsRef<Path>) -> Result<Scene> {
scene.material_colors.push(read_color()?); scene.material_colors.push(read_color()?);
} }
"sphere" => scene.objects.push(Object::Sphere(Sphere { "sphere" => scene.objects.push(Object {
center: read_vec3()?, kind: ObjectKind::Sphere(Sphere {
center: read_vec3(0)?,
radius: parts[3], radius: parts[3],
material: material_color.unwrap(), }),
})), 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}"), _ => bail!("Unknown keyword {keyword}"),
} }
} }

View file

@ -14,14 +14,11 @@ use anyhow::Result;
use clap::Parser; use clap::Parser;
use ordered_float::NotNan; use ordered_float::NotNan;
use rayon::prelude::{IntoParallelIterator, ParallelIterator}; use rayon::prelude::{IntoParallelIterator, ParallelIterator};
use scene_data::ObjectKind;
use crate::image::Image; use crate::image::Image;
use crate::input_file::parse_input_file; use crate::input_file::parse_input_file;
use crate::ray::Ray; use crate::ray::Ray;
use crate::scene_data::Object;
/// Viewing distance
const ARBITRARY_D: f64 = 1.0;
/// Simple raycaster. /// Simple raycaster.
#[derive(Parser)] #[derive(Parser)]
@ -35,6 +32,14 @@ struct Opt {
/// with an extension of .ppm) /// with an extension of .ppm)
#[clap(short = 'o', long = "output")] #[clap(short = 'o', long = "output")]
output_path: Option<PathBuf>, output_path: Option<PathBuf>,
/// 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<()> { fn main() -> Result<()> {
@ -43,10 +48,15 @@ fn main() -> Result<()> {
.output_path .output_path
.unwrap_or_else(|| opt.input_path.with_extension("ppm")); .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 // 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 // Translate image pixels to real-world 3d coords
let translate_pixel = { let translate_pixel = {
@ -79,34 +89,50 @@ fn main() -> Result<()> {
let pixels = pixels_iter let pixels = pixels_iter
.map(|(px, py)| { .map(|(px, py)| {
let pixel_in_space = translate_pixel(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 let earliest_intersection = scene
.objects .objects
.iter() .iter()
.filter_map(|object| { .filter_map(|object| {
let sphere = match object { use ObjectKind::*;
Object::Sphere(v) => v, let intersection_point_opt = match &object.kind {
_ => return None, /* TODO: Handle other object types for Sphere(sphere) => ray.intersects_sphere_at(&sphere),
* intersection as well */ Cylinder(cylinder) => ray.intersects_cylinder_at(&cylinder),
}; };
// Return both the t and the sphere, because we want to sort on the t intersection_point_opt.and_then(|t| {
// but later retrieve attributes from the sphere
ray.intersects_at(sphere).and_then(|t| {
// Unfortunately, IEEE floats in Rust don't have total ordering, // Unfortunately, IEEE floats in Rust don't have total ordering,
// because NaNs violate ordering properties. The way to remedy this // because NaNs violate ordering properties. The way to remedy this
// is to ensure we don't have NaNs by wrapping it into this type, // is to ensure we don't have NaNs by wrapping it into this type,
// which then implements total ordering // which then implements total ordering
let t = NotNan::new(t); 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. // Sort the list of intersection times by the lowest one.
.min_by_key(|(t, _)| *t); .min_by_key(|(t, _)| *t);
let pixel_color = match earliest_intersection { 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 // There was no intersection, so this should default to the scene's
// background color // background color
None => scene.bkg_color, None => scene.bkg_color,

View file

@ -1,4 +1,4 @@
use crate::scene_data::Sphere; use crate::scene_data::{Cylinder, Sphere};
use crate::vec3::Vec3; use crate::vec3::Vec3;
/// A normalized parametric Ray of the form (origin + direction * time) /// A normalized parametric Ray of the form (origin + direction * time)
@ -30,7 +30,7 @@ impl Ray {
/// sphere. /// sphere.
/// ///
/// If there is no intersection point, returns None. /// If there is no intersection point, returns None.
pub fn intersects_at(&self, sphere: &Sphere) -> Option<f64> { pub fn intersects_sphere_at(&self, sphere: &Sphere) -> Option<f64> {
let a = self.direction.x.powi(2) let a = self.direction.x.powi(2)
+ self.direction.y.powi(2) + self.direction.y.powi(2)
+ self.direction.z.powi(2); + self.direction.z.powi(2);
@ -64,6 +64,15 @@ impl Ray {
_ => unreachable!("Invalid determinant value: {discriminant}"), _ => 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<f64> {
// TODO: Implement
None
}
} }
#[cfg(test)] #[cfg(test)]
@ -82,10 +91,9 @@ mod tests {
let sphere = Sphere { let sphere = Sphere {
center: Vec3::new(0.0, 0.0, -10.0), center: Vec3::new(0.0, 0.0, -10.0),
radius: 4.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) // the intersection point in this case is (0, 0, -6)
assert_eq!(point, Some(Vec3::new(0.0, 0.0, -6.0))); assert_eq!(point, Some(Vec3::new(0.0, 0.0, -6.0)));
@ -100,10 +108,9 @@ mod tests {
let sphere = Sphere { let sphere = Sphere {
center: Vec3::new(0.0, 0.0, -10.0), center: Vec3::new(0.0, 0.0, -10.0),
radius: 4.0, radius: 4.0,
material: 0,
}; };
// oops! In this case, the ray does not intersect the sphere. // 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);
} }
} }

View file

@ -1,23 +1,31 @@
use crate::{image::Color, vec3::Vec3, ARBITRARY_D}; use crate::{image::Color, vec3::Vec3};
#[derive(Debug)] #[derive(Debug)]
pub struct Sphere { pub struct Sphere {
pub center: Vec3, pub center: Vec3,
pub radius: f64, 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 /// Index into the scene's material color list
pub material: usize, pub material: usize,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum Object { pub enum ObjectKind {
Sphere(Sphere), Sphere(Sphere),
Cylinder { Cylinder(Cylinder),
center: Vec3,
direction: Vec3,
radius: f64,
length: f64,
},
} }
#[derive(Debug)] #[derive(Debug)]
@ -36,6 +44,7 @@ pub struct Scene {
/// Horizontal field of view (in degrees) /// Horizontal field of view (in degrees)
pub hfov: f64, pub hfov: f64,
pub parallel_projection: bool,
pub image_width: usize, pub image_width: usize,
pub image_height: usize, pub image_height: usize,
@ -49,7 +58,7 @@ pub struct Scene {
impl Scene { impl Scene {
/// Determine the boundaries of the viewing window in world coordinates /// 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 // Compute viewing directions
let u = Vec3::cross(self.view_dir, self.up_dir).unit(); let u = Vec3::cross(self.view_dir, self.up_dir).unit();
let v = Vec3::cross(u, self.view_dir).unit(); let v = Vec3::cross(u, self.view_dir).unit();
@ -64,7 +73,7 @@ impl Scene {
let w_over_2d = half_hfov.tan(); let w_over_2d = half_hfov.tan();
// To find the viewing width we must multiply by 2d now // 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; 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(); let n = self.view_dir.unit();
#[rustfmt::skip] // Otherwise this line wraps over #[rustfmt::skip] // Otherwise this line wraps over
let view_window = Rect { let view_window = Rect {
upper_left: 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 * ARBITRARY_D + 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 * ARBITRARY_D - 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 * ARBITRARY_D + 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 view_window

View file

@ -71,3 +71,12 @@ location for any pixel.
(Technically really we would want the middle of the pixel, so just add (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) $\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$.

View file

@ -20,6 +20,7 @@
zip zip
unzip unzip
texlive.combined.scheme-full texlive.combined.scheme-full
imagemagick
]) ++ (with toolchain; [ ]) ++ (with toolchain; [
cargo cargo
rustc rustc