fix up reflection + refraction

This commit is contained in:
Michael Zhang 2023-04-04 23:52:12 -05:00
parent e8a5758103
commit 79b8faa00f
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
5 changed files with 213 additions and 90 deletions

View file

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

View file

@ -110,7 +110,7 @@ fn main() -> Result<()> {
let ray = Ray::from_endpoints(ray_start, pixel_in_space); let ray = Ray::from_endpoints(ray_start, pixel_in_space);
// let res= rayon::spawn(|| scene.trace_single_ray(ray, 0)); // 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::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;

View file

@ -8,7 +8,12 @@ use rayon::prelude::{
ParallelIterator, 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::{ use super::{
data::{DepthCueing, Light, LightKind, Material}, data::{DepthCueing, Light, LightKind, Material},
@ -38,6 +43,7 @@ impl Scene {
&self, &self,
obj_idx: usize, obj_idx: usize,
object: &Object, object: &Object,
origin: Point,
incident_ray: Ray, incident_ray: Ray,
intersection_context: IntersectionContext, intersection_context: IntersectionContext,
depth: usize, depth: usize,
@ -77,10 +83,25 @@ impl Scene {
// The vector pointing in the direction of the light // The vector pointing in the direction of the light
let light_direction = light.direction_from(intersection_context.point); let light_direction = light.direction_from(intersection_context.point);
let normal = intersection_context.normal.normalize(); let normal = {
let viewer_direction = self.eye_pos - intersection_context.point; 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 = let halfway_direction =
((light_direction + viewer_direction) / 2.0).normalize(); ((light_direction + incoming_ray_direction) / 2.0).normalize();
let diffuse_component = material.k_d let diffuse_component = material.k_d
* diffuse_color * diffuse_color
@ -126,6 +147,7 @@ impl Scene {
}) })
.sum(); .sum();
/*
let specular_reflection: Color = { let specular_reflection: Color = {
let reflection_ray = self.compute_reflection_ray( let reflection_ray = self.compute_reflection_ray(
incident_ray.direction, incident_ray.direction,
@ -143,34 +165,53 @@ impl Scene {
let origin = origin + JITTER_CONST * reflection_ray; let origin = origin + JITTER_CONST * reflection_ray;
let ray = Ray::new(origin, 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 fresnel_coefficient * r_lambda
}; };
*/
let (eta_i, eta_t) = match intersection_context.exiting { let (eta_i, eta_t) = match intersection_context.exiting {
// true => (material.eta, 1.0), // true => (material.eta, 1.0),
_ => (1.0, material.eta), _ => (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 ZERO_COLOR
} else { } else {
self.compute_transparency( self.compute_transparency(
&intersection_context, &intersection_context,
incident_ray, &incident_ray,
material,
eta_i, eta_i,
eta_t, eta_t,
depth, depth,
)? )?
}; };
let fresnel_coefficient = self.compute_fresnel_coefficient(
material,
&incident_ray.direction,
intersection_context.normal,
);
// This is the result of the Phong illumination equation. // This is the result of the Phong illumination equation.
let color = ambient_component let color = ambient_component + diffuse_and_specular + {
+ diffuse_and_specular // This part is all the transparency + reflection stuff
+ specular_reflection fresnel_coefficient * specular_reflection_component
+ transparency; + (1.0 - fresnel_coefficient)
* (1.0 - material.alpha)
* transparency_component
};
// Apply depth cueing to the result // Apply depth cueing to the result
let a_dc = { let a_dc = {
@ -226,7 +267,7 @@ impl Scene {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct ShadowResult { struct ShadowResult {
transparent_coefficient: f64, transparent_coefficient: f64,
opacity: f64, shadow_opacity: f64,
} }
// Get the list of intersections with all the other objects in the scene // Get the list of intersections with all the other objects in the scene
@ -241,7 +282,9 @@ impl Scene {
None None
} }
}?; }?;
let intersection_time = *intersection_context.time; let intersection_time = *intersection_context.time;
let material = &self.materials[object.material_idx];
match light.kind { match light.kind {
// In the case of point lights, we must check to see if both t > 0 and // 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 { if intersection_time <= 0.0 || intersection_time >= light_time {
None None
} else { } else {
let soft_shadow_coefficient =
self.compute_soft_shadow_coefficient(location, point, object);
let material = &self.materials[object.material_idx];
Some(ShadowResult { Some(ShadowResult {
transparent_coefficient: 1.0 - material.alpha, transparent_coefficient: material.alpha,
opacity: soft_shadow_coefficient, shadow_opacity: self
.compute_soft_shadow_coefficient(location, point, object),
}) })
} }
} }
// In the case of directional lights, only t > 0 needs to be checked, // In the case of directional lights, only t > 0 needs to be checked
// otherwise
LightKind::Directional { .. } => { LightKind::Directional { .. } => {
if intersection_time <= 0.0 { if intersection_time <= 0.0 {
None None
} else { } else {
// The object obstructed the directional light, which means (1 -
// alpha) amount of light passes through
Some(ShadowResult { Some(ShadowResult {
transparent_coefficient: 1.0, transparent_coefficient: material.alpha,
opacity: 0.0,
// Opacity is 0 because there's no jitter from an infinitely far
// away light source
shadow_opacity: 0.0,
}) // complete obstruction }) // complete obstruction
} }
} }
@ -283,12 +326,14 @@ impl Scene {
match intersections.is_empty() { match intersections.is_empty() {
true => 1.0, true => 1.0,
false => { false => {
let average = intersections.iter().map(|s| s.opacity).sum::<f64>() let average =
/ intersections.len() as f64; intersections.iter().map(|s| s.shadow_opacity).sum::<f64>()
/ intersections.len() as f64;
// S * (1 - a_0) * (1 - a_1) * (...)
let transparency = intersections let transparency = intersections
.iter() .iter()
.map(|s| s.transparent_coefficient) .map(|s| 1.0 - s.transparent_coefficient)
.product::<f64>(); .product::<f64>();
average * transparency average * transparency
@ -345,13 +390,13 @@ impl Scene {
fn compute_fresnel_coefficient( fn compute_fresnel_coefficient(
&self, &self,
material: &Material, material: &Material,
incident_ray: Vector, incident_ray: &Vector,
normal: Vector, normal: Vector,
) -> f64 { ) -> 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 { 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); let f0 = ((material.eta - 1.0) / (material.eta + 1.0)).powi(2);
@ -381,11 +426,29 @@ impl Scene {
r r
} }
fn compute_specular_reflection(
&self,
intersection_context: &IntersectionContext,
incident_ray: &Ray,
depth: usize,
) -> Result<Color> {
// 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( fn compute_transparency(
&self, &self,
intersection_context: &IntersectionContext, intersection_context: &IntersectionContext,
incident_ray: Ray, incident_ray: &Ray,
material: &Material,
eta_i: f64, eta_i: f64,
eta_t: f64, eta_t: f64,
depth: usize, depth: usize,
@ -394,72 +457,67 @@ impl Scene {
trace_span!("compute_transparency", eta_i = eta_i, eta_t = eta_t); trace_span!("compute_transparency", eta_i = eta_i, eta_t = eta_t);
let _enter = span.enter(); let _enter = span.enter();
let mut n = intersection_context.normal.normalize(); // Fix the normal direction to account for exiting a material
if intersection_context.exiting { let normal = {
n = -n; let mut n = intersection_context.normal.normalize();
} if intersection_context.exiting {
n = -n;
}
n
};
let i = incident_ray.direction.normalize(); let i = incident_ray.direction.normalize();
assert!(eta_t != 0.0, "wtf eta_t is 0"); assert!(eta_t != 0.0, "wtf eta_t is 0");
// Slide 69 // This comes in two parts: one is reflection and one is refraction. The
let mut cos_theta_i = dot(i, n); // 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 { // First, calculate whether or not refraction is happening. If total
// warn!("[{depth}] cos_theta_i = {cos_theta_i}"); // internal reflection occurs, then there's no refraction since there's
cos_theta_i = 1.0; // 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 // Calculate refraction direction
if sin_theta_i * eta_i > eta_t { let a = normal.normalize() * cos_theta_t;
warn!( let s_direction = cos_theta_i * normal - i;
sin_theta_i, let m_unit = s_direction.normalize();
eta_i, eta_t, "Total internal reflection check failed." let b = m_unit * sin_theta_t;
); let t = a + b;
return Ok(ZERO_COLOR);
}
let sin_theta_t = (eta_i / eta_t) * sin_theta_i; // Jitter a bit to reduce acne
let cos_theta_t = (1.0 - sin_theta_t.powi(2)).sqrt(); // 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 // No extra color from the transmitted ray, since it's completely
let a = (-n).normalize() * cos_theta_t; // reflected
let s_direction = cos_theta_i * n - i; None => ZERO_COLOR,
let m_unit = s_direction.normalize(); };
let b = m_unit * sin_theta_t;
let t = a + b;
// Jitter a bit to reduce acne // Calculate reflection
// TODO: Is this a good constant? // let sin_theta_t = (eta_i / eta_t) * sin_theta_i;
let origin = intersection_context.point; // let cos_theta_t = (1.0 - sin_theta_t.powi(2)).sqrt();
let origin = origin + JITTER_CONST * t; // let fresnel_coefficient = self.compute_fresnel_coefficient(&material, i,
// n);
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"
); */
Ok(value) Ok(value)
} }

View file

@ -1,13 +1,18 @@
use anyhow::Result; use anyhow::Result;
use crate::{image::Color, ray::Ray}; use crate::{image::Color, ray::Ray, Point};
use super::Scene; use super::Scene;
const MAX_RECURSION_DEPTH: usize = 10_usize; const MAX_RECURSION_DEPTH: usize = 10_usize;
impl Scene { impl Scene {
pub fn trace_single_ray(&self, ray: Ray, depth: usize) -> Result<Color> { pub fn trace_single_ray(
&self,
origin: Point,
ray: Ray,
depth: usize,
) -> Result<Color> {
if depth > MAX_RECURSION_DEPTH { if depth > MAX_RECURSION_DEPTH {
return Ok(Color::new(0.0, 0.0, 0.0)); return Ok(Color::new(0.0, 0.0, 0.0));
} }
@ -45,6 +50,7 @@ impl Scene {
.compute_pixel_color( .compute_pixel_color(
obj_idx, obj_idx,
object, object,
origin,
ray, ray,
intersection_context, intersection_context,
depth, depth,

View file

@ -2,7 +2,7 @@ use anyhow::Result;
use nalgebra::{Matrix3, Vector3}; use nalgebra::{Matrix3, Vector3};
use ordered_float::NotNan; use ordered_float::NotNan;
use crate::{Vector}; use crate::{ray::Ray, Vector};
/// Finds the minimum of an iterator of f64s, ignoring any NaN values /// Finds the minimum of an iterator of f64s, ignoring any NaN values
#[inline] #[inline]
@ -93,3 +93,44 @@ pub fn compute_rotation_matrix(
Ok(f_inverse * g * f) 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<RefractionResult> {
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,
})
}