common/
material.rs

1use crate::hittable::HitRecord;
2use crate::ray::Ray;
3use crate::utils::random_double;
4use crate::vec3::{Color, dot, random_in_unit_sphere, random_unit_vector, reflect, refract, unit_vector};
5
6/// Material trait: determines whether scattering occurs and the scattered ray and attenuation color.
7pub trait Material: Send + Sync {
8    /// Returns `Some((attenuation, scattered_ray))` if the ray scatters, or `None` if it is absorbed.
9    fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)>;
10}
11
12/// Lambertian (diffuse) material.
13pub struct Lambertian {
14    /// Albedo (diffuse color).
15    pub albedo: Color,
16}
17
18impl Lambertian {
19    /// Creates a `Lambertian` material with the given `albedo`.
20    pub fn new(albedo: Color) -> Self {
21        Lambertian { albedo }
22    }
23}
24
25impl Material for Lambertian {
26    fn scatter(&self, _r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)> {
27        let mut scatter_direction = rec.normal + random_unit_vector();
28        // Fall back to the normal direction if the scatter direction degenerates to a zero vector.
29        if scatter_direction.near_zero() {
30            scatter_direction = rec.normal;
31        }
32        let scattered = Ray::new(rec.p, scatter_direction);
33        Some((self.albedo, scattered))
34    }
35}
36
37/// Metal (specular reflection) material.
38pub struct Metal {
39    /// Albedo (specular color).
40    pub albedo: Color,
41    /// Fuzz factor (0.0 = perfect mirror, 1.0 = maximum blur).
42    pub fuzz: f64,
43}
44
45impl Metal {
46    /// Creates a `Metal` material. `fuzz` is clamped to \[0, 1\].
47    pub fn new(albedo: Color, fuzz: f64) -> Self {
48        Metal {
49            albedo,
50            fuzz: fuzz.min(1.0),
51        }
52    }
53}
54
55impl Material for Metal {
56    fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)> {
57        let reflected = reflect(unit_vector(r_in.direction()), rec.normal);
58        let scattered = Ray::new(
59            rec.p,
60            reflected + self.fuzz * random_in_unit_sphere(),
61        );
62        // Absorb the ray if the scattered direction goes below the surface.
63        if dot(scattered.direction(), rec.normal) > 0.0 {
64            Some((self.albedo, scattered))
65        } else {
66            None
67        }
68    }
69}
70
71// --- Schlick Approximation ---
72
73/// Reflectance computed via the Schlick approximation (angle-dependent reflection probability).
74fn schlick(cosine: f64, ref_idx: f64) -> f64 {
75    let r0 = ((1.0 - ref_idx) / (1.0 + ref_idx)).powi(2);
76    r0 + (1.0 - r0) * (1.0 - cosine).powi(5)
77}
78
79/// Dielectric (refractive) material (glass, water, diamond, etc.).
80pub struct Dielectric {
81    /// Index of refraction (air ≈ 1.0, glass ≈ 1.3–1.7, diamond ≈ 2.4).
82    pub ref_idx: f64,
83}
84
85impl Dielectric {
86    /// Creates a `Dielectric` material with the given index of refraction.
87    pub fn new(ref_idx: f64) -> Self {
88        Dielectric { ref_idx }
89    }
90}
91
92impl Material for Dielectric {
93    fn scatter(&self, r_in: &Ray, rec: &HitRecord) -> Option<(Color, Ray)> {
94        // Dielectrics do not absorb light (attenuation = white).
95        let attenuation = Color::new(1.0, 1.0, 1.0);
96        // Choose η/η' based on whether the ray enters from the front or back face.
97        let etai_over_etat = if rec.front_face {
98            1.0 / self.ref_idx
99        } else {
100            self.ref_idx
101        };
102
103        let unit_direction = unit_vector(r_in.direction());
104        let cos_theta = dot(-unit_direction, rec.normal).min(1.0);
105        let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
106
107        let direction = if etai_over_etat * sin_theta > 1.0 {
108            // Total internal reflection: Snell's law has no solution, so reflect only.
109            reflect(unit_direction, rec.normal)
110        } else {
111            // Schlick approximation: stochastically choose reflection or refraction.
112            let reflect_prob = schlick(cos_theta, etai_over_etat);
113            if random_double() < reflect_prob {
114                reflect(unit_direction, rec.normal)
115            } else {
116                refract(unit_direction, rec.normal, etai_over_etat)
117            }
118        };
119
120        Some((attenuation, Ray::new(rec.p, direction)))
121    }
122}