use crate::ray::{HitRecord, Photon, Ray};
use nalgebra::{Unit, Vector3};
use rand::Rng;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MaterialParams {
pub name: String,
pub reflectance_pct: f64,
pub ior: f64,
pub transmittance_pct: f64,
pub thickness_mm: f64,
pub diffusion_pct: f64,
}
impl MaterialParams {
pub fn to_material(&self) -> Material {
let is_transparent = self.transmittance_pct > 0.0;
let is_near_absorber = self.reflectance_pct < 2.0 && !is_transparent;
if is_near_absorber {
return Material::Absorber;
}
if is_transparent {
let ior = if self.ior > 0.0 { self.ior } else { 1.49 };
let min_refl = self.reflectance_pct / 100.0;
if self.diffusion_pct < 5.0 {
Material::ClearTransmitter {
ior,
transmittance: self.transmittance_pct / 100.0,
min_reflectance: min_refl,
}
} else {
let thickness_m = self.thickness_mm / 1000.0;
let tau = (self.transmittance_pct / 100.0).max(0.001);
let mu_a = -(tau.ln()) / thickness_m;
let mu_s = (self.diffusion_pct / 100.0) * 2000.0;
let g = 0.9 * (1.0 - self.diffusion_pct / 100.0);
Material::DiffuseTransmitter {
ior,
scattering_coeff: mu_s,
absorption_coeff: mu_a,
asymmetry: g,
thickness: thickness_m,
min_reflectance: min_refl,
}
}
} else {
let rho = self.reflectance_pct / 100.0;
if self.diffusion_pct < 1.0 {
Material::SpecularReflector { reflectance: rho }
} else if self.diffusion_pct > 99.0 {
Material::DiffuseReflector { reflectance: rho }
} else {
Material::MixedReflector {
reflectance: rho,
specular_fraction: 1.0 - self.diffusion_pct / 100.0,
}
}
}
}
}
#[derive(Debug, Clone)]
pub enum Material {
Absorber,
DiffuseReflector {
reflectance: f64,
},
SpecularReflector {
reflectance: f64,
},
MixedReflector {
reflectance: f64,
specular_fraction: f64,
},
ClearTransmitter {
ior: f64,
transmittance: f64,
min_reflectance: f64,
},
DiffuseTransmitter {
ior: f64,
scattering_coeff: f64,
absorption_coeff: f64,
asymmetry: f64,
thickness: f64,
min_reflectance: f64,
},
}
#[derive(Debug, Clone)]
pub enum Interaction {
Absorbed,
Reflected { new_ray: Ray, attenuation: f64 },
Transmitted { new_ray: Ray, attenuation: f64 },
}
impl Material {
pub fn interact<R: Rng>(&self, photon: &Photon, hit: &HitRecord, rng: &mut R) -> Interaction {
match self {
Material::Absorber => Interaction::Absorbed,
Material::DiffuseReflector { reflectance } => {
if rng.random::<f64>() > *reflectance {
return Interaction::Absorbed;
}
let new_dir = random_cosine_hemisphere(&hit.normal, rng);
Interaction::Reflected {
new_ray: Ray::new(hit.point + new_dir.as_ref() * 1e-6, new_dir),
attenuation: 1.0, }
}
Material::SpecularReflector { reflectance } => {
if rng.random::<f64>() > *reflectance {
return Interaction::Absorbed;
}
let reflected = reflect(&photon.ray.direction, &hit.normal);
Interaction::Reflected {
new_ray: Ray::new(hit.point + reflected.as_ref() * 1e-6, reflected),
attenuation: 1.0,
}
}
Material::MixedReflector {
reflectance,
specular_fraction,
} => {
if rng.random::<f64>() > *reflectance {
return Interaction::Absorbed;
}
let new_dir = if rng.random::<f64>() < *specular_fraction {
reflect(&photon.ray.direction, &hit.normal)
} else {
random_cosine_hemisphere(&hit.normal, rng)
};
Interaction::Reflected {
new_ray: Ray::new(hit.point + new_dir.as_ref() * 1e-6, new_dir),
attenuation: 1.0,
}
}
Material::ClearTransmitter {
ior,
transmittance,
min_reflectance,
} => {
interact_clear_transmitter(photon, hit, *ior, *transmittance, *min_reflectance, rng)
}
Material::DiffuseTransmitter {
ior,
scattering_coeff,
absorption_coeff,
asymmetry,
thickness,
min_reflectance,
} => interact_diffuse_transmitter(
photon,
hit,
*ior,
*scattering_coeff,
*absorption_coeff,
*asymmetry,
*thickness,
*min_reflectance,
rng,
),
}
}
}
fn fresnel_schlick(cos_theta: f64, ior_ratio: f64) -> f64 {
let r0 = ((1.0 - ior_ratio) / (1.0 + ior_ratio)).powi(2);
r0 + (1.0 - r0) * (1.0 - cos_theta).powi(5)
}
fn reflect(incoming: &Unit<Vector3<f64>>, normal: &Unit<Vector3<f64>>) -> Unit<Vector3<f64>> {
let d = incoming.as_ref();
let n = normal.as_ref();
Unit::new_normalize(d - 2.0 * d.dot(n) * n)
}
fn refract(
incoming: &Unit<Vector3<f64>>,
normal: &Unit<Vector3<f64>>,
eta_ratio: f64,
) -> Option<Unit<Vector3<f64>>> {
let cos_i = (-incoming.as_ref()).dot(normal.as_ref()).min(1.0);
let sin2_t = eta_ratio * eta_ratio * (1.0 - cos_i * cos_i);
if sin2_t > 1.0 {
return None; }
let cos_t = (1.0 - sin2_t).sqrt();
let refracted = eta_ratio * incoming.as_ref() + (eta_ratio * cos_i - cos_t) * normal.as_ref();
Some(Unit::new_normalize(refracted))
}
fn random_cosine_hemisphere<R: Rng>(
normal: &Unit<Vector3<f64>>,
rng: &mut R,
) -> Unit<Vector3<f64>> {
let u1: f64 = rng.random();
let u2: f64 = rng.random();
let r = u1.sqrt();
let theta = 2.0 * PI * u2;
let x = r * theta.cos();
let y = r * theta.sin();
let z = (1.0 - u1).sqrt();
let (tangent, bitangent) = build_onb(normal);
let dir = x * tangent.as_ref() + y * bitangent.as_ref() + z * normal.as_ref();
Unit::new_normalize(dir)
}
fn sample_henyey_greenstein<R: Rng>(
incoming: &Unit<Vector3<f64>>,
g: f64,
rng: &mut R,
) -> Unit<Vector3<f64>> {
let xi: f64 = rng.random();
let cos_theta = if g.abs() < 1e-6 {
1.0 - 2.0 * xi
} else {
let term = (1.0 - g * g) / (1.0 - g + 2.0 * g * xi);
(1.0 + g * g - term * term) / (2.0 * g)
};
let sin_theta = (1.0 - cos_theta * cos_theta).max(0.0).sqrt();
let phi = 2.0 * PI * rng.random::<f64>();
let (tangent, bitangent) = build_onb(incoming);
let dir = sin_theta * phi.cos() * tangent.as_ref()
+ sin_theta * phi.sin() * bitangent.as_ref()
+ cos_theta * incoming.as_ref();
Unit::new_normalize(dir)
}
fn build_onb(n: &Unit<Vector3<f64>>) -> (Unit<Vector3<f64>>, Unit<Vector3<f64>>) {
let a = if n.x.abs() > 0.9 {
Vector3::y_axis()
} else {
Vector3::x_axis()
};
let t = Unit::new_normalize(n.cross(a.as_ref()));
let b = Unit::new_normalize(n.cross(t.as_ref()));
(t, b)
}
fn interact_clear_transmitter<R: Rng>(
photon: &Photon,
hit: &HitRecord,
ior: f64,
transmittance: f64,
min_reflectance: f64,
rng: &mut R,
) -> Interaction {
let (eta_ratio, cos_i) = if hit.front_face {
(
1.0 / ior,
(-photon.ray.direction.as_ref())
.dot(hit.normal.as_ref())
.min(1.0),
)
} else {
(
ior,
(-photon.ray.direction.as_ref())
.dot(hit.normal.as_ref())
.min(1.0),
)
};
let fresnel_r = fresnel_schlick(cos_i.abs(), eta_ratio).max(min_reflectance);
if rng.random::<f64>() < fresnel_r {
let reflected = reflect(&photon.ray.direction, &hit.normal);
Interaction::Reflected {
new_ray: Ray::new(hit.point + reflected.as_ref() * 1e-6, reflected),
attenuation: 1.0,
}
} else {
match refract(&photon.ray.direction, &hit.normal, eta_ratio) {
Some(refracted) => {
let per_surface_tau = transmittance.sqrt();
Interaction::Transmitted {
new_ray: Ray::new(hit.point + refracted.as_ref() * 1e-6, refracted),
attenuation: per_surface_tau,
}
}
None => {
let reflected = reflect(&photon.ray.direction, &hit.normal);
Interaction::Reflected {
new_ray: Ray::new(hit.point + reflected.as_ref() * 1e-6, reflected),
attenuation: 1.0,
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn interact_diffuse_transmitter<R: Rng>(
photon: &Photon,
hit: &HitRecord,
ior: f64,
mu_s: f64,
mu_a: f64,
g: f64,
thickness: f64,
min_reflectance: f64,
rng: &mut R,
) -> Interaction {
let (eta_ratio, cos_i) = if hit.front_face {
(
1.0 / ior,
(-photon.ray.direction.as_ref())
.dot(hit.normal.as_ref())
.min(1.0),
)
} else {
(
ior,
(-photon.ray.direction.as_ref())
.dot(hit.normal.as_ref())
.min(1.0),
)
};
let fresnel_r = fresnel_schlick(cos_i.abs(), eta_ratio).max(min_reflectance);
if rng.random::<f64>() < fresnel_r {
let reflected = reflect(&photon.ray.direction, &hit.normal);
return Interaction::Reflected {
new_ray: Ray::new(hit.point + reflected.as_ref() * 1e-6, reflected),
attenuation: 1.0,
};
}
let transmittance = (-mu_a * thickness).exp();
if rng.random::<f64>() > transmittance {
return Interaction::Absorbed;
}
let refracted = match refract(&photon.ray.direction, &hit.normal, eta_ratio) {
Some(r) => r,
None => {
let reflected = reflect(&photon.ray.direction, &hit.normal);
return Interaction::Reflected {
new_ray: Ray::new(hit.point + reflected.as_ref() * 1e-6, reflected),
attenuation: 1.0,
};
}
};
let exit_dir_internal = if mu_s > 0.0 {
sample_henyey_greenstein(&refracted, g, rng)
} else {
refracted
};
let exit_eta = if hit.front_face { ior } else { 1.0 / ior };
let cos_exit = exit_dir_internal.dot(hit.normal.as_ref()).abs().min(1.0);
let exit_fresnel = fresnel_schlick(cos_exit, exit_eta);
if rng.random::<f64>() < exit_fresnel {
return Interaction::Absorbed;
}
let exit_normal = if hit.front_face {
Unit::new_unchecked(-hit.normal.into_inner())
} else {
hit.normal
};
let exit_dir = match refract(&exit_dir_internal, &exit_normal, exit_eta) {
Some(d) => d,
None => {
return Interaction::Absorbed; }
};
let exit_point = hit.point + exit_normal.as_ref() * thickness + exit_dir.as_ref() * 1e-6;
Interaction::Transmitted {
new_ray: Ray::new(exit_point, exit_dir),
attenuation: 1.0, }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::catalog;
#[test]
fn clear_pmma_produces_clear_transmitter() {
let params = catalog::clear_pmma_3mm();
let mat = params.to_material();
match mat {
Material::ClearTransmitter {
ior,
transmittance,
min_reflectance,
} => {
assert!((ior - 1.49).abs() < 0.01);
assert!((transmittance - 0.92).abs() < 0.01);
assert!((min_reflectance - 0.04).abs() < 0.01);
}
_ => panic!("Expected ClearTransmitter, got {:?}", mat),
}
}
#[test]
fn opal_pmma_produces_diffuse_transmitter() {
let params = catalog::opal_pmma_3mm();
let mat = params.to_material();
match mat {
Material::DiffuseTransmitter {
ior,
scattering_coeff,
absorption_coeff,
asymmetry,
thickness,
min_reflectance,
} => {
assert!((ior - 1.49).abs() < 0.01);
assert!(scattering_coeff > 0.0);
assert!(absorption_coeff > 0.0);
assert!(asymmetry < 0.1, "High diffusion should give low asymmetry");
assert!((thickness - 0.003).abs() < 0.0001);
assert!((min_reflectance - 0.04).abs() < 0.01);
}
_ => panic!("Expected DiffuseTransmitter, got {:?}", mat),
}
}
#[test]
fn white_paint_produces_diffuse_reflector() {
let params = catalog::white_paint();
let mat = params.to_material();
match mat {
Material::DiffuseReflector { reflectance } => {
assert!((reflectance - 0.85).abs() < 0.01);
}
_ => panic!("Expected DiffuseReflector, got {:?}", mat),
}
}
#[test]
fn mirror_produces_specular_reflector() {
let params = catalog::mirror_aluminum();
let mat = params.to_material();
match mat {
Material::SpecularReflector { reflectance } => {
assert!((reflectance - 0.95).abs() < 0.01);
}
_ => panic!("Expected SpecularReflector, got {:?}", mat),
}
}
#[test]
fn matte_black_near_absorber() {
let params = catalog::matte_black();
let mat = params.to_material();
match mat {
Material::DiffuseReflector { reflectance } => {
assert!((reflectance - 0.05).abs() < 0.01);
}
_ => panic!("Expected DiffuseReflector, got {:?}", mat),
}
}
#[test]
fn anodized_aluminum_produces_mixed_reflector() {
let params = catalog::anodized_aluminum();
let mat = params.to_material();
match mat {
Material::MixedReflector {
reflectance,
specular_fraction,
} => {
assert!((reflectance - 0.80).abs() < 0.01);
assert!((specular_fraction - 0.30).abs() < 0.01);
}
_ => panic!("Expected MixedReflector, got {:?}", mat),
}
}
}