From a6302805d19273c95278c8d792ffbd9b2633fe20 Mon Sep 17 00:00:00 2001 From: Sakarias Johansson Date: Thu, 12 Jan 2023 21:09:43 +0100 Subject: =?UTF-8?q?=F0=9F=96=8C=EF=B8=8F=20Add=20materials?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- racer-tracer/src/geometry.rs | 21 ++-- racer-tracer/src/geometry/sphere.rs | 8 +- racer-tracer/src/main.rs | 176 +++++++++++++++++++++++++------- racer-tracer/src/material.rs | 10 ++ racer-tracer/src/material/lambertian.rs | 33 ++++++ racer-tracer/src/material/metal.rs | 33 ++++++ racer-tracer/src/util.rs | 3 +- racer-tracer/src/vec3.rs | 58 +++++++++++ 8 files changed, 285 insertions(+), 57 deletions(-) create mode 100644 racer-tracer/src/material.rs create mode 100644 racer-tracer/src/material/lambertian.rs create mode 100644 racer-tracer/src/material/metal.rs diff --git a/racer-tracer/src/geometry.rs b/racer-tracer/src/geometry.rs index 8d86353..1cc7e28 100644 --- a/racer-tracer/src/geometry.rs +++ b/racer-tracer/src/geometry.rs @@ -1,5 +1,8 @@ pub mod sphere; +use std::sync::Arc; + +use crate::material::Material; use crate::ray::Ray; use crate::vec3::Vec3; @@ -8,17 +11,17 @@ pub struct HitRecord { pub normal: Vec3, pub t: f64, pub front_face: bool, - pub color: Vec3, + pub material: Arc>, } impl HitRecord { - fn new(point: Vec3, t: f64, color: Vec3) -> Self { + fn new(point: Vec3, t: f64, material: Arc>) -> Self { Self { point, normal: Vec3::default(), t, front_face: true, - color, + material, } } @@ -32,18 +35,6 @@ impl HitRecord { } } -impl Default for HitRecord { - fn default() -> Self { - HitRecord { - point: Vec3::default(), - normal: Vec3::default(), - t: 0.0, - front_face: true, - color: Vec3::default(), - } - } -} - pub trait Hittable { //pub trait Hittable { fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option; diff --git a/racer-tracer/src/geometry/sphere.rs b/racer-tracer/src/geometry/sphere.rs index e974c44..256abec 100644 --- a/racer-tracer/src/geometry/sphere.rs +++ b/racer-tracer/src/geometry/sphere.rs @@ -1,17 +1,19 @@ use std::option::Option; +use std::sync::Arc; use crate::geometry::{HitRecord, Hittable}; +use crate::material::Material; use crate::ray::Ray; use crate::vec3::Vec3; pub struct Sphere { pos: Vec3, radius: f64, - material: Vec3, // Just a color for now. + material: Arc>, // Just a color for now. } impl Sphere { - pub fn new(pos: Vec3, radius: f64, material: Vec3) -> Self { + pub fn new(pos: Vec3, radius: f64, material: Arc>) -> Self { Self { pos, radius, @@ -47,7 +49,7 @@ impl Hittable for Sphere { let point = ray.at(root); let outward_normal = (point - self.pos) / self.radius; - let mut hit_record = HitRecord::new(point, root, self.material); + let mut hit_record = HitRecord::new(point, root, Arc::clone(&self.material)); hit_record.set_face_normal(ray, outward_normal); Some(hit_record) } diff --git a/racer-tracer/src/main.rs b/racer-tracer/src/main.rs index c9ed169..508c22a 100644 --- a/racer-tracer/src/main.rs +++ b/racer-tracer/src/main.rs @@ -3,6 +3,7 @@ mod error; mod camera; mod geometry; mod image; +mod material; mod ray; mod scene; mod util; @@ -11,26 +12,43 @@ mod vec3; use std::{ borrow::Borrow, sync::{Arc, Mutex, RwLock}, + time::{Duration, Instant}, vec::Vec, }; -use geometry::Hittable; +use material::{lambertian::Lambertian, metal::Metal, Material}; use minifb::{Key, Window, WindowOptions}; use rayon::prelude::*; +use vec3::Color; -use crate::camera::Camera; -use crate::error::TracerError; -use crate::geometry::sphere::Sphere; -use crate::image::Image; -use crate::ray::Ray; -use crate::scene::Scene; -use crate::util::random_double; -use crate::vec3::Vec3; - -fn ray_color(scene: &dyn Hittable, ray: &Ray) -> Vec3 { - if let Some(hit_record) = scene.hit(ray, 0.0, std::f64::INFINITY) { +use crate::{ + camera::Camera, + error::TracerError, + geometry::sphere::Sphere, + geometry::Hittable, + image::Image, + ray::Ray, + scene::Scene, + util::random_double, + vec3::Vec3, + vec3::{random_in_hemisphere, random_unit_vector}, +}; + +fn ray_color(scene: &dyn Hittable, ray: &Ray, depth: usize) -> Vec3 { + if depth == 0 { + return Vec3::default(); + } + + if let Some(rec) = scene.hit(ray, 0.001, std::f64::INFINITY) { + if let Some((scattered, attenuation)) = rec.material.scatter(ray, &rec) { + return attenuation * ray_color(scene, &scattered, depth - 1); + } + return Color::default(); + //let target = rec.point + random_in_hemisphere(&rec.normal); + //let target = rec.point + rec.normal + random_unit_vector(); + //return 0.5 * ray_color(scene, &Ray::new(rec.point, target - rec.point), depth - 1); //return hit_record.color; - return 0.5 * (hit_record.normal + Vec3::new(1.0, 1.0, 1.0)); + //return 0.5 * (hit_record.normal + Vec3::new(1.0, 1.0, 1.0)); } // TODO: make sky part of scene. @@ -40,23 +58,70 @@ fn ray_color(scene: &dyn Hittable, ray: &Ray) -> Vec3 { (1.0 - t) * Vec3::new(1.0, 1.0, 1.0) + t * Vec3::new(0.5, 0.7, 1.0) } -fn raytrace(scene: &dyn Hittable, camera: &Camera, image: &Image, row: usize) -> Vec { +fn flush( + row: usize, + width: usize, + start: usize, + count: usize, + samples: usize, + source: &[Vec3], + dest: &RwLock>, +) { + let mut buf = dest + .write() + .expect("Failed to get write guard when flushing data."); + + for i in 0..count { + buf[row * width + start + i] = source[start + i].scale_sqrt(samples).as_color(); + } +} + +fn raytrace( + buffer: &RwLock>, + scene: &dyn Hittable, + camera: &Camera, + image: &Image, + row: usize, + max_depth: usize, +) { + let mut now = Instant::now(); + let flush_delay = Duration::from_millis(50 + ((random_double() * 2000.0) as u64)); + let mut flush_start: usize = 0; let mut colors: Vec = vec![Vec3::default(); image.width as usize]; - colors.iter_mut().enumerate().for_each(|(i, col)| { + + for i in 0..colors.len() { for _ in 0..image.samples_per_pixel { let u: f64 = (i as f64 + random_double()) / (image.width - 1) as f64; let v: f64 = (row as f64 + random_double()) / (image.height - 1) as f64; - *col += ray_color(scene, &camera.get_ray(u, v)); + colors[i] += ray_color(scene, &camera.get_ray(u, v), max_depth); } - }); - // TODO: Could do rolling average - let mut buffer: Vec = vec![0; image.width as usize]; - for i in 0..image.width { - buffer[i] = (colors[i] / image.samples_per_pixel as f64).as_color(); + // Update the screen buffer every now and again. + if now.elapsed() > flush_delay { + now = Instant::now(); + flush( + row, + image.width, + flush_start, + i - flush_start, + image.samples_per_pixel, + colors.as_slice(), + buffer, + ); + flush_start = i; + } } - buffer + // Flush last part of the buffer. + flush( + row, + image.width, + flush_start, + colors.len() - flush_start, + image.samples_per_pixel, + colors.as_slice(), + buffer, + ); } type Data = (Arc>>, Arc, Arc, usize); @@ -66,9 +131,9 @@ fn render( camera: Arc, image: Arc, scene: Box, + max_depth: usize, ) { let scene: &(dyn Hittable + Sync) = scene.borrow(); - let v: Vec = (0..image.height) .map(|row| { ( @@ -81,28 +146,65 @@ fn render( .collect(); v.par_iter().for_each(|(buf, camera, image, row)| { - let start = row * image.width; - let end = start + image.width; - let col_buf = raytrace(scene.borrow(), camera, image, *row); - let mut buf = buf.write().expect("Failed to get screen buffer lock."); // TODO: No except - buf[start..end].copy_from_slice(col_buf.as_slice()); + raytrace(buf, scene, camera, image, *row, max_depth); }); } +type SharedMaterial = Arc>; fn create_scene() -> Scene { let mut scene = Scene::new(); - let sphere1 = Sphere::new(Vec3::new(0.0, 0.0, -1.0), 0.5, Vec3::new(0.0, 1.0, 0.0)); - let sphere2 = Sphere::new( + let material_ground: SharedMaterial = + Arc::new(Box::new(Lambertian::new(Color::new(0.8, 0.8, 0.0)))); + let material_center: SharedMaterial = + Arc::new(Box::new(Lambertian::new(Color::new(0.7, 0.3, 0.3)))); + let material_left: SharedMaterial = + Arc::new(Box::new(Metal::new(Color::new(0.8, 0.8, 0.8), 0.3))); + let material_right: SharedMaterial = + Arc::new(Box::new(Metal::new(Color::new(0.8, 0.6, 0.2), 0.1))); + + scene.add(Box::new(Sphere::new( Vec3::new(0.0, -100.5, -1.0), 100.0, - Vec3::new(0.0, 1.0, 0.0), - ); + Arc::clone(&material_ground), + ))); + scene.add(Box::new(Sphere::new( + Vec3::new(0.0, 0.0, -1.0), + 0.5, + Arc::clone(&material_center), + ))); + scene.add(Box::new(Sphere::new( + Vec3::new(-1.0, 0.0, -1.0), + 0.5, + Arc::clone(&material_left), + ))); + scene.add(Box::new(Sphere::new( + Vec3::new(1.0, 0.0, -1.0), + 0.5, + Arc::clone(&material_right), + ))); + scene + // Materials + /* let red: Arc> = + Arc::new(Box::new(Lambertian::new(Color::new(1.0, 0.0, 0.0)))); + let green: Arc> = + Arc::new(Box::new(Lambertian::new(Color::new(0.0, 1.0, 0.0)))); + let metal_red: Arc> = + Arc::new(Box::new(Metal::new(Color::new(1.0, 0.0, 0.0)))); + + // Geometry + let sphere1 = Sphere::new(Vec3::new(0.0, 0.0, -1.0), 0.5, Arc::clone(&metal_red)); + let sphere2 = Sphere::new(Vec3::new(0.0, -100.5, -1.0), 100.0, Arc::clone(&green)); scene.add(Box::new(sphere1)); scene.add(Box::new(sphere2)); - scene + scene*/ } -fn run(aspect_ratio: f64, screen_width: usize, samples: usize) -> Result<(), TracerError> { +fn run( + aspect_ratio: f64, + screen_width: usize, + samples: usize, + max_depth: usize, +) -> Result<(), TracerError> { let image = Arc::new(image::Image::new(aspect_ratio, screen_width, samples)); let camera = Arc::new(camera::Camera::new(&image, 2.0, 1.0)); let scene: Box = Box::new(create_scene()); @@ -118,6 +220,7 @@ fn run(aspect_ratio: f64, screen_width: usize, samples: usize) -> Result<(), Tra camera, Arc::clone(&image), scene, + max_depth, ) }); s.spawn(|_| { @@ -133,10 +236,9 @@ fn run(aspect_ratio: f64, screen_width: usize, samples: usize) -> Result<(), Tra window }) .and_then(|mut window| { - // TODO: Only re-render window then buffer is changed while window.is_open() && !window.is_key_down(Key::Escape) { // Sleep a bit to not hog the lock on the buffer all the time. - std::thread::sleep(std::time::Duration::from_secs(1)); + std::thread::sleep(std::time::Duration::from_millis(100)); screen_buffer .read() @@ -162,7 +264,7 @@ fn run(aspect_ratio: f64, screen_width: usize, samples: usize) -> Result<(), Tra } fn main() { - if let Err(e) = run(16.0 / 9.0, 1200, 10) { + if let Err(e) = run(16.0 / 9.0, 1200, 1000, 50) { eprintln!("{}", e); std::process::exit(e.into()) } diff --git a/racer-tracer/src/material.rs b/racer-tracer/src/material.rs new file mode 100644 index 0000000..13c29d7 --- /dev/null +++ b/racer-tracer/src/material.rs @@ -0,0 +1,10 @@ +pub mod lambertian; +pub mod metal; + +use crate::geometry::HitRecord; +use crate::ray::Ray; +use crate::vec3::Color; + +pub trait Material { + fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Ray, Color)>; +} diff --git a/racer-tracer/src/material/lambertian.rs b/racer-tracer/src/material/lambertian.rs new file mode 100644 index 0000000..8356685 --- /dev/null +++ b/racer-tracer/src/material/lambertian.rs @@ -0,0 +1,33 @@ +use crate::{ + material::Material, + ray::Ray, + vec3::{random_unit_vector, Color, Vec3}, +}; + +pub struct Lambertian { + color: Color, +} + +impl Lambertian { + pub fn new(color: Color) -> Self { + Self { color } + } +} + +impl Material for Lambertian { + fn scatter( + &self, + ray: &crate::ray::Ray, + rec: &crate::geometry::HitRecord, + ) -> Option<(Ray, Color)> { + let mut scatter_direction = rec.normal + random_unit_vector(); + + // Catch bad scatter direction + if scatter_direction.near_zero() { + scatter_direction = rec.normal; + } + + let scattered = Ray::new(rec.point, scatter_direction); + Some((scattered, self.color)) + } +} diff --git a/racer-tracer/src/material/metal.rs b/racer-tracer/src/material/metal.rs new file mode 100644 index 0000000..162342d --- /dev/null +++ b/racer-tracer/src/material/metal.rs @@ -0,0 +1,33 @@ +use crate::{ + material::Material, + ray::Ray, + vec3::{random_in_unit_sphere, reflect, Color}, +}; + +pub struct Metal { + color: Color, + fuzz: f64, +} + +impl Metal { + pub fn new(color: Color, fuzz: f64) -> Self { + Self { color, fuzz } + } +} + +impl Material for Metal { + fn scatter( + &self, + ray: &crate::ray::Ray, + rec: &crate::geometry::HitRecord, + ) -> Option<(Ray, Color)> { + let reflected = reflect(&ray.direction().unit_vector(), &rec.normal); + let scattered = Ray::new(rec.point, reflected + self.fuzz * random_in_unit_sphere()); + + if scattered.direction().dot(&rec.normal) < 0.0 { + None + } else { + Some((scattered, self.color)) + } + } +} diff --git a/racer-tracer/src/util.rs b/racer-tracer/src/util.rs index 196b82d..dc39e1c 100644 --- a/racer-tracer/src/util.rs +++ b/racer-tracer/src/util.rs @@ -10,9 +10,8 @@ pub fn random_double() -> f64 { rng.gen::() } -/* // For later use pub fn random_double_range(min: f64, max: f64) -> f64 { let mut rng = rand::thread_rng(); rng.gen_range(min..max) -}*/ +} diff --git a/racer-tracer/src/vec3.rs b/racer-tracer/src/vec3.rs index 10f3bfa..e092754 100644 --- a/racer-tracer/src/vec3.rs +++ b/racer-tracer/src/vec3.rs @@ -1,10 +1,14 @@ use std::{fmt, ops}; +use crate::util::{random_double, random_double_range}; + #[derive(Default, Clone, Copy)] pub struct Vec3 { data: [f64; 3], } +pub type Color = Vec3; + impl Vec3 { pub fn new(x: f64, y: f64, z: f64) -> Vec3 { Vec3 { data: [x, y, z] } @@ -48,6 +52,60 @@ impl Vec3 { let blue: u32 = (self.data[2] * 255.0) as u32; ((red as u32) << 16) | ((green as u32) << 8) | blue as u32 } + + pub fn random() -> Self { + Vec3 { + data: [random_double(), random_double(), random_double()], + } + } + + pub fn random_range(min: f64, max: f64) -> Self { + Vec3 { + data: [ + random_double_range(min, max), + random_double_range(min, max), + random_double_range(min, max), + ], + } + } + + pub fn scale_sqrt(mut self, samples: usize) -> Vec3 { + let scale = 1.0 / samples as f64; + self.data[0] = (scale * self.data[0]).sqrt(); + self.data[1] = (scale * self.data[1]).sqrt(); + self.data[2] = (scale * self.data[2]).sqrt(); + self + } + + pub fn near_zero(&self) -> bool { + let s = 1e-8; + self.data[0].abs() < s && self.data[1].abs() < s && self.data[2].abs() < s + } +} + +pub fn reflect(v1: &Vec3, v2: &Vec3) -> Vec3 { + v1 - 2.0 * v1.dot(v2) * v2 +} + +pub fn random_in_unit_sphere() -> Vec3 { + let mut v = Vec3::random_range(-1.0, 1.0); + while v.length_squared() >= 1.0 { + v = Vec3::random_range(-1.0, 1.0); + } + v +} + +pub fn random_in_hemisphere(normal: &Vec3) -> Vec3 { + let unit_sphere = random_in_unit_sphere(); + if unit_sphere.dot(normal) > 0.0 { + unit_sphere + } else { + -unit_sphere + } +} + +pub fn random_unit_vector() -> Vec3 { + random_in_unit_sphere().unit_vector() } pub fn dot(v1: &Vec3, v2: &Vec3) -> f64 { -- cgit v1.2.3