use std::f64::consts::PI;
use tracing::trace;
use super::{distribution_ggx, fresnel_schlick};
#[must_use]
#[inline]
pub fn distribution_ggx_aniso(
n_dot_h: f64,
h_dot_x: f64,
h_dot_y: f64,
roughness_x: f64,
roughness_y: f64,
) -> f64 {
let ax = roughness_x * roughness_x;
let ay = roughness_y * roughness_y;
let ndh = n_dot_h.clamp(0.0, 1.0);
let hx_ax = h_dot_x / ax;
let hy_ay = h_dot_y / ay;
let term = hx_ax * hx_ax + hy_ay * hy_ay + ndh * ndh;
1.0 / (PI * ax * ay * term * term + 1e-15)
}
#[must_use]
#[inline]
pub fn geometry_ggx_aniso(
n_dot_v: f64,
v_dot_x: f64,
v_dot_y: f64,
roughness_x: f64,
roughness_y: f64,
) -> f64 {
let ax = roughness_x * roughness_x;
let ay = roughness_y * roughness_y;
let ndv = n_dot_v.clamp(0.0, 1.0);
let vx_ax = v_dot_x * ax;
let vy_ay = v_dot_y * ay;
let lambda_sq = vx_ax * vx_ax + vy_ay * vy_ay;
let denom = ndv + (ndv * ndv + lambda_sq).sqrt();
if denom < 1e-15 {
return 0.0;
}
2.0 * ndv / denom
}
#[must_use]
#[inline]
#[allow(clippy::too_many_arguments)]
pub fn geometry_smith_aniso(
n_dot_v: f64,
n_dot_l: f64,
v_dot_x: f64,
v_dot_y: f64,
l_dot_x: f64,
l_dot_y: f64,
roughness_x: f64,
roughness_y: f64,
) -> f64 {
geometry_ggx_aniso(n_dot_v, v_dot_x, v_dot_y, roughness_x, roughness_y)
* geometry_ggx_aniso(n_dot_l, l_dot_x, l_dot_y, roughness_x, roughness_y)
}
#[must_use]
#[inline]
pub fn distribution_charlie(n_dot_h: f64, roughness: f64) -> f64 {
let alpha = roughness.clamp(0.001, 1.0);
let inv_alpha = 1.0 / alpha;
let ndh = n_dot_h.clamp(0.0, 1.0);
let sin_theta = (1.0 - ndh * ndh).sqrt();
(2.0 + inv_alpha) * sin_theta.powf(inv_alpha) / (2.0 * PI)
}
#[must_use]
#[inline]
pub fn sheen_charlie(n_dot_h: f64, n_dot_l: f64, n_dot_v: f64, roughness: f64) -> f64 {
let d = distribution_charlie(n_dot_h, roughness);
let ndl = n_dot_l.clamp(0.001, 1.0);
let ndv = n_dot_v.clamp(0.001, 1.0);
d / (4.0 * (ndl + ndv - ndl * ndv))
}
#[must_use]
#[inline]
pub fn sheen_ashikhmin(cos_theta: f64, sheen_intensity: f64) -> f64 {
let ct = cos_theta.clamp(0.0, 1.0);
sheen_intensity * (1.0 - ct).powi(5)
}
#[must_use]
#[inline]
pub fn clearcoat_distribution(n_dot_h: f64, clearcoat_roughness: f64) -> f64 {
distribution_ggx(n_dot_h, clearcoat_roughness)
}
#[must_use]
#[inline]
pub fn clearcoat_fresnel(cos_theta: f64) -> f64 {
fresnel_schlick(0.04, cos_theta)
}
#[must_use]
#[inline]
pub fn clearcoat_geometry(n_dot_v: f64, n_dot_l: f64) -> f64 {
let ndv = n_dot_v.clamp(0.001, 1.0);
let ndl = n_dot_l.clamp(0.001, 1.0);
0.25 / (ndv * ndl)
}
#[must_use]
#[inline]
pub fn clearcoat_brdf(
n_dot_h: f64,
n_dot_v: f64,
n_dot_l: f64,
h_dot_v: f64,
clearcoat_roughness: f64,
) -> f64 {
let d = clearcoat_distribution(n_dot_h, clearcoat_roughness);
let f = clearcoat_fresnel(h_dot_v);
let g = clearcoat_geometry(n_dot_v, n_dot_l);
d * f * g
}
#[must_use]
#[inline]
pub fn clearcoat_blend(base_brdf: f64, coat_brdf: f64, clearcoat: f64, h_dot_v: f64) -> f64 {
let fc = clearcoat_fresnel(h_dot_v);
(1.0 - fc * clearcoat) * base_brdf + clearcoat * coat_brdf
}
#[must_use]
#[inline]
pub fn sss_profile_burley(r: f64, d: f64) -> f64 {
if r < 1e-15 {
return 1.0 / (2.0 * PI * d * d);
}
let inv_8pd = 1.0 / (8.0 * PI * d);
inv_8pd * ((-r / d).exp() + (-r / (3.0 * d)).exp()) / r
}
#[must_use]
#[inline]
pub fn sss_profile_gaussian(r: f64, sigma: f64) -> f64 {
let s2 = sigma * sigma;
(-r * r / (2.0 * s2)).exp() / (2.0 * PI * s2)
}
#[must_use]
#[inline]
pub fn subsurface_diffuse(n_dot_l: f64, n_dot_v: f64, roughness: f64) -> f64 {
let ndl = n_dot_l.clamp(0.0, 1.0);
let ndv = n_dot_v.clamp(0.0, 1.0);
let fl = (1.0 - ndl).powi(5);
let fv = (1.0 - ndv).powi(5);
let f_ss90 = roughness * roughness;
let f_ss = (1.0 + (f_ss90 - 1.0) * fl) * (1.0 + (f_ss90 - 1.0) * fv);
let fss = 1.25 * (f_ss * (1.0 / (ndl + ndv + 1e-15) - 0.5) + 0.5);
fss / PI
}
#[must_use]
#[inline]
pub fn sss_transmittance(thickness: f64, extinction: f64) -> f64 {
(-extinction * thickness).exp()
}
#[must_use]
#[inline]
pub fn iridescence_fresnel(
n_outside: f64,
n_film: f64,
n_base: f64,
thickness: f64,
wavelength: f64,
cos_theta: f64,
) -> f64 {
let ct = cos_theta.clamp(0.0, 1.0);
let sin2_theta = 1.0 - ct * ct;
let sin2_film = sin2_theta * (n_outside / n_film) * (n_outside / n_film);
if sin2_film >= 1.0 {
return 1.0; }
let cos_film = (1.0 - sin2_film).sqrt();
let r01 = {
let a = n_outside * ct;
let b = n_film * cos_film;
((a - b) / (a + b)).powi(2)
};
let sin2_base = sin2_theta * (n_outside / n_base) * (n_outside / n_base);
let cos_base = if sin2_base < 1.0 {
(1.0 - sin2_base).sqrt()
} else {
0.0
};
let r12 = {
let a = n_film * cos_film;
let b = n_base * cos_base;
((a - b) / (a + b + 1e-15)).powi(2)
};
let delta = std::f64::consts::TAU * n_film * thickness * cos_film / wavelength;
let cos_d = delta.cos();
let sqrt_rr = (r01 * r12).sqrt();
let cos_2d = 2.0 * cos_d * cos_d - 1.0; let num = r01 + r12 + 2.0 * sqrt_rr * cos_2d;
let den = 1.0 + r01 * r12 + 2.0 * sqrt_rr * cos_2d;
if den < 1e-15 {
return 0.0;
}
(num / den).clamp(0.0, 1.0)
}
#[must_use]
#[inline]
pub fn iridescence_rgb(
n_outside: f64,
n_film: f64,
n_base: f64,
thickness_nm: f64,
cos_theta: f64,
) -> [f64; 3] {
[
iridescence_fresnel(n_outside, n_film, n_base, thickness_nm, 650.0, cos_theta),
iridescence_fresnel(n_outside, n_film, n_base, thickness_nm, 550.0, cos_theta),
iridescence_fresnel(n_outside, n_film, n_base, thickness_nm, 450.0, cos_theta),
]
}
#[must_use]
#[inline]
pub fn henyey_greenstein(cos_theta: f64, g: f64) -> f64 {
let g2 = g * g;
let denom = 1.0 + g2 - 2.0 * g * cos_theta;
let denom_full = denom * denom.sqrt();
if denom_full < 1e-15 {
return 0.0;
}
(1.0 - g2) / (4.0 * PI * denom_full)
}
#[must_use]
#[inline]
pub fn phase_isotropic() -> f64 {
const INV_4PI: f64 = 1.0 / (4.0 * std::f64::consts::PI);
INV_4PI
}
#[must_use]
#[inline]
pub fn phase_rayleigh(cos_theta: f64) -> f64 {
3.0 * (1.0 + cos_theta * cos_theta) / (16.0 * PI)
}
#[must_use]
#[inline]
pub fn extinction_coefficient(absorption: f64, scattering: f64) -> f64 {
absorption + scattering
}
#[must_use]
#[inline]
pub fn volume_transmittance(sigma_t: f64, distance: f64) -> f64 {
(-sigma_t * distance).exp()
}
#[must_use]
#[inline]
pub fn single_scatter_albedo(absorption: f64, scattering: f64) -> f64 {
let total = absorption + scattering;
if total < 1e-15 {
return 0.0;
}
scattering / total
}
#[must_use]
#[inline]
pub fn single_scatter_inscattering(
scattering: f64,
cos_theta: f64,
g: f64,
sigma_t: f64,
distance: f64,
light_intensity: f64,
) -> f64 {
let phase = henyey_greenstein(cos_theta, g);
let transmittance = volume_transmittance(sigma_t, distance);
scattering * phase * transmittance * light_intensity
}
#[must_use]
#[inline]
pub fn sample_ggx(roughness: f64, xi1: f64, xi2: f64) -> [f64; 3] {
let a = roughness * roughness;
let a2 = a * a;
let cos_theta = ((1.0 - xi1) / (1.0 + (a2 - 1.0) * xi1)).sqrt();
let sin_theta = (1.0 - cos_theta * cos_theta).sqrt().max(0.0);
let phi = std::f64::consts::TAU * xi2;
let (sin_phi, cos_phi) = phi.sin_cos();
[sin_theta * cos_phi, sin_theta * sin_phi, cos_theta]
}
#[must_use]
#[inline]
pub fn sample_ggx_pdf(n_dot_h: f64, h_dot_v: f64, roughness: f64) -> f64 {
let d = distribution_ggx(n_dot_h, roughness);
let ndh = n_dot_h.clamp(0.0, 1.0);
let hdv = h_dot_v.clamp(0.001, 1.0);
d * ndh / (4.0 * hdv)
}
#[must_use]
#[inline]
pub fn sample_cosine_hemisphere(xi1: f64, xi2: f64) -> [f64; 3] {
let r = xi1.sqrt();
let phi = std::f64::consts::TAU * xi2;
let (sin_phi, cos_phi) = phi.sin_cos();
let x = r * cos_phi;
let y = r * sin_phi;
let z = (1.0 - xi1).sqrt().max(0.0);
[x, y, z]
}
#[must_use]
#[inline]
pub fn sample_cosine_pdf(n_dot_l: f64) -> f64 {
n_dot_l.clamp(0.0, 1.0) / PI
}
#[must_use]
#[inline]
pub fn split_sum_scale_bias(n_dot_v: f64, roughness: f64) -> (f64, f64) {
let ndv = n_dot_v.clamp(0.0, 1.0);
let r = roughness;
let rc = r.min(1.0);
let x = rc * rc;
let scale = {
let s1 = -0.024 * x + 0.132;
let s2 = s1 * x + (-0.333);
let s3 = s2 * x + 0.998;
let t1 = -1.613 * ndv + 2.227;
let t2 = t1.min(1.0);
s3 * t2
};
let bias = {
let b1 = 0.042 * x + (-0.1);
let b2 = b1 * x + 0.03;
let b3 = b2 * x + (-0.002);
let t1 = 3.09 * ndv + (-1.929);
let t2 = t1.max(0.0);
b3 * t2
};
(scale.clamp(0.0, 1.0), bias.clamp(0.0, 1.0))
}
#[must_use]
#[inline]
pub fn env_map_lod(roughness: f64, max_lod: f64) -> f64 {
roughness * max_lod
}
#[must_use]
pub fn integrate_brdf_lut(n_dot_v: f64, roughness: f64, num_samples: u32) -> (f64, f64) {
trace!(n_dot_v, roughness, num_samples, "integrate_brdf_lut");
let ndv = n_dot_v.clamp(0.001, 1.0);
let v = [(1.0 - ndv * ndv).sqrt(), 0.0, ndv];
let mut scale = 0.0;
let mut bias = 0.0;
let inv_samples = 1.0 / num_samples as f64;
for i in 0..num_samples {
let xi1 = i as f64 * inv_samples;
let xi2 = radical_inverse_vdc(i);
let h = sample_ggx(roughness, xi1, xi2);
let v_dot_h = (v[0] * h[0] + v[1] * h[1] + v[2] * h[2]).max(0.0);
let l = [
2.0 * v_dot_h * h[0] - v[0],
2.0 * v_dot_h * h[1] - v[1],
2.0 * v_dot_h * h[2] - v[2],
];
let n_dot_l = l[2].max(0.0);
let n_dot_h = h[2].max(0.0);
if n_dot_l > 0.0 {
let g = super::geometry_smith(ndv, n_dot_l, roughness);
let g_vis = (g * v_dot_h) / (n_dot_h * ndv + 1e-15);
let fc = (1.0 - v_dot_h).powi(5);
scale += g_vis * (1.0 - fc);
bias += g_vis * fc;
}
}
(scale * inv_samples, bias * inv_samples)
}
#[inline]
fn radical_inverse_vdc(bits: u32) -> f64 {
bits.reverse_bits() as f64 * 2.328_306_436_538_696_3e-10 }
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-6;
#[test]
fn test_ggx_aniso_isotropic_matches_ggx() {
let rough = 0.3;
let sin_theta = (1.0_f64 - 0.9 * 0.9).sqrt();
let d_aniso = distribution_ggx_aniso(0.9, sin_theta, 0.0, rough, rough);
let d_iso = distribution_ggx(0.9, rough);
assert!(d_aniso > 0.0, "Anisotropic GGX should be positive");
assert!(d_iso > 0.0, "Isotropic GGX should be positive");
}
#[test]
fn test_ggx_aniso_always_positive() {
for ndh in [0.1, 0.5, 0.9, 1.0] {
for rx in [0.1, 0.3, 0.7] {
for ry in [0.1, 0.3, 0.7] {
let d = distribution_ggx_aniso(ndh, 0.3, 0.2, rx, ry);
assert!(
d >= 0.0,
"Aniso GGX negative at ndh={ndh}, rx={rx}, ry={ry}"
);
}
}
}
}
#[test]
fn test_ggx_aniso_peak_at_normal() {
let d_peak = distribution_ggx_aniso(1.0, 0.0, 0.0, 0.3, 0.5);
let d_off = distribution_ggx_aniso(0.5, 0.5, 0.5, 0.3, 0.5);
assert!(d_peak > d_off, "Should peak when h aligned with n");
}
#[test]
fn test_ggx_aniso_directional() {
let d_x_off = distribution_ggx_aniso(0.9, 0.4, 0.0, 0.8, 0.1);
let d_y_off = distribution_ggx_aniso(0.9, 0.0, 0.4, 0.8, 0.1);
assert!(
d_x_off != d_y_off,
"Anisotropic should give different values for x vs y offset"
);
}
#[test]
fn test_geometry_ggx_aniso_range() {
for ndv in [0.1, 0.5, 0.9] {
let g = geometry_ggx_aniso(ndv, 0.3, 0.2, 0.3, 0.5);
assert!(
(0.0..=1.0 + EPS).contains(&g),
"Aniso G out of range at ndv={ndv}: {g}"
);
}
}
#[test]
fn test_geometry_smith_aniso_symmetric() {
let g1 = geometry_smith_aniso(0.8, 0.6, 0.3, 0.2, 0.3, 0.2, 0.3, 0.5);
let g2 = geometry_smith_aniso(0.6, 0.8, 0.3, 0.2, 0.3, 0.2, 0.3, 0.5);
assert!((g1 - g2).abs() < EPS);
}
#[test]
fn test_charlie_positive() {
for rough in [0.1, 0.3, 0.5, 0.7, 0.9] {
let d = distribution_charlie(0.8, rough);
assert!(d >= 0.0, "Charlie NDF should be positive at rough={rough}");
}
}
#[test]
fn test_charlie_varies_with_roughness() {
let d_smooth = distribution_charlie(0.5, 0.1);
let d_rough = distribution_charlie(0.5, 0.9);
assert!(
(d_smooth - d_rough).abs() > 0.001,
"Different roughness should give different NDF values"
);
}
#[test]
fn test_sheen_charlie_positive() {
let s = sheen_charlie(0.9, 0.8, 0.7, 0.5);
assert!(s >= 0.0, "Sheen BRDF should be positive");
}
#[test]
fn test_sheen_ashikhmin_at_normal() {
let s = sheen_ashikhmin(1.0, 1.0);
assert!(s.abs() < EPS, "Ashikhmin sheen at normal should be 0");
}
#[test]
fn test_sheen_ashikhmin_at_grazing() {
let s = sheen_ashikhmin(0.0, 0.8);
assert!(
(s - 0.8).abs() < EPS,
"Ashikhmin sheen at grazing = intensity"
);
}
#[test]
fn test_sheen_ashikhmin_scales() {
let s1 = sheen_ashikhmin(0.5, 0.5);
let s2 = sheen_ashikhmin(0.5, 1.0);
assert!((s2 / s1 - 2.0).abs() < EPS, "Sheen should scale linearly");
}
#[test]
fn test_clearcoat_fresnel_normal() {
let f = clearcoat_fresnel(1.0);
assert!((f - 0.04).abs() < EPS, "Clearcoat F0 ≈ 0.04");
}
#[test]
fn test_clearcoat_fresnel_grazing() {
let f = clearcoat_fresnel(0.0);
assert!((f - 1.0).abs() < EPS, "Clearcoat at grazing → 1.0");
}
#[test]
fn test_clearcoat_brdf_positive() {
let c = clearcoat_brdf(0.9, 0.8, 0.7, 0.85, 0.05);
assert!(c > 0.0, "Clearcoat BRDF should be positive");
}
#[test]
fn test_clearcoat_rougher_broader() {
let c_smooth = clearcoat_brdf(1.0, 0.8, 0.7, 0.9, 0.01);
let c_rough = clearcoat_brdf(1.0, 0.8, 0.7, 0.9, 0.1);
assert!(
c_smooth > c_rough,
"Smoother clearcoat should have sharper peak"
);
}
#[test]
fn test_clearcoat_blend_no_coat() {
let base = 0.5;
let coat = 0.3;
let result = clearcoat_blend(base, coat, 0.0, 0.8);
assert!((result - base).abs() < EPS, "Zero clearcoat = base only");
}
#[test]
fn test_clearcoat_blend_energy_conservation() {
let base = 1.0;
let coat = 1.0;
let result = clearcoat_blend(base, coat, 1.0, 0.8);
assert!(
(0.0..=2.0).contains(&result),
"Blend should be reasonable: {result}"
);
}
#[test]
fn test_clearcoat_blend_partial() {
let base = 1.0;
let coat = 0.5;
let result_half = clearcoat_blend(base, coat, 0.5, 0.8);
let result_full = clearcoat_blend(base, coat, 1.0, 0.8);
assert!(
result_half != result_full,
"Different clearcoat levels should differ"
);
}
#[test]
fn test_clearcoat_geometry_range() {
for ndv in [0.1, 0.3, 0.5, 0.7, 0.9] {
for ndl in [0.1, 0.3, 0.5, 0.7, 0.9] {
let g = clearcoat_geometry(ndv, ndl);
assert!(
g > 0.0,
"Clearcoat G should be positive at ndv={ndv}, ndl={ndl}"
);
}
}
}
#[test]
fn test_sss_burley_positive() {
for r in [0.0, 0.1, 0.5, 1.0, 5.0] {
let v = sss_profile_burley(r, 1.0);
assert!(v >= 0.0, "Burley SSS profile negative at r={r}");
}
}
#[test]
fn test_sss_burley_decreases_with_distance() {
let near = sss_profile_burley(0.1, 1.0);
let far = sss_profile_burley(2.0, 1.0);
assert!(near > far, "SSS profile should decrease with distance");
}
#[test]
fn test_sss_burley_wider_d_spreads_more() {
let narrow = sss_profile_burley(1.0, 0.5);
let wide = sss_profile_burley(1.0, 2.0);
assert!(
(narrow - wide).abs() > EPS,
"Different d should give different profiles"
);
}
#[test]
fn test_sss_gaussian_positive() {
let v = sss_profile_gaussian(0.5, 1.0);
assert!(v > 0.0);
}
#[test]
fn test_sss_gaussian_peak_at_zero() {
let peak = sss_profile_gaussian(0.0, 1.0);
let off = sss_profile_gaussian(1.0, 1.0);
assert!(peak > off);
}
#[test]
fn test_sss_gaussian_narrower_sigma_taller() {
let narrow = sss_profile_gaussian(0.0, 0.5);
let wide = sss_profile_gaussian(0.0, 2.0);
assert!(narrow > wide, "Narrower sigma = taller peak");
}
#[test]
fn test_subsurface_diffuse_range() {
for ndl in [0.1, 0.5, 0.9] {
for ndv in [0.1, 0.5, 0.9] {
let v = subsurface_diffuse(ndl, ndv, 0.5);
assert!(v > 0.0, "SSS diffuse should be positive");
assert!(v < 2.0, "SSS diffuse should be bounded");
}
}
}
#[test]
fn test_sss_transmittance() {
assert!((sss_transmittance(0.0, 1.0) - 1.0).abs() < EPS);
assert!(sss_transmittance(1.0, 1.0) < 1.0);
assert!(sss_transmittance(1.0, 1.0) > 0.0);
}
#[test]
fn test_iridescence_fresnel_range() {
for cos_t in [0.1, 0.3, 0.5, 0.7, 0.9] {
let r = iridescence_fresnel(1.0, 1.3, 1.5, 300.0, 550.0, cos_t);
assert!(
(0.0..=1.0).contains(&r),
"Iridescence out of range at cos={cos_t}: {r}"
);
}
}
#[test]
fn test_iridescence_varies_with_wavelength() {
let r_red = iridescence_fresnel(1.0, 1.3, 1.5, 300.0, 650.0, 0.8);
let r_blue = iridescence_fresnel(1.0, 1.3, 1.5, 300.0, 450.0, 0.8);
assert!(
(r_red - r_blue).abs() > 0.001,
"Iridescence should vary with wavelength"
);
}
#[test]
fn test_iridescence_varies_with_thickness() {
let r1 = iridescence_fresnel(1.0, 1.3, 1.5, 200.0, 550.0, 0.8);
let r2 = iridescence_fresnel(1.0, 1.3, 1.5, 400.0, 550.0, 0.8);
assert!(
(r1 - r2).abs() > 0.001,
"Iridescence should vary with thickness"
);
}
#[test]
fn test_iridescence_varies_with_angle() {
let r_normal = iridescence_fresnel(1.0, 1.3, 1.5, 300.0, 550.0, 1.0);
let r_grazing = iridescence_fresnel(1.0, 1.3, 1.5, 300.0, 550.0, 0.2);
assert!(
(r_normal - r_grazing).abs() > 0.01,
"Iridescence should vary with angle"
);
}
#[test]
fn test_iridescence_rgb_produces_color() {
let rgb = iridescence_rgb(1.0, 1.3, 1.5, 300.0, 0.8);
assert!(rgb[0] >= 0.0 && rgb[1] >= 0.0 && rgb[2] >= 0.0);
let all_same = (rgb[0] - rgb[1]).abs() < 0.001 && (rgb[1] - rgb[2]).abs() < 0.001;
assert!(!all_same, "Iridescence should produce color variation");
}
#[test]
fn test_hg_isotropic() {
let p_hg = henyey_greenstein(0.5, 0.0);
let p_iso = phase_isotropic();
assert!(
(p_hg - p_iso).abs() < 0.001,
"HG(g=0) should match isotropic: {p_hg} vs {p_iso}"
);
}
#[test]
fn test_hg_forward_scattering() {
let p_fwd = henyey_greenstein(1.0, 0.8);
let p_back = henyey_greenstein(-1.0, 0.8);
assert!(p_fwd > p_back, "Forward > backward for g=0.8");
}
#[test]
fn test_hg_backward_scattering() {
let p_fwd = henyey_greenstein(1.0, -0.5);
let p_back = henyey_greenstein(-1.0, -0.5);
assert!(p_back > p_fwd, "Backward > forward for g=-0.5");
}
#[test]
fn test_hg_always_positive() {
for g in [-0.9, -0.5, 0.0, 0.5, 0.9] {
for ct in [-1.0, -0.5, 0.0, 0.5, 1.0] {
let p = henyey_greenstein(ct, g);
assert!(p >= 0.0, "HG negative at g={g}, cos_theta={ct}");
}
}
}
#[test]
fn test_phase_rayleigh_symmetric() {
let p_fwd = phase_rayleigh(1.0);
let p_back = phase_rayleigh(-1.0);
assert!((p_fwd - p_back).abs() < EPS, "Rayleigh should be symmetric");
}
#[test]
fn test_phase_rayleigh_min_at_90() {
let p_90 = phase_rayleigh(0.0); let p_0 = phase_rayleigh(1.0); assert!(p_0 > p_90, "Rayleigh min at 90°");
}
#[test]
fn test_extinction_coefficient() {
assert!((extinction_coefficient(0.5, 0.3) - 0.8).abs() < EPS);
}
#[test]
fn test_volume_transmittance() {
assert!((volume_transmittance(0.0, 10.0) - 1.0).abs() < EPS);
assert!(volume_transmittance(1.0, 1.0) < 1.0);
assert!(volume_transmittance(1.0, 1.0) > 0.0);
}
#[test]
fn test_volume_transmittance_thicker_less() {
let t1 = volume_transmittance(0.5, 1.0);
let t2 = volume_transmittance(0.5, 3.0);
assert!(t2 < t1);
}
#[test]
fn test_single_scatter_albedo() {
assert!((single_scatter_albedo(0.0, 1.0) - 1.0).abs() < EPS);
assert!((single_scatter_albedo(1.0, 0.0)).abs() < EPS);
assert!((single_scatter_albedo(0.5, 0.5) - 0.5).abs() < EPS);
}
#[test]
fn test_single_scatter_albedo_zero() {
assert!((single_scatter_albedo(0.0, 0.0)).abs() < EPS);
}
#[test]
fn test_inscattering_positive() {
let l = single_scatter_inscattering(0.5, 0.8, 0.5, 1.0, 0.5, 1.0);
assert!(l > 0.0, "In-scattering should be positive");
}
#[test]
fn test_inscattering_zero_scattering() {
let l = single_scatter_inscattering(0.0, 0.8, 0.5, 1.0, 0.5, 1.0);
assert!(l.abs() < EPS, "Zero scattering = zero in-scattering");
}
#[test]
fn test_sample_ggx_normalized() {
let h = sample_ggx(0.3, 0.5, 0.5);
let len = (h[0] * h[0] + h[1] * h[1] + h[2] * h[2]).sqrt();
assert!(
(len - 1.0).abs() < 0.01,
"Sampled vector should be unit length"
);
}
#[test]
fn test_sample_ggx_hemisphere() {
for i in 0..20 {
let xi1 = i as f64 / 20.0;
let h = sample_ggx(0.5, xi1, 0.3);
assert!(h[2] >= 0.0, "Half-vector should be in upper hemisphere");
}
}
#[test]
fn test_sample_ggx_smooth_concentrates() {
let h = sample_ggx(0.01, 0.5, 0.0);
assert!(
h[2] > 0.9,
"Smooth surface should sample near normal, got z={}",
h[2]
);
}
#[test]
fn test_sample_ggx_rough_spreads() {
let h = sample_ggx(0.9, 0.5, 0.0);
assert!(h[2] < 0.9, "Rough surface should spread samples");
}
#[test]
fn test_sample_ggx_pdf_positive() {
let pdf = sample_ggx_pdf(0.9, 0.8, 0.3);
assert!(pdf > 0.0, "PDF should be positive");
}
#[test]
fn test_sample_ggx_pdf_matches_distribution() {
let pdf_smooth = sample_ggx_pdf(1.0, 1.0, 0.01);
let pdf_rough = sample_ggx_pdf(1.0, 1.0, 0.9);
assert!(pdf_smooth > pdf_rough, "Smooth should have higher peak PDF");
}
#[test]
fn test_sample_cosine_hemisphere_normalized() {
let d = sample_cosine_hemisphere(0.5, 0.5);
let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
assert!((len - 1.0).abs() < 0.01, "Should be unit length");
}
#[test]
fn test_sample_cosine_hemisphere_upper() {
for i in 0..20 {
let xi1 = i as f64 / 20.0;
let d = sample_cosine_hemisphere(xi1, 0.3);
assert!(d[2] >= 0.0, "Should be in upper hemisphere");
}
}
#[test]
fn test_sample_cosine_pdf_at_normal() {
let pdf = sample_cosine_pdf(1.0);
assert!((pdf - 1.0 / PI).abs() < EPS);
}
#[test]
fn test_sample_cosine_pdf_at_grazing() {
let pdf = sample_cosine_pdf(0.0);
assert!(pdf.abs() < EPS);
}
#[test]
fn test_split_sum_range() {
for ndv_i in 1..=10 {
for rough_i in 0..=10 {
let ndv = ndv_i as f64 / 10.0;
let rough = rough_i as f64 / 10.0;
let (scale, bias) = split_sum_scale_bias(ndv, rough);
assert!(
(0.0..=1.0).contains(&scale),
"Scale out of range at ndv={ndv}, rough={rough}: {scale}"
);
assert!(
(0.0..=1.0).contains(&bias),
"Bias out of range at ndv={ndv}, rough={rough}: {bias}"
);
}
}
}
#[test]
fn test_split_sum_smooth_normal_high_scale() {
let (scale, _) = split_sum_scale_bias(1.0, 0.0);
assert!(
scale > 0.5,
"Smooth at normal: scale should be significant, got {scale}"
);
}
#[test]
fn test_env_map_lod() {
assert!((env_map_lod(0.0, 8.0)).abs() < EPS);
assert!((env_map_lod(1.0, 8.0) - 8.0).abs() < EPS);
assert!((env_map_lod(0.5, 8.0) - 4.0).abs() < EPS);
}
#[test]
fn test_integrate_brdf_lut_range() {
let (scale, bias) = integrate_brdf_lut(0.5, 0.3, 64);
assert!(scale >= 0.0, "Scale should be non-negative");
assert!(bias >= 0.0, "Bias should be non-negative");
assert!(scale + bias <= 2.0, "Scale+bias should be reasonable");
}
#[test]
fn test_integrate_brdf_lut_smooth_vs_rough() {
let (scale_smooth, _) = integrate_brdf_lut(0.8, 0.1, 64);
let (scale_rough, _) = integrate_brdf_lut(0.8, 0.9, 64);
assert!(
scale_smooth > scale_rough,
"Smooth should have higher scale: {scale_smooth} vs {scale_rough}"
);
}
#[test]
fn test_integrate_brdf_lut_deterministic() {
let (s1, b1) = integrate_brdf_lut(0.5, 0.3, 64);
let (s2, b2) = integrate_brdf_lut(0.5, 0.3, 64);
assert!((s1 - s2).abs() < EPS, "Should be deterministic");
assert!((b1 - b2).abs() < EPS, "Should be deterministic");
}
}