From 95eebf21b40d69235f7a9b799bdf5282d8147a20 Mon Sep 17 00:00:00 2001 From: ishanjain28 Date: Sun, 8 Mar 2020 21:39:06 +0530 Subject: [PATCH] Added Dielectrics, Implemented Neg for Vec3 --- .gitignore | 1 + src/demos/dielectric_material.rs | 103 ++++++++++++++++++++++++++++ src/demos/diffuse_materials.rs | 4 +- src/demos/materials.rs | 15 ++-- src/demos/mod.rs | 2 + src/main.rs | 1 + src/types/hitable.rs | 14 ++++ src/types/material.rs | 114 +++++++++++++++++++++++++------ src/types/sphere.rs | 6 +- src/types/vec3.rs | 10 ++- 10 files changed, 240 insertions(+), 30 deletions(-) create mode 100644 src/demos/dielectric_material.rs diff --git a/.gitignore b/.gitignore index f37b37f..5a436ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ *.ppm +*.png diff --git a/src/demos/dielectric_material.rs b/src/demos/dielectric_material.rs new file mode 100644 index 0000000..cfbe084 --- /dev/null +++ b/src/demos/dielectric_material.rs @@ -0,0 +1,103 @@ +use { + crate::{ + demos::{Chunk, Demo}, + types::{ + material::{Dielectric, Lambertian, Metal}, + Hitable, HitableList, Ray, Sphere, Vec3, + }, + Camera, + }, + rand::Rng, +}; + +pub struct DielectricMaterial; + +impl Demo for DielectricMaterial { + fn name(&self) -> &'static str { + "dielectric-material" + } + + fn render_chunk(&self, chunk: &mut Chunk, samples: u8) { + let x = chunk.x; + let y = chunk.y; + let nx = chunk.nx; + let ny = chunk.ny; + let start_x = chunk.start_x; + let start_y = chunk.start_y; + let buffer = &mut chunk.buffer; + + let world = HitableList { + list: vec![ + Box::new(Sphere::with_material( + Vec3::new(0.0, 0.0, -1.0), + 0.5, + Box::new(Lambertian::new(Vec3::new(0.1, 0.2, 0.5))), + )), + Box::new(Sphere::with_material( + Vec3::new(0.0, -100.5, -1.0), + 100.0, + Box::new(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(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(Dielectric::new(1.5)), + )), + Box::new(Sphere::with_material( + Vec3::new(-1.0, 0.0, -1.0), + -0.45, + Box::new(Dielectric::new(1.5)), + )), + ], + }; + + let camera: Camera = Default::default(); + let mut rng = rand::thread_rng(); + let mut offset = 0; + + for j in start_y..start_y + ny { + for i in start_x..start_x + nx { + let mut color = Vec3::new(0.0, 0.0, 0.0); + for _s in 0..samples { + let u = (i as f64 + rng.gen::()) / x as f64; + let v = (j as f64 + rng.gen::()) / y as f64; + + let ray = camera.get_ray(u, v); + color += calc_color(ray, &world, 0); + } + + color /= samples as f64; + + // gamma 2 corrected + buffer[offset] = (255.99 * color.r().sqrt()) as u8; + buffer[offset + 1] = (255.99 * color.g().sqrt()) as u8; + buffer[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/diffuse_materials.rs b/src/demos/diffuse_materials.rs index 5c0ab72..8ee015d 100644 --- a/src/demos/diffuse_materials.rs +++ b/src/demos/diffuse_materials.rs @@ -67,7 +67,7 @@ fn calc_color(ray: Ray, world: &HitableList, rng: &mut rand::rngs::ThreadRng) -> // 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); + let target = hit_rec.p + hit_rec.normal + random_point_in_unit_sphere(rng); calc_color(Ray::new(hit_rec.p, target - hit_rec.p), &world, rng) * 0.5 } else { let unit_direction = ray.direction().unit_vector(); @@ -76,7 +76,7 @@ fn calc_color(ray: Ray, world: &HitableList, rng: &mut rand::rngs::ThreadRng) -> } } -fn random_point_in_unit_space(rng: &mut rand::rngs::ThreadRng) -> Vec3 { +fn random_point_in_unit_sphere(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 index 27a50fe..8e9e80b 100644 --- a/src/demos/materials.rs +++ b/src/demos/materials.rs @@ -1,7 +1,10 @@ use { crate::{ demos::{Chunk, Demo}, - types::{material, Hitable, HitableList, Ray, Sphere, Vec3}, + types::{ + material::{Lambertian, Metal}, + Hitable, HitableList, Ray, Sphere, Vec3, + }, Camera, }, rand::Rng, @@ -11,7 +14,7 @@ pub struct Materials; impl Demo for Materials { fn name(&self) -> &'static str { - "Metal Material" + "metal-material" } fn render_chunk(&self, chunk: &mut Chunk, samples: u8) { @@ -28,22 +31,22 @@ impl Demo for Materials { 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(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(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(Metal::with_fuzz(Vec3::new(0.8, 0.6, 0.2), 0.3)), )), 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))), + Box::new(Metal::with_fuzz(Vec3::new(0.8, 0.8, 0.8), 0.5)), )), ], }; diff --git a/src/demos/mod.rs b/src/demos/mod.rs index 7bf13d5..f110147 100644 --- a/src/demos/mod.rs +++ b/src/demos/mod.rs @@ -1,3 +1,4 @@ +mod dielectric_material; mod diffuse_materials; mod hitable_sphere; mod linear_gradient_rectangle; @@ -7,6 +8,7 @@ mod simple_rectangle; mod simple_sphere; mod surface_normal_sphere; +pub use dielectric_material::DielectricMaterial; pub use diffuse_materials::DiffuseMaterials; pub use hitable_sphere::HitableSphere; pub use linear_gradient_rectangle::LinearGradientRectangle; diff --git a/src/main.rs b/src/main.rs index 54a0ed0..00f24b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -78,6 +78,7 @@ fn main() -> Result<(), String> { 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), + Some(Keycode::Num9) => active_demo = Box::new(demos::DielectricMaterial), None => unreachable!(), _ => (), }; diff --git a/src/types/hitable.rs b/src/types/hitable.rs index f330540..c505832 100644 --- a/src/types/hitable.rs +++ b/src/types/hitable.rs @@ -1,9 +1,23 @@ use crate::types::{Material, Ray, Vec3}; pub struct HitRecord<'a> { + /// Rays are represented by A + t * B + /// where A is the source point and B destination point + /// by adjusting t we can move forward/back on the ray + /// + /// t is the point at which a ray intersected another object. + /// As in, If we put this value of t in A + t * B equation, We'll get the exact + /// point at which a ray intersects some other object pub t: f64, + /// Ray object otherwise is represented by the Source/Destination points + /// p is what we get when we perform the operation, A + t * B + /// i.e. A vector from Ray source to the point t pub p: Vec3, + + /// unit outward facing normal pub normal: Vec3, + + /// material if any of the surface pub material: Option<&'a Box>, } diff --git a/src/types/material.rs b/src/types/material.rs index 13ccf9f..a09240a 100644 --- a/src/types/material.rs +++ b/src/types/material.rs @@ -2,6 +2,7 @@ use { crate::types::{HitRecord, Ray, Vec3}, rand::Rng, }; + pub trait Material { fn scatter(&self, ray: &Ray, hit_rec: &HitRecord) -> (Vec3, Option); } @@ -10,46 +11,45 @@ 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); +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_sphere(&mut rng); + let scattered_ray = Ray::new(hit_rec.p, target - hit_rec.p); + + (self.albedo, Some(scattered_ray)) } - point } pub struct Metal { albedo: Vec3, + fuzz: f64, } impl Metal { pub fn new(albedo: Vec3) -> Self { - Self { albedo } + Self { albedo, fuzz: 0.0 } + } + pub fn with_fuzz(albedo: Vec3, fuzz: f64) -> Self { + Self { albedo, fuzz } } } impl Material for Metal { fn scatter(&self, ray_in: &Ray, hit_rec: &HitRecord) -> (Vec3, Option) { + let mut rng = rand::thread_rng(); + let reflected_ray = reflect(ray_in.direction().unit_vector(), hit_rec.normal); - let scattered_ray = Ray::new(hit_rec.p, reflected_ray); + let scattered_ray = Ray::new( + hit_rec.p, + reflected_ray + random_point_in_unit_sphere(&mut rng) * self.fuzz, + ); if scattered_ray.direction().dot(&hit_rec.normal) > 0.0 { (self.albedo, Some(scattered_ray)) @@ -59,6 +59,82 @@ impl Material for Metal { } } +pub struct Dielectric { + reflection_index: f64, +} + +impl Dielectric { + pub fn new(reflection_index: f64) -> Self { + Self { reflection_index } + } +} + +impl Material for Dielectric { + fn scatter(&self, ray_in: &Ray, hit_rec: &HitRecord) -> (Vec3, Option) { + let reflected_ray = reflect(ray_in.direction(), hit_rec.normal); + // Glass absorbs nothing! So, Attenuation is always going to be 1.0 for this + let attenuation = Vec3::new(1.0, 1.0, 1.0); + let mut rng = rand::thread_rng(); + + let (outward_normal, ni_over_nt, cosine) = if ray_in.direction().dot(&hit_rec.normal) > 0.0 + { + ( + -hit_rec.normal, + self.reflection_index, + (ray_in.direction().dot(&hit_rec.normal) * self.reflection_index) + / ray_in.direction().length(), + ) + } else { + ( + hit_rec.normal, + 1.0 / self.reflection_index, + (-ray_in.direction().dot(&hit_rec.normal)) / ray_in.direction().length(), + ) + }; + + if let Some(refracted_ray) = refract(ray_in.direction(), outward_normal, ni_over_nt) { + let reflect_prob = schlick(cosine, self.reflection_index); + + if rng.gen::() < reflect_prob { + (attenuation, Some(Ray::new(hit_rec.p, reflected_ray))) + } else { + (attenuation, Some(Ray::new(hit_rec.p, refracted_ray))) + } + } else { + (attenuation, Some(Ray::new(hit_rec.p, reflected_ray))) + } + } +} + +// Polynomial approximation to figure out reflectivity as the angle changes +fn schlick(cosine: f64, reflection_index: f64) -> f64 { + let mut r0 = (1.0 - reflection_index) / (1.0 + reflection_index); + r0 = r0 * r0; + r0 + (1.0 - r0) * (1.0 - cosine).powf(5.0) +} + fn reflect(incident: Vec3, normal: Vec3) -> Vec3 { incident - normal * incident.dot(&normal) * 2.0 } + +// Snell's Law +fn refract(incident: Vec3, normal: Vec3, ni_over_nt: f64) -> Option { + let uv = incident.unit_vector(); + let dt = uv.dot(&normal); + let discriminant = 1.0 - ni_over_nt * ni_over_nt * (1.0 - dt * dt); + if discriminant > 0.0 { + Some((uv - normal * dt) * ni_over_nt - normal * discriminant.sqrt()) + } else { + None + } +} + +fn random_point_in_unit_sphere(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 +} diff --git a/src/types/sphere.rs b/src/types/sphere.rs index 69f5cfa..417f4a9 100644 --- a/src/types/sphere.rs +++ b/src/types/sphere.rs @@ -1,4 +1,5 @@ use crate::types::{HitRecord, Hitable, Material, Ray, Vec3}; + pub struct Sphere { center: Vec3, radius: f64, @@ -36,9 +37,10 @@ impl Hitable for Sphere { // Check this for detailed proof // https://vchizhov.github.io/resources/ray%20tracing/ray%20tracing%20tutorial%20series%20vchizhov/ray_casting/part1/intersecting_a_sphere.md.html#appendix let discriminant = b * b - a * c; + let discriminant_root = discriminant.sqrt(); if discriminant > 0.0 { - let root = (-b - discriminant.sqrt()) / a; + let root = (-b - discriminant_root) / a; if root < t_max && root > t_min { let p = ray.point_at_parameter(root); return Some(HitRecord { @@ -49,7 +51,7 @@ impl Hitable for Sphere { }); } - let root = (-b + discriminant.sqrt()) / a; + let root = (-b + discriminant_root) / a; if root < t_max && root > t_min { let p = ray.point_at_parameter(root); diff --git a/src/types/vec3.rs b/src/types/vec3.rs index cfc0e9c..12d931e 100644 --- a/src/types/vec3.rs +++ b/src/types/vec3.rs @@ -1,6 +1,6 @@ use std::{ fmt::{Display, Formatter, Result as FmtResult}, - ops::{Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Sub, SubAssign}, + ops::{Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign}, }; #[derive(Copy, Clone)] @@ -107,6 +107,14 @@ impl SubAssign for Vec3 { } } +impl Neg for Vec3 { + type Output = Vec3; + + fn neg(self) -> Vec3 { + Vec3([-self[0], -self[1], -self[2]]) + } +} + impl MulAssign for Vec3 { fn mul_assign(&mut self, o: Vec3) { self[0] *= o[0];