use std::f64::consts::{FRAC_1_PI, PI};
#[must_use]
#[inline]
pub fn fresnel_schlick(f0: f64, cos_theta: f64) -> f64 {
let ct = cos_theta.clamp(0.0, 1.0);
f0 + (1.0 - f0) * (1.0 - ct).powi(5)
}
#[must_use]
#[inline]
pub fn fresnel_schlick_rgb(f0: [f64; 3], cos_theta: f64) -> [f64; 3] {
let ct = cos_theta.clamp(0.0, 1.0);
let factor = (1.0 - ct).powi(5);
[
f0[0] + (1.0 - f0[0]) * factor,
f0[1] + (1.0 - f0[1]) * factor,
f0[2] + (1.0 - f0[2]) * factor,
]
}
#[must_use]
#[inline]
pub fn distribution_ggx(n_dot_h: f64, roughness: f64) -> f64 {
let a = roughness * roughness;
let a2 = a * a;
let ndh = n_dot_h.clamp(0.0, 1.0);
let denom = ndh * ndh * (a2 - 1.0) + 1.0;
a2 / (PI * denom * denom + 1e-15)
}
#[must_use]
#[inline]
pub fn distribution_beckmann(n_dot_h: f64, roughness: f64) -> f64 {
let a = roughness * roughness;
let ndh = n_dot_h.clamp(0.0, 1.0);
let cos2 = ndh * ndh;
let tan2 = (1.0 - cos2) / (cos2 + 1e-15);
let exp_term = (-tan2 / (a * a)).exp();
exp_term / (PI * a * a * cos2 * cos2 + 1e-15)
}
#[must_use]
#[inline]
pub fn geometry_schlick_ggx(n_dot_v: f64, roughness: f64) -> f64 {
let r = roughness + 1.0;
let k = (r * r) / 8.0; let ndv = n_dot_v.clamp(0.0, 1.0);
ndv / (ndv * (1.0 - k) + k + 1e-15)
}
#[must_use]
#[inline]
pub fn geometry_smith(n_dot_v: f64, n_dot_l: f64, roughness: f64) -> f64 {
geometry_schlick_ggx(n_dot_v, roughness) * geometry_schlick_ggx(n_dot_l, roughness)
}
#[must_use]
#[inline]
pub fn cook_torrance(
n_dot_h: f64,
h_dot_v: f64,
n_dot_v: f64,
n_dot_l: f64,
roughness: f64,
f0: f64,
) -> f64 {
let d = distribution_ggx(n_dot_h, roughness);
let f = fresnel_schlick(f0, h_dot_v);
let g = geometry_smith(n_dot_v, n_dot_l, roughness);
let denom = 4.0 * n_dot_v.clamp(0.001, 1.0) * n_dot_l.clamp(0.001, 1.0);
d * f * g / denom
}
#[must_use]
#[inline]
pub fn lambert_diffuse(albedo: f64) -> f64 {
albedo * FRAC_1_PI
}
#[must_use]
#[inline]
pub fn lambert_diffuse_rgb(albedo: [f64; 3]) -> [f64; 3] {
[
albedo[0] * FRAC_1_PI,
albedo[1] * FRAC_1_PI,
albedo[2] * FRAC_1_PI,
]
}
#[must_use]
#[inline]
pub fn ior_to_f0(ior: f64) -> f64 {
let r = (1.0 - ior) / (1.0 + ior);
r * r
}
mod advanced;
pub use advanced::*;
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-6;
#[test]
fn test_fresnel_schlick_normal() {
assert!((fresnel_schlick(0.04, 1.0) - 0.04).abs() < EPS);
}
#[test]
fn test_fresnel_schlick_grazing() {
assert!((fresnel_schlick(0.04, 0.0) - 1.0).abs() < EPS);
}
#[test]
fn test_fresnel_schlick_metal() {
assert!((fresnel_schlick(0.95, 1.0) - 0.95).abs() < EPS);
}
#[test]
fn test_fresnel_schlick_monotonic() {
let mut prev = fresnel_schlick(0.04, 1.0);
for i in (0..10).rev() {
let ct = i as f64 / 10.0;
let f = fresnel_schlick(0.04, ct);
assert!(
f >= prev - EPS,
"Fresnel should increase as angle increases"
);
prev = f;
}
}
#[test]
fn test_fresnel_schlick_range() {
for ct in [0.0, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0] {
let f = fresnel_schlick(0.04, ct);
assert!(
(0.04 - EPS..=1.0 + EPS).contains(&f),
"Fresnel out of range at cos_theta={ct}: {f}"
);
}
}
#[test]
fn test_fresnel_schlick_clamps_input() {
let f = fresnel_schlick(0.04, -0.5);
assert!((f - 1.0).abs() < EPS);
let f = fresnel_schlick(0.04, 1.5);
assert!((f - 0.04).abs() < EPS);
}
#[test]
fn test_fresnel_schlick_rgb_normal() {
let f = fresnel_schlick_rgb([0.04, 0.04, 0.04], 1.0);
assert!((f[0] - 0.04).abs() < EPS);
assert!((f[1] - 0.04).abs() < EPS);
assert!((f[2] - 0.04).abs() < EPS);
}
#[test]
fn test_fresnel_schlick_rgb_grazing() {
let f = fresnel_schlick_rgb([0.04, 0.5, 0.95], 0.0);
assert!((f[0] - 1.0).abs() < EPS);
assert!((f[1] - 1.0).abs() < EPS);
assert!((f[2] - 1.0).abs() < EPS);
}
#[test]
fn test_fresnel_schlick_rgb_matches_scalar() {
for ct in [0.0, 0.3, 0.7, 1.0] {
let scalar = fresnel_schlick(0.04, ct);
let rgb = fresnel_schlick_rgb([0.04, 0.04, 0.04], ct);
assert!((rgb[0] - scalar).abs() < EPS);
}
}
#[test]
fn test_ggx_smooth() {
let d_peak = distribution_ggx(1.0, 0.01);
let d_off = distribution_ggx(0.5, 0.01);
assert!(d_peak > d_off * 100.0);
}
#[test]
fn test_ggx_rough() {
let d_peak = distribution_ggx(1.0, 0.9);
let d_off = distribution_ggx(0.5, 0.9);
assert!(d_peak > d_off);
assert!(d_peak < d_off * 100.0);
}
#[test]
fn test_ggx_always_positive() {
for ndh in [0.0, 0.1, 0.5, 0.9, 1.0] {
for rough in [0.01, 0.1, 0.5, 0.9, 1.0] {
let d = distribution_ggx(ndh, rough);
assert!(d >= 0.0, "GGX negative at ndh={ndh}, rough={rough}: {d}");
}
}
}
#[test]
fn test_ggx_peak_at_one() {
for rough in [0.1, 0.3, 0.5, 0.7, 0.9] {
let d_peak = distribution_ggx(1.0, rough);
let d_half = distribution_ggx(0.5, rough);
assert!(
d_peak >= d_half,
"GGX should peak at n·h=1 for roughness={rough}"
);
}
}
#[test]
fn test_beckmann_positive() {
let d = distribution_beckmann(0.8, 0.5);
assert!(d > 0.0);
}
#[test]
fn test_beckmann_peak_at_normal() {
let d_normal = distribution_beckmann(1.0, 0.3);
let d_off = distribution_beckmann(0.7, 0.3);
assert!(d_normal > d_off);
}
#[test]
fn test_beckmann_always_positive() {
for ndh in [0.1, 0.3, 0.5, 0.7, 0.9, 1.0] {
for rough in [0.1, 0.3, 0.5, 0.7, 0.9] {
let d = distribution_beckmann(ndh, rough);
assert!(d >= 0.0, "Beckmann negative at ndh={ndh}, rough={rough}");
}
}
}
#[test]
fn test_geometry_schlick_ggx_normal() {
let g = geometry_schlick_ggx(1.0, 0.5);
assert!(g > 0.9, "Should have minimal occlusion head-on");
}
#[test]
fn test_geometry_schlick_ggx_grazing() {
let g = geometry_schlick_ggx(0.01, 0.5);
assert!(g < 0.1, "Should have heavy occlusion at grazing");
}
#[test]
fn test_geometry_schlick_ggx_range() {
for ndv in [0.0, 0.1, 0.5, 0.9, 1.0] {
for rough in [0.0, 0.3, 0.5, 0.7, 1.0] {
let g = geometry_schlick_ggx(ndv, rough);
assert!(
(0.0..=1.0 + EPS).contains(&g),
"G out of range at ndv={ndv}, rough={rough}: {g}"
);
}
}
}
#[test]
fn test_geometry_smith_no_occlusion() {
let g = geometry_smith(1.0, 1.0, 0.1);
assert!(g > 0.9);
}
#[test]
fn test_geometry_smith_grazing() {
let g = geometry_smith(0.01, 0.01, 0.5);
assert!(g < 0.1);
}
#[test]
fn test_geometry_smith_symmetric() {
let g1 = geometry_smith(0.8, 0.6, 0.3);
let g2 = geometry_smith(0.6, 0.8, 0.3);
assert!((g1 - g2).abs() < EPS, "Smith geometry should be symmetric");
}
#[test]
fn test_cook_torrance_positive() {
let spec = cook_torrance(0.9, 0.85, 0.8, 0.7, 0.3, 0.04);
assert!(spec > 0.0);
}
#[test]
fn test_cook_torrance_rougher_is_broader() {
let smooth = cook_torrance(1.0, 0.9, 0.8, 0.7, 0.1, 0.04);
let rough = cook_torrance(1.0, 0.9, 0.8, 0.7, 0.9, 0.04);
assert!(
smooth > rough,
"Smooth surface should have higher specular peak"
);
}
#[test]
fn test_cook_torrance_metal_higher() {
let dielectric = cook_torrance(0.9, 0.85, 0.8, 0.7, 0.3, 0.04);
let metal = cook_torrance(0.9, 0.85, 0.8, 0.7, 0.3, 0.95);
assert!(
metal > dielectric,
"Metal should have higher specular reflectance"
);
}
#[test]
fn test_lambert_diffuse() {
let d = lambert_diffuse(1.0);
assert!((d - 1.0 / PI).abs() < EPS);
}
#[test]
fn test_lambert_diffuse_zero() {
assert!(lambert_diffuse(0.0).abs() < EPS);
}
#[test]
fn test_lambert_diffuse_rgb() {
let d = lambert_diffuse_rgb([0.5, 0.3, 0.1]);
assert!((d[0] - 0.5 / PI).abs() < EPS);
assert!((d[1] - 0.3 / PI).abs() < EPS);
assert!((d[2] - 0.1 / PI).abs() < EPS);
}
#[test]
fn test_lambert_diffuse_rgb_matches_scalar() {
let albedo = 0.7;
let scalar = lambert_diffuse(albedo);
let rgb = lambert_diffuse_rgb([albedo, albedo, albedo]);
assert!((rgb[0] - scalar).abs() < EPS);
assert!((rgb[1] - scalar).abs() < EPS);
assert!((rgb[2] - scalar).abs() < EPS);
}
#[test]
fn test_ior_to_f0_glass() {
let f0 = ior_to_f0(1.5);
assert!((f0 - 0.04).abs() < 0.001);
}
#[test]
fn test_ior_to_f0_water() {
let f0 = ior_to_f0(1.333);
assert!((f0 - 0.02).abs() < 0.005);
}
#[test]
fn test_ior_to_f0_diamond() {
let f0 = ior_to_f0(2.417);
assert!(f0 > 0.15);
}
#[test]
fn test_ior_to_f0_air() {
assert!(ior_to_f0(1.0).abs() < EPS);
}
#[test]
fn test_ior_to_f0_increases_with_ior() {
let f0_water = ior_to_f0(1.333);
let f0_glass = ior_to_f0(1.5);
let f0_diamond = ior_to_f0(2.417);
assert!(f0_water < f0_glass);
assert!(f0_glass < f0_diamond);
}
#[test]
fn test_ior_to_f0_range() {
for n in [1.0, 1.1, 1.5, 2.0, 2.5, 3.0, 4.0] {
let f0 = ior_to_f0(n);
assert!((0.0..1.0).contains(&f0), "F0 out of range for n={n}: {f0}");
}
}
}