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 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<_>>>()?;

View file

@ -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::<f64>()
/ intersections.len() as f64;
let average =
intersections.iter().map(|s| s.shadow_opacity).sum::<f64>()
/ 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::<f64>();
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<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(
&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)
}

View file

@ -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<Color> {
pub fn trace_single_ray(
&self,
origin: Point,
ray: Ray,
depth: usize,
) -> Result<Color> {
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,

View file

@ -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<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,
})
}