From 79b8faa00fe8d6e78c7d3cafd1444b6fe6c69804 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Tue, 4 Apr 2023 23:52:12 -0500 Subject: [PATCH] fix up reflection + refraction --- .../examples/sample-1-but-only-1-sphere.txt | 18 ++ assignment-1d/src/main.rs | 2 +- assignment-1d/src/scene/illumination.rs | 230 +++++++++++------- assignment-1d/src/scene/tracing.rs | 10 +- assignment-1d/src/utils.rs | 43 +++- 5 files changed, 213 insertions(+), 90 deletions(-) create mode 100644 assignment-1d/examples/sample-1-but-only-1-sphere.txt diff --git a/assignment-1d/examples/sample-1-but-only-1-sphere.txt b/assignment-1d/examples/sample-1-but-only-1-sphere.txt new file mode 100644 index 000000000..147e62b --- /dev/null +++ b/assignment-1d/examples/sample-1-but-only-1-sphere.txt @@ -0,0 +1,18 @@ +eye 0 5 0 +viewdir 0 0 1 +updir 0 1 0 +hfov 45 +imsize 1080 1080 +bkgcolor 0.5 0.7 0.9 1 +light 0 -1 0 0 1 1 1 +mtlcolor 1 1 1 1 1 1 0.2 0.4 0.6 60 1 0 +sphere -1.5 4 15 1 + +mtlcolor 1 1 1 1 1 1 0.2 0.8 0 20 1 0 +v 10 0 5 +v -10 0 5 +v -10 0 25 +v 10 0 25 + +f 1 2 3 +f 1 3 4 diff --git a/assignment-1d/src/main.rs b/assignment-1d/src/main.rs index e1447f1..c51dcf6 100644 --- a/assignment-1d/src/main.rs +++ b/assignment-1d/src/main.rs @@ -110,7 +110,7 @@ fn main() -> Result<()> { let ray = Ray::from_endpoints(ray_start, pixel_in_space); // let res= rayon::spawn(|| scene.trace_single_ray(ray, 0)); - scene.trace_single_ray(ray, 0) + scene.trace_single_ray(scene.eye_pos, ray, 0) }) .collect::>>()?; diff --git a/assignment-1d/src/scene/illumination.rs b/assignment-1d/src/scene/illumination.rs index 7972b68..6f663b0 100644 --- a/assignment-1d/src/scene/illumination.rs +++ b/assignment-1d/src/scene/illumination.rs @@ -8,7 +8,12 @@ use rayon::prelude::{ ParallelIterator, }; -use crate::{image::Color, ray::Ray, utils::dot, Point, Vector}; +use crate::{ + image::Color, + ray::Ray, + utils::{compute_refraction_lengths, dot, RefractionResult}, + Point, Vector, +}; use super::{ data::{DepthCueing, Light, LightKind, Material}, @@ -38,6 +43,7 @@ impl Scene { &self, obj_idx: usize, object: &Object, + origin: Point, incident_ray: Ray, intersection_context: IntersectionContext, depth: usize, @@ -77,10 +83,25 @@ impl Scene { // The vector pointing in the direction of the light let light_direction = light.direction_from(intersection_context.point); - let normal = intersection_context.normal.normalize(); - let viewer_direction = self.eye_pos - intersection_context.point; + let normal = { + let mut normal = intersection_context.normal.normalize(); + + // If we're exiting the material, the normal should face the other direction since that's + // how the reflection works + if intersection_context.exiting { + normal = -normal; + } + + normal + }; + + // Viewer direction is no longer towards the eye, but to the last origin point, so that + // transmitted rays reflect properly + // let viewer_direction = self.eye_pos - intersection_context.point; + let incoming_ray_direction = (intersection_context.point - origin).normalize(); + let halfway_direction = - ((light_direction + viewer_direction) / 2.0).normalize(); + ((light_direction + incoming_ray_direction) / 2.0).normalize(); let diffuse_component = material.k_d * diffuse_color @@ -126,6 +147,7 @@ impl Scene { }) .sum(); + /* let specular_reflection: Color = { let reflection_ray = self.compute_reflection_ray( incident_ray.direction, @@ -143,34 +165,53 @@ impl Scene { let origin = origin + JITTER_CONST * reflection_ray; let ray = Ray::new(origin, reflection_ray); - let r_lambda = self.trace_single_ray(ray, depth + 1)?; + let r_lambda = self.trace_single_ray(origin, ray, depth + 1)?; fresnel_coefficient * r_lambda }; + */ let (eta_i, eta_t) = match intersection_context.exiting { // true => (material.eta, 1.0), _ => (1.0, material.eta), }; - let transparency = if eta_t < 1.0 { + let specular_reflection_component = if material.k_s == 0.0 { + ZERO_COLOR + } else { + self.compute_specular_reflection( + &intersection_context, + &incident_ray, + depth, + )? + }; + + let transparency_component = if eta_t < 1.0 { ZERO_COLOR } else { self.compute_transparency( &intersection_context, - incident_ray, - material, + &incident_ray, eta_i, eta_t, depth, )? }; + let fresnel_coefficient = self.compute_fresnel_coefficient( + material, + &incident_ray.direction, + intersection_context.normal, + ); + // This is the result of the Phong illumination equation. - let color = ambient_component - + diffuse_and_specular - + specular_reflection - + transparency; + let color = ambient_component + diffuse_and_specular + { + // This part is all the transparency + reflection stuff + fresnel_coefficient * specular_reflection_component + + (1.0 - fresnel_coefficient) + * (1.0 - material.alpha) + * transparency_component + }; // Apply depth cueing to the result let a_dc = { @@ -226,7 +267,7 @@ impl Scene { #[derive(Clone, Copy)] struct ShadowResult { transparent_coefficient: f64, - opacity: f64, + shadow_opacity: f64, } // Get the list of intersections with all the other objects in the scene @@ -241,7 +282,9 @@ impl Scene { None } }?; + let intersection_time = *intersection_context.time; + let material = &self.materials[object.material_idx]; match light.kind { // In the case of point lights, we must check to see if both t > 0 and @@ -252,27 +295,27 @@ impl Scene { if intersection_time <= 0.0 || intersection_time >= light_time { None } else { - let soft_shadow_coefficient = - self.compute_soft_shadow_coefficient(location, point, object); - - let material = &self.materials[object.material_idx]; - Some(ShadowResult { - transparent_coefficient: 1.0 - material.alpha, - opacity: soft_shadow_coefficient, + transparent_coefficient: material.alpha, + shadow_opacity: self + .compute_soft_shadow_coefficient(location, point, object), }) } } - // In the case of directional lights, only t > 0 needs to be checked, - // otherwise + // In the case of directional lights, only t > 0 needs to be checked LightKind::Directional { .. } => { if intersection_time <= 0.0 { None } else { + // The object obstructed the directional light, which means (1 - + // alpha) amount of light passes through Some(ShadowResult { - transparent_coefficient: 1.0, - opacity: 0.0, + transparent_coefficient: material.alpha, + + // Opacity is 0 because there's no jitter from an infinitely far + // away light source + shadow_opacity: 0.0, }) // complete obstruction } } @@ -283,12 +326,14 @@ impl Scene { match intersections.is_empty() { true => 1.0, false => { - let average = intersections.iter().map(|s| s.opacity).sum::() - / intersections.len() as f64; + let average = + intersections.iter().map(|s| s.shadow_opacity).sum::() + / intersections.len() as f64; + // S * (1 - a_0) * (1 - a_1) * (...) let transparency = intersections .iter() - .map(|s| s.transparent_coefficient) + .map(|s| 1.0 - s.transparent_coefficient) .product::(); average * transparency @@ -345,13 +390,13 @@ impl Scene { fn compute_fresnel_coefficient( &self, material: &Material, - incident_ray: Vector, + incident_ray: &Vector, normal: Vector, ) -> f64 { - let mut cos_theta_i = dot(incident_ray, normal); + let mut cos_theta_i = dot(*incident_ray, normal); if cos_theta_i < 0.0 { - cos_theta_i = dot(incident_ray, -normal); + cos_theta_i = dot(*incident_ray, -normal); } let f0 = ((material.eta - 1.0) / (material.eta + 1.0)).powi(2); @@ -381,11 +426,29 @@ impl Scene { r } + fn compute_specular_reflection( + &self, + intersection_context: &IntersectionContext, + incident_ray: &Ray, + depth: usize, + ) -> Result { + // Specular reflection + let reflection_ray = self.compute_reflection_ray( + incident_ray.direction.clone(), + intersection_context.normal, + ); + + let origin = intersection_context.point; + let origin = origin + JITTER_CONST * reflection_ray; + let ray = Ray::new(origin, reflection_ray); + + self.trace_single_ray(origin, ray, depth + 1) + } + fn compute_transparency( &self, intersection_context: &IntersectionContext, - incident_ray: Ray, - material: &Material, + incident_ray: &Ray, eta_i: f64, eta_t: f64, depth: usize, @@ -394,72 +457,67 @@ impl Scene { trace_span!("compute_transparency", eta_i = eta_i, eta_t = eta_t); let _enter = span.enter(); - let mut n = intersection_context.normal.normalize(); - if intersection_context.exiting { - n = -n; - } + // Fix the normal direction to account for exiting a material + let normal = { + let mut n = intersection_context.normal.normalize(); + if intersection_context.exiting { + n = -n; + } + + n + }; let i = incident_ray.direction.normalize(); assert!(eta_t != 0.0, "wtf eta_t is 0"); - // Slide 69 - let mut cos_theta_i = dot(i, n); + // This comes in two parts: one is reflection and one is refraction. The + // refraction component will only occur if the angle remains below the + // critical angle. The reflection amount is added in proportion to the + // Fresnel coefficient. - if cos_theta_i > 1.0 { - // warn!("[{depth}] cos_theta_i = {cos_theta_i}"); - cos_theta_i = 1.0; - } + // First, calculate whether or not refraction is happening. If total + // internal reflection occurs, then there's no refraction since there's + // no ray escaping the medium. - let sin_theta_i = (1.0 - cos_theta_i.powi(2)).sqrt(); + let value = + match compute_refraction_lengths(normal, &incident_ray, eta_i, eta_t) { + Some(RefractionResult { + cos_theta_i, + sin_theta_i, + sin_theta_t, + cos_theta_t, + }) => { + // Now that we identified that there is refraction happening, transmit + // a ray through the material at the scene behind it in the + // new direction. - // Total internal reflection check - if sin_theta_i * eta_i > eta_t { - warn!( - sin_theta_i, - eta_i, eta_t, "Total internal reflection check failed." - ); - return Ok(ZERO_COLOR); - } + // Calculate refraction direction + let a = normal.normalize() * cos_theta_t; + let s_direction = cos_theta_i * normal - i; + let m_unit = s_direction.normalize(); + let b = m_unit * sin_theta_t; + let t = a + b; - let sin_theta_t = (eta_i / eta_t) * sin_theta_i; - let cos_theta_t = (1.0 - sin_theta_t.powi(2)).sqrt(); + // Jitter a bit to reduce acne + // TODO: Is this a good constant? + let origin = intersection_context.point; + let origin = origin + JITTER_CONST * t; + let ray = Ray::new(origin, t); - let fresnel_coefficient = self.compute_fresnel_coefficient(&material, i, n); + self.trace_single_ray(origin, ray, depth + 1)? + } - // Calculate refraction direction - let a = (-n).normalize() * cos_theta_t; - let s_direction = cos_theta_i * n - i; - let m_unit = s_direction.normalize(); - let b = m_unit * sin_theta_t; - let t = a + b; + // No extra color from the transmitted ray, since it's completely + // reflected + None => ZERO_COLOR, + }; - // Jitter a bit to reduce acne - // TODO: Is this a good constant? - let origin = intersection_context.point; - let origin = origin + JITTER_CONST * t; - - let ray = Ray::new(origin, t); - if ray.has_nan() { - warn!("WTF"); - } - /* assert!( - !ray.has_nan(), - "ray is nan WTF {cos_theta_i} {sin_theta_i} ({}) {sin_theta_t} {cos_theta_t} | normal: {:?}", - eta_i / eta_t, - intersection_context.normal, - ); */ - let t_lambda = self.trace_single_ray(ray, depth + 1)?; - - let value = (1.0 - fresnel_coefficient) * (1.0 - material.alpha) * t_lambda; - /* debug!( - depth, - fresnel_coefficient, - alpha = material.alpha, - ?t_lambda, - ?value, - "computing final value" - ); */ + // Calculate reflection + // let sin_theta_t = (eta_i / eta_t) * sin_theta_i; + // let cos_theta_t = (1.0 - sin_theta_t.powi(2)).sqrt(); + // let fresnel_coefficient = self.compute_fresnel_coefficient(&material, i, + // n); Ok(value) } diff --git a/assignment-1d/src/scene/tracing.rs b/assignment-1d/src/scene/tracing.rs index 5d6cc63..fb733fa 100644 --- a/assignment-1d/src/scene/tracing.rs +++ b/assignment-1d/src/scene/tracing.rs @@ -1,13 +1,18 @@ use anyhow::Result; -use crate::{image::Color, ray::Ray}; +use crate::{image::Color, ray::Ray, Point}; use super::Scene; const MAX_RECURSION_DEPTH: usize = 10_usize; impl Scene { - pub fn trace_single_ray(&self, ray: Ray, depth: usize) -> Result { + pub fn trace_single_ray( + &self, + origin: Point, + ray: Ray, + depth: usize, + ) -> Result { if depth > MAX_RECURSION_DEPTH { return Ok(Color::new(0.0, 0.0, 0.0)); } @@ -45,6 +50,7 @@ impl Scene { .compute_pixel_color( obj_idx, object, + origin, ray, intersection_context, depth, diff --git a/assignment-1d/src/utils.rs b/assignment-1d/src/utils.rs index 68b211e..0df891c 100644 --- a/assignment-1d/src/utils.rs +++ b/assignment-1d/src/utils.rs @@ -2,7 +2,7 @@ use anyhow::Result; use nalgebra::{Matrix3, Vector3}; use ordered_float::NotNan; -use crate::{Vector}; +use crate::{ray::Ray, Vector}; /// Finds the minimum of an iterator of f64s, ignoring any NaN values #[inline] @@ -93,3 +93,44 @@ pub fn compute_rotation_matrix( Ok(f_inverse * g * f) } + +pub struct RefractionResult { + pub cos_theta_i: f64, + pub sin_theta_i: f64, + pub sin_theta_t: f64, + pub cos_theta_t: f64, +} + +/// This function computes the 4 values: +/// +/// - cos_theta_i +/// - sin_theta_i +/// - sin_theta_t +/// - cos_theta_t +/// +/// If total internal reflection occurs, return None instead. +pub fn compute_refraction_lengths( + normal: Vector, + incident_ray: &Ray, + eta_i: f64, + eta_t: f64, +) -> Option { + let i = incident_ray.direction.normalize(); + let cos_theta_i = dot(i, normal); + let sin_theta_i = (1.0 - cos_theta_i.powi(2)).sqrt(); + + if sin_theta_i * eta_i > eta_t { + info!("Total internal reflection encountered."); + return None; + } + + let sin_theta_t = (eta_i / eta_t) * sin_theta_i; + let cos_theta_t = (1.0 - sin_theta_t.powi(2)).sqrt(); + + Some(RefractionResult { + cos_theta_i, + sin_theta_i, + sin_theta_t, + cos_theta_t, + }) +}