use derive_builder::Builder; use crate::{ lights::PointLight, patterns::Pattern, shapes::Shape, tuples::{dot, reflect, Color, Tuple}, Float, BLACK, WHITE, }; #[derive(Builder, Debug, PartialEq, Clone)] #[builder(default)] pub struct Material { #[builder(setter(into))] pub color: Pattern, pub ambient: Float, pub diffuse: Float, pub specular: Float, pub shininess: Float, pub reflective: Float, pub transparency: Float, pub refractive_index: Float, } impl Default for Material { /// Creates the default material. fn default() -> Material { Material { color: WHITE.into(), ambient: 0.1, diffuse: 0.9, specular: 0.9, shininess: 200., reflective: 0.0, transparency: 0.0, refractive_index: 1.0, } } } /// Compute lighting contributions using the Phong reflection model. pub fn lighting( material: &Material, object: &Shape, light: &PointLight, point: Tuple, eyev: Tuple, normalv: Tuple, in_shadow: bool, ) -> Color { // Combine the surface color with the light's color. let color = material.color.pattern_at_object(object, point); let effective_color = color * light.intensity; // Find the direciton of the light source. let lightv = (light.position - point).normalize(); // Compute the ambient distribution. let ambient = effective_color * material.ambient; // This is the cosine of the angle between the light vector an the normal // vector. A negative number means the light is on the other side of the // surface. let light_dot_normal = dot(lightv, normalv); let (diffuse, specular) = if light_dot_normal < 0. { (BLACK, BLACK) } else { // Compute the diffuse contribution. let diffuse = effective_color * material.diffuse * light_dot_normal; // This represents the cosine of the angle between the relfection vector // and the eye vector. A negative number means the light reflects away // from the eye. let reflectv = reflect(-lightv, normalv); let reflect_dot_eye = dot(reflectv, eyev); let specular = if reflect_dot_eye <= 0. { BLACK } else { // Compute the specular contribution. let factor = reflect_dot_eye.powf(material.shininess); light.intensity * material.specular * factor }; (diffuse, specular) }; if in_shadow { ambient } else { ambient + diffuse + specular } } #[cfg(test)] mod tests { use crate::{materials::Material, WHITE}; #[test] fn default() { let m = Material::default(); assert_eq!( m, Material { color: WHITE.into(), ambient: 0.1, diffuse: 0.9, specular: 0.9, shininess: 200., reflective: 0.0, transparency: 0.0, refractive_index: 1.0, } ); } mod lighting { use crate::{ lights::PointLight, materials::{lighting, Material}, patterns::{Pattern, BLACK_PAT, WHITE_PAT}, shapes::Shape, tuples::{point, vector, Color}, Float, BLACK, WHITE, }; #[test] fn eye_between_light_and_surface() { let in_shadow = false; let m = Material::default(); let position = point(0., 0., 0.); let object = Shape::sphere(); // Lighting with the eye between the light and the surface. let eyev = vector(0., 0., -1.); let normalv = vector(0., 0., -1.); let light = PointLight::new(point(0., 0., -10.), WHITE); let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow); assert_eq!(result, Color::new(1.9, 1.9, 1.9)); } #[test] fn eye_between_light_and_surface_offset_45() { // Lighting with the eye between the light and the surface, eye offset 45°. let in_shadow = false; let m = Material::default(); let position = point(0., 0., 0.); let object = Shape::sphere(); let eyev = vector(0., (2. as Float).sqrt() / 2., -(2. as Float).sqrt() / 2.); let normalv = vector(0., 0., -1.); let light = PointLight::new(point(0., 0., -10.), WHITE); let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow); assert_eq!(result, WHITE); } #[test] fn eye_opposite_surface_light_offset_45() { // Lighting with the eye opposite surface, light offset 45°. let in_shadow = false; let m = Material::default(); let position = point(0., 0., 0.); let object = Shape::sphere(); let eyev = vector(0., 0., -1.); let normalv = vector(0., 0., -1.); let light = PointLight::new(point(0., 10., -10.), WHITE); let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow); assert_eq!(result, Color::new(0.7364, 0.7364, 0.7364)); } #[test] fn eye_in_path_of_reflection_vector() { // Lighting with the eye in the path of the reflection vector. let in_shadow = false; let m = Material::default(); let position = point(0., 0., 0.); let object = Shape::sphere(); let eyev = vector(0., -(2.0 as Float).sqrt() / 2., -(2.0 as Float).sqrt() / 2.); let normalv = vector(0., 0., -1.); let light = PointLight::new(point(0., 10., -10.), WHITE); let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow); assert_eq!(result, Color::new(1.63639, 1.63639, 1.63639)); } #[test] fn light_behind_surface() { // Lighting with the light behind the surface. let in_shadow = false; let m = Material::default(); let position = point(0., 0., 0.); let object = Shape::sphere(); let eyev = vector(0., 0., -1.); let normalv = vector(0., 0., -1.); let light = PointLight::new(point(0., 0., 10.), WHITE); let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow); assert_eq!(result, Color::new(0.1, 0.1, 0.1)); } #[test] fn surface_in_shadow() { // Lighting with the surface in shadow. let m = Material::default(); let position = point(0., 0., 0.); let object = Shape::sphere(); let in_shadow = true; let eyev = vector(0., 0., -1.); let normalv = vector(0., 0., -1.); let light = PointLight::new(point(0., 0., -10.), WHITE); let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow); assert_eq!(result, Color::new(0.1, 0.1, 0.1)); } #[test] fn pattern_applied() { // Lighting with a pattern applied. let object = Shape::sphere(); let m = Material { color: Pattern::stripe(WHITE_PAT, BLACK_PAT), ambient: 1., diffuse: 0., specular: 0., ..Material::default() }; let eyev = vector(0., 0., -1.); let normalv = vector(0., 0., -1.); let light = PointLight::new(point(0., 0., -10.), WHITE); let c1 = lighting( &m, &object, &light, point(0.9, 0., 0.), eyev, normalv, false, ); let c2 = lighting( &m, &object, &light, point(1.1, 0., 0.), eyev, normalv, false, ); assert_eq!(c1, WHITE); assert_eq!(c2, BLACK); } } }