diff --git a/src/demos/diffuse_materials.rs b/src/demos/diffuse_materials.rs index 19a1cd1..b43d646 100644 --- a/src/demos/diffuse_materials.rs +++ b/src/demos/diffuse_materials.rs @@ -39,9 +39,15 @@ impl Demo for DiffuseMaterials { } color /= samples as f64; - buf[offset] = (255.99 * color.r()) as u8; - buf[offset + 1] = (255.99 * color.g()) as u8; - buf[offset + 2] = (255.99 * color.b()) as u8; + // Without taking square root of each color, we get a picture that + // is quite dark + // Spheres in this case are absorbing 50% of the light casted on them + // So, IRL, It *should* look a bit lighter in color + // To do that, We apply gamma correction by a factor of 2 + // which means multiple rgb values by 1/gamma aka 1/2 + buf[offset] = (255.99 * color.r().sqrt()) as u8; + buf[offset + 1] = (255.99 * color.g().sqrt()) as u8; + buf[offset + 2] = (255.99 * color.b().sqrt()) as u8; offset += 4; } } @@ -49,8 +55,12 @@ impl Demo for DiffuseMaterials { } fn calc_color(ray: Ray, world: &HitableList, rng: &mut rand::rngs::ThreadRng) -> Vec3 { - if let Some(hit_rec) = world.hit(&ray, 0.0, std::f64::MAX) { - let target = hit_rec.p + hit_rec.normal + random_point_in_unit_sphere(rng); + // The value of t_min here could've been 0.0 but since f32/f64 can only be + // partially compared, It may cause shadow acne effect. + // To combat this problem, We set a bias + // More information here, https://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/#shadow-acne + if let Some(hit_rec) = world.hit(&ray, 0.001, std::f64::MAX) { + let target = hit_rec.p + hit_rec.normal + random_point_in_unit_space(rng); calc_color(Ray::new(hit_rec.p, target - hit_rec.p), &world, rng) * 0.5 } else { let unit_direction = ray.direction().unit_vector(); @@ -59,7 +69,7 @@ fn calc_color(ray: Ray, world: &HitableList, rng: &mut rand::rngs::ThreadRng) -> } } -fn random_point_in_unit_sphere(rng: &mut rand::rngs::ThreadRng) -> Vec3 { +fn random_point_in_unit_space(rng: &mut rand::rngs::ThreadRng) -> Vec3 { let mut point = Vec3::new(rng.gen::(), rng.gen::(), rng.gen::()) * 2.0 - Vec3::new(1.0, 1.0, 1.0); while point.sq_len() >= 1.0 { diff --git a/src/demos/materials.rs b/src/demos/materials.rs new file mode 100644 index 0000000..c2dd008 --- /dev/null +++ b/src/demos/materials.rs @@ -0,0 +1,86 @@ +use { + crate::{ + demos::Demo, + types::{material, Hitable, HitableList, Ray, Sphere, Vec3}, + Camera, + }, + rand::Rng, +}; + +pub struct Materials; + +impl Demo for Materials { + fn name(&self) -> &'static str { + "Metal Material" + } + + fn render(&self, buf: &mut [u8], width: usize, height: usize, samples: u8) { + let world = HitableList { + list: vec![ + Box::new(Sphere::with_material( + Vec3::new(0.0, 0.0, -1.0), + 0.5, + Box::new(material::Lambertian::new(Vec3::new(0.8, 0.3, 0.3))), + )), + Box::new(Sphere::with_material( + Vec3::new(0.0, -100.5, -1.0), + 100.0, + Box::new(material::Lambertian::new(Vec3::new(0.8, 0.8, 0.0))), + )), + Box::new(Sphere::with_material( + Vec3::new(1.0, 0.0, -1.0), + 0.5, + Box::new(material::Metal::new(Vec3::new(0.8, 0.6, 0.2))), + )), + Box::new(Sphere::with_material( + Vec3::new(-1.0, 0.0, -1.0), + 0.5, + Box::new(material::Metal::new(Vec3::new(0.8, 0.8, 0.8))), + )), + ], + }; + + let camera: Camera = Default::default(); + let mut rng = rand::thread_rng(); + let mut offset = 0; + + for j in (0..height).rev() { + for i in 0..width { + let mut color = Vec3::new(0.0, 0.0, 0.0); + for _s in 0..samples { + let u = (i as f64 + rng.gen::()) / width as f64; + let v = (j as f64 + rng.gen::()) / height as f64; + + let ray = camera.get_ray(u, v); + color += calc_color(ray, &world, 0); + } + + color /= samples as f64; + // gamma 2 corrected + buf[offset] = (255.99 * color.r().sqrt()) as u8; + buf[offset + 1] = (255.99 * color.g().sqrt()) as u8; + buf[offset + 2] = (255.99 * color.b().sqrt()) as u8; + offset += 4; + } + } + } +} + +fn calc_color(ray: Ray, world: &HitableList, depth: u32) -> Vec3 { + if let Some(hit_rec) = world.hit(&ray, 0.001, std::f64::MAX) { + if depth >= 50 { + Vec3::new(0.0, 0.0, 0.0) + } else { + let material = hit_rec.material.as_ref(); + if let (attenuation, Some(scattered_ray)) = material.unwrap().scatter(&ray, &hit_rec) { + calc_color(scattered_ray, &world, depth + 1) * attenuation + } else { + Vec3::new(0.0, 0.0, 0.0) + } + } + } else { + let unit_direction = ray.direction().unit_vector(); + let t = 0.5 * (unit_direction.y() + 1.0); + Vec3::new(1.0, 1.0, 1.0) * (1.0 - t) + Vec3::new(0.5, 0.7, 1.0) * t + } +} diff --git a/src/demos/mod.rs b/src/demos/mod.rs index 43d6aee..b84e88c 100644 --- a/src/demos/mod.rs +++ b/src/demos/mod.rs @@ -1,6 +1,7 @@ mod diffuse_materials; mod hitable_sphere; mod linear_gradient_rectangle; +mod materials; mod simple_antialiasing; mod simple_rectangle; mod simple_sphere; @@ -9,6 +10,7 @@ mod surface_normal_sphere; pub use diffuse_materials::DiffuseMaterials; pub use hitable_sphere::HitableSphere; pub use linear_gradient_rectangle::LinearGradientRectangle; +pub use materials::Materials; pub use simple_antialiasing::SimpleAntialiasing; pub use simple_rectangle::SimpleRectangle; pub use simple_sphere::SimpleSphere; diff --git a/src/main.rs b/src/main.rs index e32a7b2..90a8910 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,6 +73,7 @@ fn main() -> Result<(), String> { Some(Keycode::Num5) => active_demo = Box::new(demos::HitableSphere), Some(Keycode::Num6) => active_demo = Box::new(demos::SimpleAntialiasing), Some(Keycode::Num7) => active_demo = Box::new(demos::DiffuseMaterials), + Some(Keycode::Num8) => active_demo = Box::new(demos::Materials), None => unreachable!(), _ => (), }; diff --git a/src/types/hitable.rs b/src/types/hitable.rs index 0368cf5..f330540 100644 --- a/src/types/hitable.rs +++ b/src/types/hitable.rs @@ -1,9 +1,10 @@ -use crate::types::{Ray, Vec3}; +use crate::types::{Material, Ray, Vec3}; -pub struct HitRecord { +pub struct HitRecord<'a> { pub t: f64, pub p: Vec3, pub normal: Vec3, + pub material: Option<&'a Box>, } pub trait Hitable { diff --git a/src/types/material.rs b/src/types/material.rs new file mode 100644 index 0000000..13ccf9f --- /dev/null +++ b/src/types/material.rs @@ -0,0 +1,64 @@ +use { + crate::types::{HitRecord, Ray, Vec3}, + rand::Rng, +}; +pub trait Material { + fn scatter(&self, ray: &Ray, hit_rec: &HitRecord) -> (Vec3, Option); +} + +pub struct Lambertian { + albedo: Vec3, +} + +impl Material for Lambertian { + fn scatter(&self, _ray: &Ray, hit_rec: &HitRecord) -> (Vec3, Option) { + let mut rng = rand::thread_rng(); + let target = hit_rec.p + hit_rec.normal + random_point_in_unit_space(&mut rng); + let scattered_ray = Ray::new(hit_rec.p, target - hit_rec.p); + + (self.albedo, Some(scattered_ray)) + } +} + +impl Lambertian { + pub fn new(a: Vec3) -> Self { + Self { albedo: a } + } +} + +fn random_point_in_unit_space(rng: &mut rand::rngs::ThreadRng) -> Vec3 { + let mut point = Vec3::new(rng.gen::(), rng.gen::(), rng.gen::()) * 2.0 + - Vec3::new(1.0, 1.0, 1.0); + while point.sq_len() >= 1.0 { + point = Vec3::new(rng.gen::(), rng.gen::(), rng.gen::()) * 2.0 + - Vec3::new(1.0, 1.0, 1.0); + } + point +} + +pub struct Metal { + albedo: Vec3, +} + +impl Metal { + pub fn new(albedo: Vec3) -> Self { + Self { albedo } + } +} + +impl Material for Metal { + fn scatter(&self, ray_in: &Ray, hit_rec: &HitRecord) -> (Vec3, Option) { + let reflected_ray = reflect(ray_in.direction().unit_vector(), hit_rec.normal); + let scattered_ray = Ray::new(hit_rec.p, reflected_ray); + + if scattered_ray.direction().dot(&hit_rec.normal) > 0.0 { + (self.albedo, Some(scattered_ray)) + } else { + (self.albedo, None) + } + } +} + +fn reflect(incident: Vec3, normal: Vec3) -> Vec3 { + incident - normal * incident.dot(&normal) * 2.0 +} diff --git a/src/types/mod.rs b/src/types/mod.rs index b0582db..215abf0 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,11 +1,13 @@ mod hitable; mod hitable_list; +pub mod material; mod ray; mod sphere; mod vec3; pub use hitable::{HitRecord, Hitable}; pub use hitable_list::HitableList; +pub use material::Material; pub use ray::Ray; pub use sphere::Sphere; pub use vec3::Vec3; diff --git a/src/types/sphere.rs b/src/types/sphere.rs index 867974f..69f5cfa 100644 --- a/src/types/sphere.rs +++ b/src/types/sphere.rs @@ -1,12 +1,24 @@ -use crate::types::{HitRecord, Hitable, Ray, Vec3}; +use crate::types::{HitRecord, Hitable, Material, Ray, Vec3}; pub struct Sphere { center: Vec3, radius: f64, + material: Option>, } impl Sphere { pub fn new(center: Vec3, radius: f64) -> Self { - Self { center, radius } + Self { + center, + radius, + material: None, + } + } + pub fn with_material(center: Vec3, radius: f64, material: Box) -> Self { + Self { + center, + radius, + material: Some(material), + } } } @@ -33,6 +45,7 @@ impl Hitable for Sphere { t: root, p, normal: (p - self.center) / self.radius, + material: self.material.as_ref(), }); } @@ -44,6 +57,7 @@ impl Hitable for Sphere { t: root, p, normal: (p - self.center) / self.radius, + material: self.material.as_ref(), }); } } diff --git a/src/types/vec3.rs b/src/types/vec3.rs index 906bc37..cfc0e9c 100644 --- a/src/types/vec3.rs +++ b/src/types/vec3.rs @@ -130,6 +130,13 @@ impl Mul for Vec3 { } } +impl Mul for Vec3 { + type Output = Vec3; + fn mul(self, o: Vec3) -> Vec3 { + Vec3([self[0] * o[0], self[1] * o[1], self[2] * o[2]]) + } +} + impl Div for Vec3 { type Output = Vec3;