From a278969bbfa4f6396996166f89cb261fe9b90869 Mon Sep 17 00:00:00 2001 From: Ishan Jain Date: Tue, 2 Mar 2021 17:54:10 +0530 Subject: [PATCH] Refactored the project and added the Texture trait --- src/demo.rs | 38 +++--- src/hitable.rs | 8 +- src/main.rs | 7 +- src/materials/dielectric.rs | 67 +++++++++++ src/materials/lambertian.rs | 26 ++++ src/materials/metal.rs | 43 +++++++ src/materials/mod.rs | 51 ++++++++ src/shapes/mod.rs | 5 + src/{types => shapes}/moving_sphere.rs | 29 ++++- src/{types => shapes}/sphere.rs | 28 ++++- src/texture/mod.rs | 9 ++ src/texture/solid.rs | 17 +++ src/types/material.rs | 160 ------------------------- src/types/mod.rs | 6 - 14 files changed, 299 insertions(+), 195 deletions(-) create mode 100644 src/materials/dielectric.rs create mode 100644 src/materials/lambertian.rs create mode 100644 src/materials/metal.rs create mode 100644 src/materials/mod.rs create mode 100644 src/shapes/mod.rs rename src/{types => shapes}/moving_sphere.rs (72%) rename src/{types => shapes}/sphere.rs (71%) create mode 100644 src/texture/mod.rs create mode 100644 src/texture/solid.rs delete mode 100644 src/types/material.rs diff --git a/src/demo.rs b/src/demo.rs index 7e51370..36c8b46 100644 --- a/src/demo.rs +++ b/src/demo.rs @@ -1,19 +1,17 @@ -use { - crate::{ - types::{ - material::{Dielectric, Lambertian, Metal}, - MovingSphere, Ray, Sphere, Vec3, - }, - BvhNode, Camera, Hitable, HORIZONTAL_PARTITION, VERTICAL_PARTITION, - }, - rand::{rngs::SmallRng, Rng, SeedableRng}, - rayon::prelude::*, - std::{ - fmt::{Display, Formatter, Result as FmtResult}, - fs::File, - io::Write, - sync::{Arc, Mutex}, - }, +use crate::{ + materials::{Dielectric, Lambertian, Metal}, + shapes::{MovingSphere, Sphere}, + texture::Solid, + types::{Ray, Vec3}, + BvhNode, Camera, Hitable, HORIZONTAL_PARTITION, VERTICAL_PARTITION, +}; +use rand::{rngs::SmallRng, Rng, SeedableRng}; +use rayon::prelude::*; +use std::{ + fmt::{Display, Formatter, Result as FmtResult}, + fs::File, + io::Write, + sync::{Arc, Mutex}, }; #[derive(Debug)] @@ -57,7 +55,7 @@ impl Demo { world.push(Arc::new(Sphere::new( Vec3::new(0.0, -1000.0, 0.0), 1000.0, - Lambertian::new(Vec3::new(0.5, 0.5, 0.5)), + Lambertian::new(Solid::new(Vec3::new(0.5, 0.5, 0.5))), ))); let radius = 0.2; @@ -79,11 +77,11 @@ impl Demo { 0.0, 1.0, radius, - Lambertian::new(Vec3::new( + Lambertian::new(Solid::new(Vec3::new( rng.gen::() * rng.gen::(), rng.gen::() * rng.gen::(), rng.gen::() * rng.gen::(), - )), + ))), ))); } else if choose_material_probability < 0.95 { // metal material @@ -115,7 +113,7 @@ impl Demo { world.push(Arc::new(Sphere::new( Vec3::new(-4.0, 1.0, 0.0), 1.0, - Lambertian::new(Vec3::new(0.4, 0.2, 0.1)), + Lambertian::new(Solid::new(Vec3::new(0.4, 0.2, 0.1))), ))); world.push(Arc::new(Sphere::new( Vec3::new(4.0, 1.0, 0.0), diff --git a/src/hitable.rs b/src/hitable.rs index 3de389d..7dba258 100644 --- a/src/hitable.rs +++ b/src/hitable.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use crate::{ - types::{Material, Ray, Vec3}, - Aabb, + types::{Ray, Vec3}, + Aabb, Material, }; pub struct HitRecord<'a> { @@ -24,6 +24,10 @@ pub struct HitRecord<'a> { /// material if any of the surface pub material: &'a dyn Material, + + /// texture coordinates for an object + pub u: f64, + pub v: f64, } pub trait Hitable { diff --git a/src/main.rs b/src/main.rs index 7b859b1..bf3877d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,9 @@ mod camera; mod demo; mod hitable; mod hitable_list; +mod materials; +mod shapes; +mod texture; mod types; pub use camera::Camera; @@ -14,9 +17,11 @@ pub use aabb::Aabb; pub use bvh::BvhNode; pub use hitable::{HitRecord, Hitable}; pub use hitable_list::HitableList; +pub use materials::Material; use std::time::Instant; +pub use texture::Texture; -const NUM_SAMPLES: u8 = 25; +const NUM_SAMPLES: u8 = 20; const VERTICAL_PARTITION: usize = 12; const HORIZONTAL_PARTITION: usize = 12; const WIDTH: usize = 1920; diff --git a/src/materials/dielectric.rs b/src/materials/dielectric.rs new file mode 100644 index 0000000..78743ef --- /dev/null +++ b/src/materials/dielectric.rs @@ -0,0 +1,67 @@ +use rand::{prelude::SmallRng, Rng}; + +use crate::{ + materials::{reflect, refract, schlick}, + types::{Ray, Vec3}, + HitRecord, Material, +}; + +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, + rng: &mut SmallRng, + ) -> (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 (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, ray_in.time())), + ) + } else { + ( + attenuation, + Some(Ray::new(hit_rec.p, refracted_ray, ray_in.time())), + ) + } + } else { + ( + attenuation, + Some(Ray::new(hit_rec.p, reflected_ray, ray_in.time())), + ) + } + } +} diff --git a/src/materials/lambertian.rs b/src/materials/lambertian.rs new file mode 100644 index 0000000..a304011 --- /dev/null +++ b/src/materials/lambertian.rs @@ -0,0 +1,26 @@ +use rand::prelude::SmallRng; + +use crate::{ + materials::random_point_in_unit_sphere, + types::{Ray, Vec3}, + HitRecord, Material, Texture, +}; + +pub struct Lambertian { + albedo: T, +} + +impl Lambertian { + pub fn new(albedo: T) -> Self { + Self { albedo } + } +} + +impl Material for Lambertian { + fn scatter(&self, ray: &Ray, hit_rec: &HitRecord, rng: &mut SmallRng) -> (Vec3, Option) { + let scatter_direction = hit_rec.normal + random_point_in_unit_sphere(rng); + let scattered_ray = Ray::new(hit_rec.p, scatter_direction, ray.time()); + + (self.albedo.value(hit_rec.u, hit_rec.v), Some(scattered_ray)) + } +} diff --git a/src/materials/metal.rs b/src/materials/metal.rs new file mode 100644 index 0000000..a056f1f --- /dev/null +++ b/src/materials/metal.rs @@ -0,0 +1,43 @@ +use rand::prelude::SmallRng; + +use crate::{ + materials::{random_point_in_unit_sphere, reflect}, + types::{Ray, Vec3}, + HitRecord, Material, +}; + +pub struct Metal { + albedo: Vec3, + fuzz: f64, +} + +impl Metal { + pub fn new(albedo: Vec3) -> Self { + 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, + rng: &mut SmallRng, + ) -> (Vec3, Option) { + let reflected_ray = reflect(ray_in.direction().unit_vector(), hit_rec.normal); + let scattered_ray = Ray::new( + hit_rec.p, + reflected_ray + random_point_in_unit_sphere(rng) * self.fuzz, + ray_in.time(), + ); + + if scattered_ray.direction().dot(&hit_rec.normal) > 0.0 { + (self.albedo, Some(scattered_ray)) + } else { + (self.albedo, None) + } + } +} diff --git a/src/materials/mod.rs b/src/materials/mod.rs new file mode 100644 index 0000000..0d22bd6 --- /dev/null +++ b/src/materials/mod.rs @@ -0,0 +1,51 @@ +mod dielectric; +mod lambertian; +mod metal; + +pub use dielectric::Dielectric; +pub use lambertian::Lambertian; +pub use metal::Metal; +use rand::{prelude::SmallRng, Rng}; + +use crate::{ + types::{Ray, Vec3}, + HitRecord, +}; + +pub trait Material: Send + Sync { + fn scatter(&self, ray: &Ray, hit_rec: &HitRecord, rng: &mut SmallRng) -> (Vec3, Option); +} + +// Christophe Schlick's Polynomial approximation to figure out reflectivity as the angle changes +// See Fresnel Equations, https://en.wikipedia.org/wiki/Fresnel_equations +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 R) -> 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/shapes/mod.rs b/src/shapes/mod.rs new file mode 100644 index 0000000..9b309a8 --- /dev/null +++ b/src/shapes/mod.rs @@ -0,0 +1,5 @@ +mod moving_sphere; +mod sphere; + +pub use moving_sphere::MovingSphere; +pub use sphere::Sphere; diff --git a/src/types/moving_sphere.rs b/src/shapes/moving_sphere.rs similarity index 72% rename from src/types/moving_sphere.rs rename to src/shapes/moving_sphere.rs index a866ae6..3bc1d40 100644 --- a/src/types/moving_sphere.rs +++ b/src/shapes/moving_sphere.rs @@ -1,6 +1,6 @@ use crate::{ - types::{Material, Ray, Vec3}, - Aabb, HitRecord, Hitable, + types::{Ray, Vec3}, + Aabb, HitRecord, Hitable, Material, }; pub struct MovingSphere { @@ -36,6 +36,19 @@ impl MovingSphere { + (self.center_end - self.center_start) * ((time - self.time_start) / (self.time_end - self.time_start)) } + + /// p is a point on the sphere of radius 1 & center at origin + /// u is between [0,1]. Angle around Y axis from -X axis + /// v is between [0,1]. Angle from -Y to +Y axis + pub fn get_uv(p: Vec3) -> (f64, f64) { + let theta = (-p.y()).acos(); + let phi = f64::atan2(-p.z(), p.x()) + std::f64::consts::PI; + + let u = phi / (2.0 * std::f64::consts::PI); + let v = theta / std::f64::consts::PI; + + (u, v) + } } impl Hitable for MovingSphere { @@ -52,19 +65,29 @@ impl Hitable for MovingSphere { let root = (-b - discriminant_root) / a; if root < t_max && root > t_min { let p = ray.point_at_parameter(root); + let normal = (p - self.center(ray.time())) / self.radius; + let (u, v) = Self::get_uv(normal); + return Some(HitRecord { t: root, p, - normal: (p - self.center(ray.time())) / self.radius, + u, + v, + normal, material: &self.material, }); } let root = (-b + discriminant_root) / a; if root < t_max && root > t_min { let p = ray.point_at_parameter(root); + let normal = (p - self.center(ray.time())) / self.radius; + let (u, v) = Self::get_uv(normal); + return Some(HitRecord { t: root, p, + u, + v, normal: (p - self.center(ray.time())) / self.radius, material: &self.material, }); diff --git a/src/types/sphere.rs b/src/shapes/sphere.rs similarity index 71% rename from src/types/sphere.rs rename to src/shapes/sphere.rs index a915eba..5be5190 100644 --- a/src/types/sphere.rs +++ b/src/shapes/sphere.rs @@ -1,6 +1,6 @@ use crate::{ - types::{Material, Ray, Vec3}, - Aabb, HitRecord, Hitable, + types::{Ray, Vec3}, + Aabb, HitRecord, Hitable, Material, }; pub struct Sphere { @@ -17,6 +17,19 @@ impl Sphere { material, } } + + /// p is a point on the sphere of radius 1 & center at origin + /// u is between [0,1]. Angle around Y axis from -X axis + /// v is between [0,1]. Angle from -Y to +Y axis + pub fn get_uv(p: Vec3) -> (f64, f64) { + let theta = (-p.y()).acos(); + let phi = f64::atan2(-p.z(), p.x()) + std::f64::consts::PI; + + let u = phi / (2.0 * std::f64::consts::PI); + let v = theta / std::f64::consts::PI; + + (u, v) + } } impl Hitable for Sphere { @@ -39,10 +52,15 @@ impl Hitable for Sphere { let root = (-b - discriminant_root) / a; if root < t_max && root > t_min { let p = ray.point_at_parameter(root); + let normal = (p - self.center) / self.radius; + let (u, v) = Self::get_uv(normal); + return Some(HitRecord { t: root, p, - normal: (p - self.center) / self.radius, + u, + v, + normal, material: &self.material, }); } @@ -50,10 +68,14 @@ impl Hitable for Sphere { let root = (-b + discriminant_root) / a; if root < t_max && root > t_min { let p = ray.point_at_parameter(root); + let normal = (p - self.center) / self.radius; + let (u, v) = Self::get_uv(normal); return Some(HitRecord { t: root, p, + u, + v, normal: (p - self.center) / self.radius, material: &self.material, }); diff --git a/src/texture/mod.rs b/src/texture/mod.rs new file mode 100644 index 0000000..d3ded52 --- /dev/null +++ b/src/texture/mod.rs @@ -0,0 +1,9 @@ +mod solid; + +pub use solid::Solid; + +use crate::types::Vec3; + +pub trait Texture { + fn value(&self, u: f64, v: f64) -> Vec3; +} diff --git a/src/texture/solid.rs b/src/texture/solid.rs new file mode 100644 index 0000000..48d7438 --- /dev/null +++ b/src/texture/solid.rs @@ -0,0 +1,17 @@ +use crate::{types::Vec3, Texture}; + +pub struct Solid { + color: Vec3, +} + +impl Solid { + pub const fn new(color: Vec3) -> Self { + Self { color } + } +} + +impl Texture for Solid { + fn value(&self, _u: f64, _v: f64) -> Vec3 { + self.color + } +} diff --git a/src/types/material.rs b/src/types/material.rs deleted file mode 100644 index 2380508..0000000 --- a/src/types/material.rs +++ /dev/null @@ -1,160 +0,0 @@ -use { - crate::{ - types::{Ray, Vec3}, - HitRecord, - }, - rand::{rngs::SmallRng, Rng}, -}; - -pub trait Material: Send + Sync { - fn scatter(&self, ray: &Ray, hit_rec: &HitRecord, rng: &mut SmallRng) -> (Vec3, Option); -} - -pub struct Lambertian { - albedo: Vec3, -} - -impl Lambertian { - pub fn new(a: Vec3) -> Self { - Self { albedo: a } - } -} - -impl Material for Lambertian { - fn scatter(&self, ray: &Ray, hit_rec: &HitRecord, rng: &mut SmallRng) -> (Vec3, Option) { - let target = hit_rec.p + hit_rec.normal + random_point_in_unit_sphere(rng); - let scattered_ray = Ray::new(hit_rec.p, target - hit_rec.p, ray.time()); - - (self.albedo, Some(scattered_ray)) - } -} - -pub struct Metal { - albedo: Vec3, - fuzz: f64, -} - -impl Metal { - pub fn new(albedo: Vec3) -> Self { - 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, - rng: &mut SmallRng, - ) -> (Vec3, Option) { - let reflected_ray = reflect(ray_in.direction().unit_vector(), hit_rec.normal); - let scattered_ray = Ray::new( - hit_rec.p, - reflected_ray + random_point_in_unit_sphere(rng) * self.fuzz, - ray_in.time(), - ); - - if scattered_ray.direction().dot(&hit_rec.normal) > 0.0 { - (self.albedo, Some(scattered_ray)) - } else { - (self.albedo, None) - } - } -} - -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, - rng: &mut SmallRng, - ) -> (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 (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, ray_in.time())), - ) - } else { - ( - attenuation, - Some(Ray::new(hit_rec.p, refracted_ray, ray_in.time())), - ) - } - } else { - ( - attenuation, - Some(Ray::new(hit_rec.p, reflected_ray, ray_in.time())), - ) - } - } -} - -// Christophe Schlick's Polynomial approximation to figure out reflectivity as the angle changes -// See Fresnel Equations, https://en.wikipedia.org/wiki/Fresnel_equations -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 R) -> 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/mod.rs b/src/types/mod.rs index d05bb9a..d5dd4ac 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,11 +1,5 @@ -pub mod material; -mod moving_sphere; mod ray; -mod sphere; mod vec3; -pub use material::Material; -pub use moving_sphere::MovingSphere; pub use ray::Ray; -pub use sphere::Sphere; pub use vec3::Vec3;