truster/
material.rs

1//! Holds the [Material] struct.
2
3use std::rc::Rc;
4
5use crate::color::Color;
6use crate::light::PointLight;
7use crate::shape::Shape;
8use crate::texture::{solid_color::SolidColor, Texture};
9use crate::tuple::Tuple;
10
11/// Material with lighting properties. Give it to a shape to change its appearance.
12#[derive(Clone)]
13pub struct Material {
14    pub texture: Rc<dyn Texture>,
15    pub ambient: f64,
16    pub diffuse: f64,
17    pub specular: f64,
18    pub shininess: f64,
19}
20
21impl Material {
22    /// Shades the object. Returns the color they would emit at `position`. `light` is the light
23    /// that is lighting the scene. `eye` is the direction of the 'eye' that is looking at the
24    /// scene. `normal` is the normal vector of the shape that the material is on at `position`.
25    /// `in_shadow` should be true if `position` is in a shadow of `light`.
26    pub fn lighting(
27        &self,
28        shape: Rc<dyn Shape>,
29        light: &PointLight,
30        position: Tuple,
31        eye: Tuple,
32        normal: Tuple,
33        in_shadow: bool,
34    ) -> Color {
35        let color = self.texture.color_at_shape(position, Rc::clone(&shape)) * light.color();
36        let lightv = (light.position() - position).normalized();
37        let ambient = color * self.ambient;
38        let light_dot_normal = lightv.dot(normal);
39
40        if in_shadow || light_dot_normal < 0.0 {
41            return ambient;
42        }
43
44        let diffuse = color * self.diffuse * light_dot_normal;
45        let reflectv = (-lightv).reflect(normal);
46        let reflect_dot_eye = reflectv.dot(eye);
47
48        if reflect_dot_eye <= 0.0 {
49            return ambient + diffuse;
50        }
51
52        let factor = reflect_dot_eye.powf(self.shininess);
53        let specular = light.color() * self.specular * factor;
54
55        return ambient + diffuse + specular;
56    }
57}
58
59impl Default for Material {
60    fn default() -> Self {
61        Self {
62            texture: Rc::new(SolidColor::new(Color::new(1.0, 1.0, 1.0))),
63            ambient: 0.1,
64            diffuse: 0.9,
65            specular: 0.9,
66            shininess: 200.0,
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::shape::sphere::Sphere;
75
76    #[test]
77    fn lighting_eye_between_light_and_surface() {
78        let shape: Rc<dyn Shape> = Rc::new(Sphere::new());
79
80        let material = Material::default();
81        let position = Tuple::point(0.0, 0.0, 0.0);
82
83        let eye = Tuple::vector(0.0, 0.0, -1.0);
84        let normal = Tuple::vector(0.0, 0.0, -1.0);
85        let light = PointLight::new(Tuple::point(0.0, 0.0, -10.0), Color::new(1.0, 1.0, 1.0));
86
87        let result = material.lighting(shape, &light, position, eye, normal, false);
88        assert_eq!(result, Color::new(1.9, 1.9, 1.9));
89    }
90
91    #[test]
92    fn lighting_eye_between_light_and_surface_light_offset_45deg() {
93        let shape: Rc<dyn Shape> = Rc::new(Sphere::new());
94
95        let material = Material::default();
96        let position = Tuple::point(0.0, 0.0, 0.0);
97
98        let eye = Tuple::vector(0.0, (2.0 as f64).sqrt() / 2.0, -(2.0 as f64).sqrt() / 2.0);
99        let normal = Tuple::vector(0.0, 0.0, -1.0);
100        let light = PointLight::new(Tuple::point(0.0, 0.0, -10.0), Color::new(1.0, 1.0, 1.0));
101
102        let result = material.lighting(shape, &light, position, eye, normal, false);
103        assert_eq!(result, Color::new(1.0, 1.0, 1.0));
104    }
105
106    #[test]
107    fn lighting_eye_opposite_surface_light_offset_45deg() {
108        let shape: Rc<dyn Shape> = Rc::new(Sphere::new());
109
110        let material = Material::default();
111        let position = Tuple::point(0.0, 0.0, 0.0);
112
113        let eye = Tuple::vector(0.0, 0.0, -1.0);
114        let normal = Tuple::vector(0.0, 0.0, -1.0);
115        let light = PointLight::new(Tuple::point(0.0, 10.0, -10.0), Color::new(1.0, 1.0, 1.0));
116
117        let result = material.lighting(shape, &light, position, eye, normal, false);
118        assert_eq!(
119            result,
120            Color::new(0.7363961030678927, 0.7363961030678927, 0.7363961030678927)
121        );
122    }
123
124    #[test]
125    fn lighting_eye_in_path_reflector() {
126        let shape: Rc<dyn Shape> = Rc::new(Sphere::new());
127
128        let material = Material::default();
129        let position = Tuple::point(0.0, 0.0, 0.0);
130
131        let eye = Tuple::vector(0.0, -(2.0 as f64).sqrt() / 2.0, -(2.0 as f64).sqrt() / 2.0);
132        let normal = Tuple::vector(0.0, 0.0, -1.0);
133        let light = PointLight::new(Tuple::point(0.0, 10.0, -10.0), Color::new(1.0, 1.0, 1.0));
134
135        let result = material.lighting(shape, &light, position, eye, normal, false);
136        assert_eq!(
137            result,
138            Color::new(1.6363961030678928, 1.6363961030678928, 1.6363961030678928)
139        );
140    }
141
142    #[test]
143    fn lighting_light_behind_surface() {
144        let shape: Rc<dyn Shape> = Rc::new(Sphere::new());
145
146        let material = Material::default();
147        let position = Tuple::point(0.0, 0.0, 0.0);
148
149        let eye = Tuple::vector(0.0, 0.0, -1.0);
150        let normal = Tuple::vector(0.0, 0.0, -1.0);
151        let light = PointLight::new(Tuple::point(0.0, 0.0, 10.0), Color::new(1.0, 1.0, 1.0));
152
153        let result = material.lighting(shape, &light, position, eye, normal, false);
154        assert_eq!(result, Color::new(0.1, 0.1, 0.1));
155    }
156
157    #[test]
158    fn lighting_surface_in_shadow() {
159        let shape: Rc<dyn Shape> = Rc::new(Sphere::new());
160
161        let material = Material::default();
162        let position = Tuple::point(0.0, 0.0, 0.0);
163
164        let eye = Tuple::vector(0.0, 0.0, -1.0);
165        let normal = Tuple::vector(0.0, 0.0, -1.0);
166        let light = PointLight::new(Tuple::point(0.0, 0.0, -10.0), Color::new(1.0, 1.0, 1.0));
167
168        let result = material.lighting(shape, &light, position, eye, normal, true);
169        assert_eq!(result, Color::new(0.1, 0.1, 0.1));
170    }
171}