#[must_use]
pub fn fresnel_dielectric(cos_i: f64, n_from: f64, n_to: f64) -> f64 {
let ci = cos_i.clamp(0.0, 1.0);
if (n_from - n_to).abs() < f64::EPSILON {
return 0.0;
}
let eta = n_from / n_to;
let sin2_t = eta * eta * ci.mul_add(-ci, 1.0);
if sin2_t >= 1.0 {
return 1.0;
}
let ct = (1.0 - sin2_t).max(0.0).sqrt();
let denom_s = n_from.mul_add(ci, n_to * ct);
let denom_p = n_to.mul_add(ci, n_from * ct);
if denom_s.abs() < f64::MIN_POSITIVE || denom_p.abs() < f64::MIN_POSITIVE {
return 1.0;
}
let rs = n_from.mul_add(ci, -(n_to * ct)) / denom_s;
let rp = n_to.mul_add(ci, -(n_from * ct)) / denom_p;
(0.5 * rp.mul_add(rp, rs * rs)).clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::fresnel_dielectric;
fn tracer_reflectance_f32(n1: f32, n2: f32, cos_i: f32) -> f32 {
let eta = n1 / n2;
let sin2_t = eta * eta * cos_i.mul_add(-cos_i, 1.0);
let cos_t = (1.0 - sin2_t).max(0.0).sqrt();
let r_s = f32::mul_add(n2, -cos_t, n1 * cos_i) / f32::mul_add(n2, cos_t, n1 * cos_i);
let r_p = f32::mul_add(n1, -cos_t, n2 * cos_i) / f32::mul_add(n1, cos_t, n2 * cos_i);
0.5 * r_p.mul_add(r_p, r_s * r_s)
}
#[test]
fn normal_incidence_is_the_textbook_value() {
for (n1, n2) in [
(1.0, 1.5),
(1.5, 1.0),
(1.0, 2.417),
(1.544, 1.333),
(1.7, 1.7),
] {
let expect = ((n1 - n2) / (n1 + n2)) * ((n1 - n2) / (n1 + n2));
let got = fresnel_dielectric(1.0, n1, n2);
assert!(
(got - expect).abs() < 1e-12,
"n {n1} -> {n2}: got {got}, expected {expect}"
);
}
}
#[test]
fn matching_indices_never_reflect() {
for k in 0..=20 {
let ci = f64::from(k) / 20.0;
assert!(fresnel_dielectric(ci, 1.62, 1.62).abs() < 1e-12);
}
}
#[test]
fn total_internal_reflection_returns_one() {
let ci = 45.0_f64.to_radians().cos();
assert!((fresnel_dielectric(ci, 2.417, 1.0) - 1.0).abs() < f64::EPSILON);
let ci_in = 20.0_f64.to_radians().cos();
let r = fresnel_dielectric(ci_in, 2.417, 1.0);
assert!(r < 1.0 && r > 0.1, "{r}");
assert!((fresnel_dielectric(0.0, 1.0, 1.5) - 1.0).abs() < 1e-12);
assert!((fresnel_dielectric(0.0, 1.5, 1.0) - 1.0).abs() < f64::EPSILON);
}
#[test]
fn reciprocity_between_the_two_directions() {
let cases = [
(1.0, 1.5, 0.3),
(1.0, 1.5, 0.9),
(1.33, 1.76, 0.5),
(1.0, 2.417, 0.1),
];
for (n1, n2, ci) in cases {
let sin2_t = (n1 / n2) * (n1 / n2) * f64::mul_add(ci, -ci, 1.0);
let ct = (1.0_f64 - sin2_t).sqrt();
let forward = fresnel_dielectric(ci, n1, n2);
let back = fresnel_dielectric(ct, n2, n1);
assert!(
(forward - back).abs() < 1e-12,
"n {n1} -> {n2}, cos_i {ci}: {forward} vs {back}"
);
}
}
#[test]
fn rises_monotonically_toward_grazing_incidence() {
let mut last = 0.0;
for k in (0..=100).rev() {
let r = fresnel_dielectric(f64::from(k) / 100.0, 1.0, 1.5);
assert!(
r >= last - 1e-15,
"R must not fall as cos_i falls: {r} < {last}"
);
last = r;
}
assert!((last - 1.0).abs() < 1e-12);
}
#[test]
fn agrees_with_the_tracers_own_f32_expression() {
let cases = [
(1.0_f32, 1.544_f32),
(1.544, 1.0),
(1.0, 2.417),
(1.333, 1.76),
(1.76, 1.333),
];
for (n1, n2) in cases {
for k in 1..=40_u8 {
let ci = f32::from(k) / 40.0;
let sin2_t = (n1 / n2) * (n1 / n2) * f32::mul_add(ci, -ci, 1.0);
if sin2_t >= 0.999 {
continue; }
let tracer = f64::from(tracer_reflectance_f32(n1, n2, ci));
let mine = fresnel_dielectric(f64::from(ci), f64::from(n1), f64::from(n2));
assert!(
(tracer - mine).abs() < 2e-5,
"n {n1} -> {n2}, cos_i {ci}: tracer {tracer}, f64 {mine}"
);
}
}
}
#[test]
fn out_of_range_cosines_are_clamped() {
assert!(
(fresnel_dielectric(1.5, 1.0, 1.5) - fresnel_dielectric(1.0, 1.0, 1.5)).abs() < 1e-15
);
assert!((fresnel_dielectric(-0.5, 1.0, 1.5) - 1.0).abs() < 1e-12);
}
}