From 35259ad16db87e02c4724537a81e06ed7ea8555c Mon Sep 17 00:00:00 2001 From: ishanjain28 Date: Sat, 14 Mar 2020 23:03:07 +0530 Subject: [PATCH] Added basic structure of the project from ria-weekend project 1. Added Motion Blur Demo --- .gitignore | 3 + Cargo.lock | 267 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 10 ++ README.md | 8 ++ src/camera.rs | 91 +++++++++++++ src/demos/mod.rs | 117 ++++++++++++++++ src/demos/motion_blur.rs | 185 +++++++++++++++++++++++++ src/main.rs | 109 +++++++++++++++ src/types/hitable.rs | 31 +++++ src/types/hitable_list.rs | 25 ++++ src/types/material.rs | 151 +++++++++++++++++++++ src/types/mod.rs | 15 +++ src/types/moving_sphere.rs | 72 ++++++++++ src/types/ray.rs | 29 ++++ src/types/sphere.rs | 61 +++++++++ src/types/vec3.rs | 192 ++++++++++++++++++++++++++ 16 files changed, 1366 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/camera.rs create mode 100644 src/demos/mod.rs create mode 100644 src/demos/motion_blur.rs create mode 100644 src/main.rs create mode 100644 src/types/hitable.rs create mode 100644 src/types/hitable_list.rs create mode 100644 src/types/material.rs create mode 100644 src/types/mod.rs create mode 100644 src/types/moving_sphere.rs create mode 100644 src/types/ray.rs create mode 100644 src/types/sphere.rs create mode 100644 src/types/vec3.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a436ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target/ +*.ppm +*.png diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2bd1a3c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,267 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "crossbeam-deque" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "lazy_static", + "maybe-uninit", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "either" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8" +dependencies = [ + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memoffset" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "num_cpus" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rayon" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098" +dependencies = [ + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" +dependencies = [ + "crossbeam-deque", + "crossbeam-queue", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "rtnw" +version = "0.1.0" +dependencies = [ + "rand", + "rayon", + "sdl2", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sdl2" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f74124048ea86b5cd50236b2443f6f57cf4625a8e8818009b4e50dbb8729a43" +dependencies = [ + "bitflags", + "lazy_static", + "libc", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e1deb61ff274d29fb985017d4611d4004b113676eaa9c06754194caf82094e" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9f661c6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rtnw" +version = "0.1.0" +authors = ["ishanjain28 "] +edition = "2018" + +[dependencies] +sdl2 = "0.33.0" +rand = "0.7.3" +rayon = "1.3.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e9c473 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# Ray Tracing the Next Week + +This is my attempt at Ray Tracing the Next Week book by Peter Shirley. + +# Demo Renders + +![[1] Motion Blur](https://dl.ishanjain.me/images/motion_blur-2000x1000.png) +[1] Motion Blur diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..bed30fe --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,91 @@ +use { + crate::types::{Ray, Vec3}, + rand::Rng, +}; + +pub struct Camera { + origin: Vec3, + horizontal: Vec3, + vertical: Vec3, + lower_left_corner: Vec3, + lens_radius: f32, + + // position vectors + u: Vec3, + v: Vec3, + w: Vec3, + + shutter_open: f32, + shutter_close: f32, +} + +impl Camera { + // vertical_fov is the viewable angle from top->bottom + // look_from is basically camera position + // look_at is the point where camera is looking + // v_up is camera's up vector. i.e. it points upwards from the camera + // orthogonal to look_from - look_at vector + pub fn new( + look_from: Vec3, + look_at: Vec3, + v_up: Vec3, + vertical_fov: f32, + aspect: f32, + aperture: f32, + focus_distance: f32, + shutter_open: f32, + shutter_close: f32, + ) -> Self { + // convert degree to radian + let angle = vertical_fov * std::f32::consts::PI / 180.0; + let half_height = (angle / 2.0).tan(); + let half_width = aspect * half_height; + + let origin = look_from; + let w = (look_from - look_at).unit_vector(); + let u = v_up.cross(&w).unit_vector(); + let v = w.cross(&u); + + let lower_left_corner = origin + - u * focus_distance * half_width + - v * focus_distance * half_height + - w * focus_distance; + let horizontal = u * half_width * focus_distance * 2.0; + let vertical = v * half_height * focus_distance * 2.0; + let lens_radius = aperture / 2.0; + + Self { + lens_radius, + lower_left_corner, + horizontal, + vertical, + origin, + u, + v, + w, + shutter_open, + shutter_close, + } + } + + pub fn get_ray(&self, u: f32, v: f32) -> Ray { + let mut rng = rand::thread_rng(); + let rd = random_in_unit_disk(&mut rng) * self.lens_radius; + let offset = self.u * rd.x() + self.v * rd.y(); + let time = self.shutter_open + rng.gen::() * (self.shutter_close - self.shutter_open); + Ray::new( + self.origin + offset, + self.lower_left_corner + self.horizontal * u + self.vertical * v - self.origin - offset, + time, + ) + } +} + +fn random_in_unit_disk(rng: &mut rand::rngs::ThreadRng) -> Vec3 { + let mut p = Vec3::new(rng.gen::(), rng.gen::(), 0.0) * 2.0 - Vec3::new(1.0, 1.0, 0.0); + + while p.dot(&p) >= 1.0 { + p = Vec3::new(rng.gen::(), rng.gen::(), 0.0) * 2.0 - Vec3::new(1.0, 0.0, 0.0); + } + p +} diff --git a/src/demos/mod.rs b/src/demos/mod.rs new file mode 100644 index 0000000..6caa751 --- /dev/null +++ b/src/demos/mod.rs @@ -0,0 +1,117 @@ +use { + crate::{ + types::{HitableList, Vec3}, + Camera, HORIZONTAL_PARTITION, VERTICAL_PARTITION, + }, + rayon::prelude::*, + std::{ + fs::File, + io::Write, + sync::{Arc, Mutex}, + }, +}; + +mod motion_blur; + +pub use motion_blur::MotionBlur; + +pub struct Chunk { + x: usize, + y: usize, + nx: usize, + ny: usize, + start_x: usize, + start_y: usize, + buffer: Vec, +} + +pub trait Demo: std::marker::Sync { + fn render(&self, buf: &mut Vec, width: usize, height: usize, samples: u8) { + let nx = width / VERTICAL_PARTITION; + let ny = height / HORIZONTAL_PARTITION; + let world = self.world(); + let camera = self.camera(nx as f32 / ny as f32); + + let buf = Arc::new(Mutex::new(buf)); + + (0..VERTICAL_PARTITION).into_par_iter().for_each(|j| { + let buf = buf.clone(); + (0..HORIZONTAL_PARTITION).into_par_iter().for_each(|i| { + let start_y = j * ny; + let start_x = i * nx; + let x = width; + let y = height; + let mut chunk = Chunk { + x, + y, + nx, + ny, + start_x, + start_y, + buffer: vec![0; nx * ny * 4], + }; + self.render_chunk(&mut chunk, camera.as_ref(), world.as_ref(), samples); + + let mut buf = buf.lock().unwrap(); + + let mut temp_offset = 0; + for j in start_y..start_y + ny { + let real_offset = ((y - j - 1) * x + start_x) * 4; + + buf[real_offset..real_offset + nx * 4] + .copy_from_slice(&chunk.buffer[temp_offset..temp_offset + nx * 4]); + + temp_offset += nx * 4; + } + }) + }); + } + + fn world(&self) -> Option { + None + } + + fn camera(&self, aspect_ratio: f32) -> Option { + let lookfrom = Vec3::new(0.0, 0.0, 0.0); + let lookat = Vec3::new(0.0, 0.0, -1.0); + Some(Camera::new( + lookfrom, + lookat, + Vec3::new(0.0, 1.0, 0.0), + 90.0, + aspect_ratio, + 0.0, //aperture + 1.0, // focus_distance + 0.0, // shutter open time + 1.0, // shutter close time + )) + } + + fn render_chunk( + &self, + chunk: &mut Chunk, + camera: Option<&Camera>, + world: Option<&HitableList>, + samples: u8, + ); + + fn name(&self) -> &'static str; + + fn save_as_ppm(&self, buf: &[u8], width: usize, height: usize) { + let header = format!("P3\n{} {}\n255\n", width, height); + + let mut file = match File::create(&format!("{}-{}x{}.ppm", self.name(), width, height)) { + Ok(file) => file, + Err(e) => panic!("couldn't create {}: {}", self.name(), e), + }; + file.write_all(header.as_bytes()) + .expect("error in writing file header"); + + for i in buf.chunks(4) { + match file.write_all(format!("{} {} {}\n", i[0], i[1], i[2]).as_bytes()) { + Ok(_) => (), + Err(e) => panic!("couldn't write to {}: {}", self.name(), e), + } + } + } +} diff --git a/src/demos/motion_blur.rs b/src/demos/motion_blur.rs new file mode 100644 index 0000000..8cf9afa --- /dev/null +++ b/src/demos/motion_blur.rs @@ -0,0 +1,185 @@ +use { + crate::{ + demos::{Chunk, Demo}, + types::{ + material::{Dielectric, Lambertian, Metal}, + Hitable, HitableList, MovingSphere, Ray, Sphere, Vec3, + }, + Camera, + }, + rand::Rng, +}; + +const RANGE: i32 = 10; + +pub struct MotionBlur; + +impl Demo for MotionBlur { + fn name(&self) -> &'static str { + "motion_blur" + } + + fn world(&self) -> Option { + let mut world = HitableList { + list: Vec::with_capacity(500), + }; + + world.push(Box::new(Sphere::new( + Vec3::new(0.0, -1000.0, 0.0), + 1000.0, + Box::new(Lambertian::new(Vec3::new(0.5, 0.5, 0.5))), + ))); + + let mut rng = rand::thread_rng(); + let radius = 0.2; + let l = Vec3::new(4.0, 0.2, 0.0); + + for a in -RANGE..RANGE { + let a = a as f32; + for b in -RANGE..RANGE { + let b = b as f32; + let choose_material_probability = rng.gen::(); + let center = Vec3::new(a + 0.9 * rng.gen::(), 0.2, b + 0.9 * rng.gen::()); + + if (center - l).length() > 0.9 { + if choose_material_probability < 0.8 { + // diffuse material + world.push(Box::new(MovingSphere::new( + center, + center + Vec3::new(0.0, 0.5 * rng.gen::(), 0.0), + 0.0, + 1.0, + radius, + Box::new(Lambertian::new(Vec3::new( + rng.gen::() * rng.gen::(), + rng.gen::() * rng.gen::(), + rng.gen::() * rng.gen::(), + ))), + ))); + } else if choose_material_probability < 0.95 { + // metal material + world.push(Box::new(Sphere::new( + center, + radius, + Box::new(Metal::with_fuzz( + Vec3::new( + (1.0 + rng.gen::()) * 0.5, + (1.0 + rng.gen::()) * 0.5, + (1.0 + rng.gen::()) * 0.5, + ), + 0.5 * rng.gen::(), + )), + ))); + } else { + // glass material + world.push(Box::new(Sphere::new( + center, + radius, + Box::new(Dielectric::new(1.5)), + ))); + } + } + } + } + + world.push(Box::new(Sphere::new( + Vec3::new(0.0, 1.0, 0.0), + 1.0, + Box::new(Dielectric::new(1.5)), + ))); + world.push(Box::new(Sphere::new( + Vec3::new(-4.0, 1.0, 0.0), + 1.0, + Box::new(Lambertian::new(Vec3::new(0.4, 0.2, 0.1))), + ))); + world.push(Box::new(Sphere::new( + Vec3::new(4.0, 1.0, 0.0), + 1.0, + Box::new(Metal::with_fuzz(Vec3::new(0.7, 0.6, 0.5), 0.0)), + ))); + + Some(world) + } + + fn camera(&self, aspect_ratio: f32) -> Option { + let lookfrom = Vec3::new(13.0, 2.0, 3.0); + let lookat = Vec3::new(0.0, 0.0, 0.0); + let aperture = 0.1; + let focus_distance = 10.0; + let camera = Camera::new( + lookfrom, + lookat, + Vec3::new(0.0, 1.0, 0.0), + 20.0, + aspect_ratio, + aperture, + focus_distance, + 0.0, + 1.0, + ); + Some(camera) + } + + fn render_chunk( + &self, + chunk: &mut Chunk, + camera: Option<&Camera>, + world: Option<&HitableList>, + samples: u8, + ) { + let &mut Chunk { + x, + y, + nx, + ny, + start_x, + start_y, + ref mut buffer, + } = chunk; + let camera = camera.unwrap(); + let world = world.unwrap(); + + 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 f32 + rng.gen::()) / x as f32; + let v = (j as f32 + rng.gen::()) / y as f32; + + let ray = camera.get_ray(u, v); + color += calc_color(ray, &world, 0); + } + + color /= samples as f32; + + // 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::f32::MAX) { + if depth >= 50 { + Vec3::new(0.0, 0.0, 0.0) + } else { + let material = hit_rec.material; + if let (attenuation, Some(scattered_ray)) = material.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/main.rs b/src/main.rs new file mode 100644 index 0000000..d52c90b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,109 @@ +#![allow(clippy::suspicious_arithmetic_impl)] +#![feature(test)] +extern crate test; + +mod camera; +mod demos; +mod types; + +pub use camera::Camera; + +use { + demos::Demo, + sdl2::{ + event::{Event, WindowEvent}, + keyboard::Keycode, + pixels::PixelFormatEnum, + }, + std::time::Instant, +}; + +const NUM_SAMPLES: u8 = 100; +const VERTICAL_PARTITION: usize = 8; +const HORIZONTAL_PARTITION: usize = 8; + +fn main() -> Result<(), String> { + let sdl_ctx = sdl2::init()?; + let video_subsys = sdl_ctx.video()?; + let (mut width, mut height): (usize, usize) = (2000, 1000); + + let window = video_subsys + .window("Ray tracing in a weekend", width as u32, height as u32) + .position_centered() + .build() + .map_err(|e| e.to_string())?; + + let mut event_pump = sdl_ctx.event_pump()?; + + let mut canvas = window + .into_canvas() + .target_texture() + .build() + .map_err(|e| e.to_string())?; + + // RGBA framebuffer + let mut buffer = vec![0; height * width * 4]; + + let texture_creator = canvas.texture_creator(); + let mut texture = texture_creator + .create_texture_static(PixelFormatEnum::BGR888, width as u32, height as u32) + .map_err(|e| e.to_string())?; + + //println!("{:?} {:?} {:?}", texture.query(), texture.color_mod(), texture.alpha_mod()); + + let mut active_demo: &dyn Demo = &demos::MotionBlur; + // TODO: Should update when window is unfocus since the project window retains + // data from overlapped window + // TODO: Maybe consider using condition variable to make loop {} not run at full + // speed at all times pinning a core at 100% + let mut should_update = true; + + loop { + for event in event_pump.poll_iter() { + match event { + Event::Quit { .. } + | Event::KeyDown { + keycode: Some(Keycode::Escape), + .. + } => return Ok(()), + Event::KeyUp { keycode, .. } => { + match keycode { + Some(Keycode::S) => { + active_demo.save_as_ppm(&buffer, width, height); + should_update = false; + } + None => unreachable!(), + _ => (), + }; + } + Event::Window { + win_event: WindowEvent::Resized(w, h), + .. + } => { + width = w as usize; + height = h as usize; + buffer.resize(width * height * 4, 0); + texture = texture_creator + .create_texture_static(PixelFormatEnum::BGR888, width as u32, height as u32) + .expect("error in resizing texture"); + should_update = true; + } + _ => {} + }; + } + if should_update { + let now = Instant::now(); + active_demo.render(&mut buffer, width, height, NUM_SAMPLES); + println!( + "Demo {} Time Taken(s) = {}", + active_demo.name(), + now.elapsed().as_secs_f32() + ); + + texture.update(None, &buffer, width * 4).unwrap(); + canvas.copy(&texture, None, None).unwrap(); + canvas.present(); + should_update = false; + } + } +} diff --git a/src/types/hitable.rs b/src/types/hitable.rs new file mode 100644 index 0000000..99935a5 --- /dev/null +++ b/src/types/hitable.rs @@ -0,0 +1,31 @@ +use { + crate::types::Vec3, + crate::types::{Material, Ray}, +}; + +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: f32, + /// 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: &'a Box, +} + +pub trait Hitable: Send + Sync { + fn hit(&self, _ray: &Ray, _t_min: f32, _t_max: f32) -> Option { + None + } +} diff --git a/src/types/hitable_list.rs b/src/types/hitable_list.rs new file mode 100644 index 0000000..cec3490 --- /dev/null +++ b/src/types/hitable_list.rs @@ -0,0 +1,25 @@ +use crate::types::{HitRecord, Hitable, Ray}; + +pub struct HitableList { + pub list: Vec>, +} + +impl Hitable for HitableList { + fn hit(&self, ray: &Ray, t_min: f32, t_max: f32) -> Option { + let mut closest_so_far = t_max; + let mut hit_rec: Option = None; + for obj in &self.list { + if let Some(l_hit_rec) = obj.hit(ray, t_min, closest_so_far) { + closest_so_far = l_hit_rec.t; + hit_rec = Some(l_hit_rec); + } + } + hit_rec + } +} + +impl HitableList { + pub fn push(&mut self, obj: Box) { + self.list.push(obj); + } +} diff --git a/src/types/material.rs b/src/types/material.rs new file mode 100644 index 0000000..2362bdf --- /dev/null +++ b/src/types/material.rs @@ -0,0 +1,151 @@ +use { + crate::types::{HitRecord, Ray, Vec3}, + rand::Rng, +}; + +pub trait Material: Send + Sync { + fn scatter(&self, ray: &Ray, hit_rec: &HitRecord) -> (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) -> (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, ray.time()); + + (self.albedo, Some(scattered_ray)) + } +} + +pub struct Metal { + albedo: Vec3, + fuzz: f32, +} + +impl Metal { + pub fn new(albedo: Vec3) -> Self { + Self { albedo, fuzz: 0.0 } + } + pub fn with_fuzz(albedo: Vec3, fuzz: f32) -> 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 + random_point_in_unit_sphere(&mut 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: f32, +} + +impl Dielectric { + pub fn new(reflection_index: f32) -> 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, 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: f32, reflection_index: f32) -> f32 { + 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: f32) -> 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/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..e864b9e --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,15 @@ +mod hitable; +mod hitable_list; +pub mod material; +mod moving_sphere; +mod ray; +mod sphere; +mod vec3; + +pub use hitable::{HitRecord, Hitable}; +pub use hitable_list::HitableList; +pub use material::Material; +pub use moving_sphere::MovingSphere; +pub use ray::Ray; +pub use sphere::Sphere; +pub use vec3::Vec3; diff --git a/src/types/moving_sphere.rs b/src/types/moving_sphere.rs new file mode 100644 index 0000000..72f2b4f --- /dev/null +++ b/src/types/moving_sphere.rs @@ -0,0 +1,72 @@ +use crate::types::{HitRecord, Hitable, Material, Ray, Vec3}; + +pub struct MovingSphere { + radius: f32, + center_start: Vec3, + center_end: Vec3, + time_start: f32, + time_end: f32, + material: Box, +} + +impl MovingSphere { + pub fn new( + center_start: Vec3, + center_end: Vec3, + time_start: f32, + time_end: f32, + radius: f32, + material: Box, + ) -> Self { + Self { + center_start, + center_end, + time_start, + time_end, + radius, + material, + } + } + + fn center(&self, time: f32) -> Vec3 { + self.center_start + + (self.center_end - self.center_start) + * ((time - self.time_start) / (self.time_end - self.time_start)) + } +} + +impl Hitable for MovingSphere { + fn hit(&self, ray: &Ray, t_min: f32, t_max: f32) -> Option { + let oc = ray.origin() - self.center(ray.time()); + let a = ray.direction().dot(&ray.direction()); + let b = oc.dot(&ray.direction()); + let c = oc.dot(&oc) - self.radius * self.radius; + + let discriminant = b * b - a * c; + let discriminant_root = discriminant.sqrt(); + + if discriminant > 0.0 { + let root = (-b - discriminant_root) / a; + if root < t_max && root > t_min { + let p = ray.point_at_parameter(root); + return Some(HitRecord { + t: root, + p, + normal: (p - self.center(ray.time())) / self.radius, + material: &self.material, + }); + } + let root = (-b + discriminant_root) / a; + if root < t_max && root > t_min { + let p = ray.point_at_parameter(root); + return Some(HitRecord { + t: root, + p, + normal: (p - self.center(ray.time())) / self.radius, + material: &self.material, + }); + } + } + None + } +} diff --git a/src/types/ray.rs b/src/types/ray.rs new file mode 100644 index 0000000..d776020 --- /dev/null +++ b/src/types/ray.rs @@ -0,0 +1,29 @@ +use crate::types::Vec3; + +pub struct Ray { + a: Vec3, + b: Vec3, + time: f32, +} + +impl Ray { + pub fn new(a: Vec3, b: Vec3, time: f32) -> Ray { + Ray { a, b, time } + } + #[inline] + pub const fn origin(&self) -> Vec3 { + self.a + } + #[inline] + pub const fn direction(&self) -> Vec3 { + self.b + } + #[inline] + pub fn point_at_parameter(&self, t: f32) -> Vec3 { + self.a + self.b * t + } + #[inline] + pub const fn time(&self) -> f32 { + self.time + } +} diff --git a/src/types/sphere.rs b/src/types/sphere.rs new file mode 100644 index 0000000..e799943 --- /dev/null +++ b/src/types/sphere.rs @@ -0,0 +1,61 @@ +use crate::types::{HitRecord, Hitable, Material, Ray, Vec3}; + +pub struct Sphere { + center: Vec3, + radius: f32, + material: Box, +} + +impl Sphere { + pub fn new(center: Vec3, radius: f32, material: Box) -> Self { + Self { + center, + radius, + material, + } + } +} + +impl Hitable for Sphere { + fn hit(&self, ray: &Ray, t_min: f32, t_max: f32) -> Option { + let oc = ray.origin() - self.center; + let a = ray.direction().dot(&ray.direction()); + let b = oc.dot(&ray.direction()); + let c = oc.dot(&oc) - self.radius * self.radius; + + // The discriminant is calculated using b^2 - 4 * a * c + // but in this specific case, If we put the equation in the + // formula to find quadratic roots, We can get this shorter + // formula to find the discriminant. + // 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_root) / a; + if root < t_max && root > t_min { + let p = ray.point_at_parameter(root); + return Some(HitRecord { + t: root, + p, + normal: (p - self.center) / self.radius, + material: &self.material, + }); + } + + let root = (-b + discriminant_root) / a; + if root < t_max && root > t_min { + let p = ray.point_at_parameter(root); + + return Some(HitRecord { + t: root, + p, + normal: (p - self.center) / self.radius, + material: &self.material, + }); + } + } + None + } +} diff --git a/src/types/vec3.rs b/src/types/vec3.rs new file mode 100644 index 0000000..163eba0 --- /dev/null +++ b/src/types/vec3.rs @@ -0,0 +1,192 @@ +use std::{ + fmt::{Display, Formatter, Result as FmtResult}, + ops::{Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign}, +}; + +#[derive(Copy, Clone)] +pub struct Vec3([f32; 3]); + +impl Vec3 { + #[inline] + pub const fn new(a: f32, b: f32, c: f32) -> Vec3 { + Vec3([a, b, c]) + } + #[inline] + pub fn x(&self) -> f32 { + self[0] + } + #[inline] + pub fn y(&self) -> f32 { + self[1] + } + #[inline] + pub fn z(&self) -> f32 { + self[2] + } + #[inline] + pub fn r(&self) -> f32 { + self[0] + } + #[inline] + pub fn g(&self) -> f32 { + self[1] + } + #[inline] + pub fn b(&self) -> f32 { + self[2] + } + + #[inline] + pub fn length(&self) -> f32 { + self.sq_len().sqrt() + } + + #[inline] + pub fn sq_len(&self) -> f32 { + self[0] * self[0] + self[1] * self[1] + self[2] * self[2] + } + + #[inline] + pub fn dot(&self, v: &Vec3) -> f32 { + self[0] * v[0] + self[1] * v[1] + self[2] * v[2] + } + + #[inline] + pub fn cross(&self, v: &Vec3) -> Vec3 { + Vec3([ + self[1] * v[2] - self[2] * v[1], + self[2] * v[0] - self[0] * v[2], + self[0] * v[1] - self[1] * v[0], + ]) + } + + #[inline] + pub fn make_unit_vector(&mut self) { + let k = 1.0f32 / (self[0] * self[0] + self[1] * self[1] + self[2] * self[2]); + self[0] *= k; + self[1] *= k; + self[2] *= k; + } + + #[inline] + pub fn unit_vector(&self) -> Vec3 { + let length = self.length(); + Vec3([self[0] / length, self[1] / length, self[2] / length]) + } +} + +impl Add for Vec3 { + type Output = Vec3; + + fn add(self, o: Vec3) -> Vec3 { + Vec3([self[0] + o[0], self[1] + o[1], self[2] + o[2]]) + } +} + +impl AddAssign for Vec3 { + fn add_assign(&mut self, o: Vec3) { + self.0[0] += o.0[0]; + self.0[1] += o.0[1]; + self.0[2] += o.0[2]; + } +} + +impl Sub for Vec3 { + type Output = Vec3; + + fn sub(self, o: Vec3) -> Vec3 { + Vec3([self[0] - o[0], self[1] - o[1], self[2] - o[2]]) + } +} + +impl SubAssign for Vec3 { + fn sub_assign(&mut self, o: Vec3) { + self[0] -= o[0]; + self[1] -= o[1]; + self[2] -= o[2]; + } +} + +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]; + self[1] *= o[1]; + self[2] *= o[2]; + } +} + +impl MulAssign for Vec3 { + fn mul_assign(&mut self, o: f32) { + self[0] *= o; + self[1] *= o; + self[2] *= o; + } +} + +impl Mul for Vec3 { + type Output = Vec3; + fn mul(self, o: f32) -> Vec3 { + Vec3([self[0] * o, self[1] * o, self[2] * o]) + } +} + +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; + + fn div(self, o: Vec3) -> Vec3 { + Vec3([self[0] / o[0], self[1] / o[1], self[2] / o[2]]) + } +} + +impl Div for Vec3 { + type Output = Vec3; + + fn div(self, o: f32) -> Vec3 { + let o = 1.0 / o; + Vec3([self[0] * o, self[1] * o, self[2] * o]) + } +} + +impl DivAssign for Vec3 { + fn div_assign(&mut self, o: f32) { + let o = 1.0 / o; + self.0[0] *= o; + self.0[1] *= o; + self.0[2] *= o; + } +} + +impl Index for Vec3 { + type Output = f32; + + fn index(&self, q: usize) -> &f32 { + &self.0[q] + } +} + +impl IndexMut for Vec3 { + fn index_mut(&mut self, q: usize) -> &mut f32 { + &mut self.0[q] + } +} + +impl Display for Vec3 { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + f.write_fmt(format_args!("{} {} {}", self[0], self[1], self[2])) + } +}