fix up reflection + refraction
This commit is contained in:
parent
e8a5758103
commit
79b8faa00f
5 changed files with 213 additions and 90 deletions
18
assignment-1d/examples/sample-1-but-only-1-sphere.txt
Normal file
18
assignment-1d/examples/sample-1-but-only-1-sphere.txt
Normal 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
|
|
@ -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<_>>>()?;
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue