implement shadows

This commit is contained in:
Michael Zhang 2023-02-15 13:09:56 -06:00
parent 1885fe59a1
commit 43b85ae5d8
12 changed files with 213 additions and 61 deletions

View file

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

View file

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

View file

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

View file

@ -4,6 +4,8 @@
extern crate anyhow;
#[macro_use]
extern crate derivative;
#[macro_use]
extern crate tracing;
pub mod image;
pub mod ray;

View file

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

View file

@ -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<Option<IntersectionContext>> {

View file

@ -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<Option<IntersectionContext>>;
pub fn intersects_ray_at(
&self,
ray: &Ray,
) -> Result<Option<IntersectionContext>> {
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<dyn ObjectKind>,
pub kind: ObjectKind,
/// Index into the scene's material color list
pub material: usize,
@ -66,12 +81,36 @@ pub struct Light {
pub color: Vector3<f64>,
}
impl Light {
/// Get the unit directional vector pointing from the given point to this
/// light source
pub fn direction_from(&self, point: Vector3<f64>) -> Vector3<f64> {
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,
}

View file

@ -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<f64>,
use crate::{image::Color, ray::Ray};
/// The intersection point.
#[derivative(PartialEq = "ignore", Ord = "ignore")]
pub point: Vector3<f64>,
/// The normal vector protruding from the surface of the object at the
/// intersection point
#[derivative(PartialEq = "ignore", Ord = "ignore")]
pub normal: Vector3<f64>,
}
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<f64> = 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<f64>,
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::<Vec<_>>();
!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<f64>,
/// The intersection point.
#[derivative(PartialEq = "ignore", Ord = "ignore")]
pub point: Vector3<f64>,
/// The normal vector protruding from the surface of the object at the
/// intersection point
#[derivative(PartialEq = "ignore", Ord = "ignore")]
pub normal: Vector3<f64>,
}
impl Eq for IntersectionContext {}
impl IntersectionContext {}

View file

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

View file

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

View file

@ -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<Option<IntersectionContext>> {

View file

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