Add parallel projection support
This commit is contained in:
parent
b4feb95743
commit
7885b58947
10 changed files with 166 additions and 51 deletions
1
assignment-1/.gitignore
vendored
1
assignment-1/.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
/target
|
||||
/assignment-1
|
||||
/examples/*.png
|
||||
*.ppm
|
||||
*.zip
|
||||
*.pdf
|
||||
|
|
|
@ -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 $@ $<
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
21
assignment-1/examples/objects.txt
Normal file
21
assignment-1/examples/objects.txt
Normal 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
|
|
@ -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()?,
|
||||
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}"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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$.
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
zip
|
||||
unzip
|
||||
texlive.combined.scheme-full
|
||||
imagemagick
|
||||
]) ++ (with toolchain; [
|
||||
cargo
|
||||
rustc
|
||||
|
|
Loading…
Reference in a new issue