From 43b85ae5d86053f69f4d226212912c810f435b14 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Wed, 15 Feb 2023 13:09:56 -0600 Subject: [PATCH] implement shadows --- assignment-1b/Cargo.lock | 39 +++++++ assignment-1b/Cargo.toml | 1 + assignment-1b/examples/objects.txt | 2 +- assignment-1b/src/lib.rs | 2 + assignment-1b/src/main.rs | 12 +- assignment-1b/src/scene/cylinder.rs | 4 +- assignment-1b/src/scene/data.rs | 47 +++++++- assignment-1b/src/scene/illumination.rs | 144 ++++++++++++++++++------ assignment-1b/src/scene/input_file.rs | 6 +- assignment-1b/src/scene/mod.rs | 2 +- assignment-1b/src/scene/sphere.rs | 4 +- flake.nix | 11 +- 12 files changed, 213 insertions(+), 61 deletions(-) diff --git a/assignment-1b/Cargo.lock b/assignment-1b/Cargo.lock index e61aaab..4d21e1b 100644 --- a/assignment-1b/Cargo.lock +++ b/assignment-1b/Cargo.lock @@ -28,6 +28,7 @@ dependencies = [ "num", "ordered-float", "rayon", + "tracing", ] [[package]] @@ -388,6 +389,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -532,6 +539,38 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + [[package]] name = "typenum" version = "1.16.0" diff --git a/assignment-1b/Cargo.toml b/assignment-1b/Cargo.toml index 78b50df..6adca8d 100644 --- a/assignment-1b/Cargo.toml +++ b/assignment-1b/Cargo.toml @@ -16,3 +16,4 @@ nalgebra = "0.32.1" num = { version = "0.4.0", features = ["serde"] } ordered-float = "3.4.0" rayon = "1.6.1" +tracing = "0.1.37" diff --git a/assignment-1b/examples/objects.txt b/assignment-1b/examples/objects.txt index f7ba514..cabaccf 100644 --- a/assignment-1b/examples/objects.txt +++ b/assignment-1b/examples/objects.txt @@ -5,7 +5,7 @@ hfov 60 updir 0 1 0 bkgcolor 0.1 0.1 0.1 -depthcueing 0.1 0.1 0.1 1 0.2 100 5 +depthcueing 0.1 0.1 0.1 1 0.2 100 0 light -10 10 -3 0 0.8 0.8 0.8 light -10 10 -3 1 0.8 0.8 0.8 diff --git a/assignment-1b/src/lib.rs b/assignment-1b/src/lib.rs index a9d997a..8fe737c 100644 --- a/assignment-1b/src/lib.rs +++ b/assignment-1b/src/lib.rs @@ -4,6 +4,8 @@ extern crate anyhow; #[macro_use] extern crate derivative; +#[macro_use] +extern crate tracing; pub mod image; pub mod ray; diff --git a/assignment-1b/src/main.rs b/assignment-1b/src/main.rs index ff8c9b9..bd2a51b 100644 --- a/assignment-1b/src/main.rs +++ b/assignment-1b/src/main.rs @@ -98,12 +98,13 @@ fn main() -> Result<()> { let intersections = scene .objects .iter() - .filter_map(|object| { + .enumerate() + .filter_map(|(i, object)| { match object.kind.intersects_ray_at(&ray) { Ok(Some(t)) => { // Return both the t and the sphere, because we want to sort on // the t but later retrieve attributes from the sphere - Some(Ok((t, object))) + Some(Ok((i, t, object))) } Ok(None) => None, Err(e) => Some(Err(e)), @@ -113,13 +114,12 @@ fn main() -> Result<()> { // Sort the list of intersection times by the lowest one. let earliest_intersection = - intersections.into_iter().min_by_key(|(t, _)| t.time); + intersections.into_iter().min_by_key(|(_, t, _)| t.time); Ok(match earliest_intersection { // Take the object's material color - Some((intersection_context, object)) => { - scene.compute_pixel_color(object.material, intersection_context) - } + Some((obj_idx, intersection_context, object)) => scene + .compute_pixel_color(obj_idx, object.material, intersection_context), // There was no intersection, so this should default to the scene's // background color diff --git a/assignment-1b/src/scene/cylinder.rs b/assignment-1b/src/scene/cylinder.rs index 632fc30..5e1b6ea 100644 --- a/assignment-1b/src/scene/cylinder.rs +++ b/assignment-1b/src/scene/cylinder.rs @@ -15,12 +15,12 @@ pub struct Cylinder { pub length: f64, } -impl ObjectKind for Cylinder { +impl Cylinder { /// Given a cylinder, returns the first time at which this ray intersects the /// cylinder. /// /// If there is no intersection point, returns None. - fn intersects_ray_at( + pub fn intersects_ray_at( &self, ray: &Ray, ) -> Result> { diff --git a/assignment-1b/src/scene/data.rs b/assignment-1b/src/scene/data.rs index 36c4b02..66fd149 100644 --- a/assignment-1b/src/scene/data.rs +++ b/assignment-1b/src/scene/data.rs @@ -6,23 +6,38 @@ use nalgebra::Vector3; use crate::image::Color; use crate::ray::Ray; +use super::cylinder::Cylinder; use super::illumination::IntersectionContext; +use super::sphere::Sphere; use super::Scene; -pub trait ObjectKind: Debug + Send + Sync { +#[derive(Debug)] +pub enum ObjectKind { + Sphere(Sphere), + Cylinder(Cylinder), +} + +impl ObjectKind { /// Determine where the ray intersects this object, returning the earliest /// time this happens. Returns None if no intersection occurs. /// /// Also known as Trace_Ray in the slides, except not the part where it calls /// Shade_Ray. - fn intersects_ray_at(&self, ray: &Ray) - -> Result>; + pub fn intersects_ray_at( + &self, + ray: &Ray, + ) -> Result> { + match self { + ObjectKind::Sphere(sphere) => sphere.intersects_ray_at(ray), + ObjectKind::Cylinder(cylinder) => cylinder.intersects_ray_at(ray), + } + } } /// An object in the scene #[derive(Debug)] pub struct Object { - pub kind: Box, + pub kind: ObjectKind, /// Index into the scene's material color list pub material: usize, @@ -66,12 +81,36 @@ pub struct Light { pub color: Vector3, } +impl Light { + /// Get the unit directional vector pointing from the given point to this + /// light source + pub fn direction_from(&self, point: Vector3) -> Vector3 { + match self.kind { + LightKind::Point { location } => location - point, + LightKind::Directional { direction } => -direction, + } + .normalize() + } +} + #[derive(Debug)] pub struct DepthCueing { + /// The color to tint (should be the same as the background color, to avoid + /// bizarre visual effects) pub color: Color, + + /// Proportion of the color influenced by the depth tint when the distance is + /// maxed (caps at 1.0) pub a_max: f64, + + /// Proportion of the color influenced by the depth tint when the distance is + /// at the minimum (caps at 1.0) pub a_min: f64, + + /// The max distance that should be affected by the depth tint pub dist_max: f64, + + /// The min distance that should be affected by the depth tint pub dist_min: f64, } diff --git a/assignment-1b/src/scene/illumination.rs b/assignment-1b/src/scene/illumination.rs index f129ec5..c2f5934 100644 --- a/assignment-1b/src/scene/illumination.rs +++ b/assignment-1b/src/scene/illumination.rs @@ -1,36 +1,16 @@ +use anyhow::Result; use nalgebra::Vector3; use ordered_float::NotNan; - -use crate::image::Color; - -use super::{ - data::{DepthCueing, LightKind}, - Scene, +use rayon::prelude::{ + IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator, }; -/// Information about an intersection -#[derive(Derivative)] -#[derivative(Debug, PartialEq, PartialOrd, Ord)] -pub struct IntersectionContext { - /// The time of the intersection in the parametric ray - /// - /// 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. - pub time: NotNan, +use crate::{image::Color, ray::Ray}; - /// The intersection point. - #[derivative(PartialEq = "ignore", Ord = "ignore")] - pub point: Vector3, - - /// The normal vector protruding from the surface of the object at the - /// intersection point - #[derivative(PartialEq = "ignore", Ord = "ignore")] - pub normal: Vector3, -} - -impl Eq for IntersectionContext {} +use super::{ + data::{DepthCueing, Light, LightKind}, + Scene, +}; impl Scene { /// Determine the color that should be used to fill this pixel. @@ -42,6 +22,7 @@ impl Scene { /// Also known as Shade_Ray in the slides. pub fn compute_pixel_color( &self, + obj_idx: usize, material_idx: usize, intersection_context: IntersectionContext, ) -> Color { @@ -57,16 +38,10 @@ impl Scene { // Diffuse and specular lighting for each separate light let diffuse_and_specular: Vector3 = self .lights - .iter() + .par_iter() .map(|light| { // The vector pointing in the direction of the light - let light_direction = match light.kind { - LightKind::Point { location } => { - location - intersection_context.point - } - LightKind::Directional { direction } => -direction, - } - .normalize(); + let light_direction = light.direction_from(intersection_context.point); let normal = intersection_context.normal.normalize(); let viewer_direction = self.eye_pos - intersection_context.point; @@ -84,8 +59,14 @@ impl Scene { .max(0.0) .powf(material.exponent); + let shadow_flag = + match self.in_shadow_of(obj_idx, intersection_context.point, light) { + true => 0.0, + false => 1.0, + }; + let diffuse_and_specular = diffuse_component + specular_component; - light.color.component_mul(&diffuse_and_specular) + shadow_flag * light.color.component_mul(&diffuse_and_specular) }) .sum(); @@ -119,4 +100,93 @@ impl Scene { clamped_result } + + /// Perform another ray casting to see if there are any objects obstructing + /// the light source to this particular point + pub fn in_shadow_of( + &self, + obj_idx: usize, + point: Vector3, + light: &Light, + ) -> bool { + let light_direction = light.direction_from(point); + let ray = Ray { + origin: point, + direction: light_direction.normalize(), + }; + + // Small helper for iterating over all of the objects in the scene except + // for the current one + let other_objects = self + .objects + .par_iter() + .enumerate() + .filter(|(i, _)| *i != obj_idx); + + // Get the list of intersections with all the other objects in the scene + let intersections = other_objects + .filter_map(|(_, object)| { + let intersection_context = match object.kind.intersects_ray_at(&ray) { + Ok(v) => v, + Err(err) => { + error!("Error while performing shadow casting: {err}"); + None + } + }?; + + match light.kind { + // In the case of point lights, we must check to see if both t > 0 and + // t is less than the time it took to even get to the light. + LightKind::Point { location } => { + let light_time = (location - ray.origin).norm(); + let intersection_time = *intersection_context.time; + + if intersection_time <= 0.0 || intersection_time >= light_time { + None + } else { + Some(intersection_context) + } + } + + // In the case of directional lights, only t > 0 needs to be checked, + // otherwise + LightKind::Directional { .. } => { + if *intersection_context.time <= 0.0 { + None + } else { + Some(intersection_context) + } + } + } + }) + .collect::>(); + + !intersections.is_empty() + } } + +/// Information about an intersection +#[derive(Derivative)] +#[derivative(Debug, PartialEq, PartialOrd, Ord)] +pub struct IntersectionContext { + /// The time of the intersection in the parametric ray + /// + /// 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. + pub time: NotNan, + + /// The intersection point. + #[derivative(PartialEq = "ignore", Ord = "ignore")] + pub point: Vector3, + + /// The normal vector protruding from the surface of the object at the + /// intersection point + #[derivative(PartialEq = "ignore", Ord = "ignore")] + pub normal: Vector3, +} + +impl Eq for IntersectionContext {} + +impl IntersectionContext {} diff --git a/assignment-1b/src/scene/input_file.rs b/assignment-1b/src/scene/input_file.rs index e2114f8..8fb4a5d 100644 --- a/assignment-1b/src/scene/input_file.rs +++ b/assignment-1b/src/scene/input_file.rs @@ -10,7 +10,7 @@ use crate::scene::{ Scene, }; -use super::data::DepthCueing; +use super::data::{DepthCueing, ObjectKind}; impl Scene { /// Parse the input file into a scene @@ -125,7 +125,7 @@ impl Scene { } "sphere" => scene.objects.push(Object { - kind: Box::new(Sphere { + kind: ObjectKind::Sphere(Sphere { center: read_vec3(0)?, radius: parts[3], }), @@ -138,7 +138,7 @@ impl Scene { }), "cylinder" => scene.objects.push(Object { - kind: Box::new(Cylinder { + kind: ObjectKind::Cylinder(Cylinder { center: read_vec3(0)?, direction: read_vec3(3)?, radius: parts[6], diff --git a/assignment-1b/src/scene/mod.rs b/assignment-1b/src/scene/mod.rs index 16456ee..4bf217b 100644 --- a/assignment-1b/src/scene/mod.rs +++ b/assignment-1b/src/scene/mod.rs @@ -1,8 +1,8 @@ pub mod cylinder; pub mod data; +pub mod illumination; pub mod input_file; pub mod sphere; -pub mod illumination; use nalgebra::Vector3; diff --git a/assignment-1b/src/scene/sphere.rs b/assignment-1b/src/scene/sphere.rs index 1633ade..f064b0d 100644 --- a/assignment-1b/src/scene/sphere.rs +++ b/assignment-1b/src/scene/sphere.rs @@ -12,12 +12,12 @@ pub struct Sphere { pub radius: f64, } -impl ObjectKind for Sphere { +impl Sphere { /// Given a sphere, returns the first time at which this ray intersects the /// sphere. /// /// If there is no intersection point, returns None. - fn intersects_ray_at( + pub fn intersects_ray_at( &self, ray: &Ray, ) -> Result> { diff --git a/flake.nix b/flake.nix index 336b83d..7971020 100644 --- a/flake.nix +++ b/flake.nix @@ -13,14 +13,15 @@ in rec { devShell = pkgs.mkShell { packages = (with pkgs; [ - cargo-watch cargo-deny cargo-edit - pandoc - zip - unzip - texlive.combined.scheme-full + cargo-flamegraph + cargo-watch imagemagick + pandoc + texlive.combined.scheme-full + unzip + zip (python310.withPackages (p: with p; [ numpy ])) ]) ++ (with toolchain; [