Add parallel projection support

This commit is contained in:
Michael Zhang 2023-02-01 17:12:30 -06:00
parent b4feb95743
commit 7885b58947
10 changed files with 166 additions and 51 deletions

View file

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

View file

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

View file

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

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::{
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<Path>) -> Result<Scene> {
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<Path>) -> Result<Scene> {
.map(|s| s.parse::<f64>().map_err(|e| e.into()))
.collect::<Result<Vec<_>>>()?;
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<Path>) -> Result<Scene> {
};
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<Path>) -> Result<Scene> {
scene.material_colors.push(read_color()?);
}
"sphere" => scene.objects.push(Object::Sphere(Sphere {
center: read_vec3()?,
"sphere" => scene.objects.push(Object {
kind: ObjectKind::Sphere(Sphere {
center: read_vec3(0)?,
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}"),
}
}

View file

@ -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<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<()> {
@ -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,

View file

@ -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<f64> {
pub fn intersects_sphere_at(&self, sphere: &Sphere) -> Option<f64> {
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<f64> {
// 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);
}
}

View file

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

View file

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

View file

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