diff --git a/.gitignore b/.gitignore index ea8c4bf..fda8fe0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -/target +target/ +*.ppm +*.png +!renders/* diff --git a/Cargo.lock b/Cargo.lock index eed3a4c..595883f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,42 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cfg-if" version = "1.0.0" @@ -9,10 +45,102 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "getrandom" -version = "0.2.3" +name = "cmake" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", @@ -20,10 +148,134 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.107" +name = "hermit-abi" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "image" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" + +[[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.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" + +[[package]] +name = "libm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" + +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "packed_simd" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf965495ba3190af4c54375cb7d5d4777954457a215035a783f9aec5380b19f" +dependencies = [ + "cfg-if", + "libm", +] + +[[package]] +name = "png" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] [[package]] name = "ppv-lite86" @@ -72,14 +324,83 @@ dependencies = [ ] [[package]] -name = "raytracing-the-rest-of-your-life" -version = "0.1.0" +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" dependencies = [ - "rand", + "either", + "rayon-core", ] [[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +name = "rayon-core" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "raytracing-the-rest-of-your-life" +version = "0.1.0" +dependencies = [ + "image", + "num-traits", + "packed_simd", + "rand", + "rayon", + "sdl2", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sdl2" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a" +dependencies = [ + "bitflags", + "lazy_static", + "libc", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0" +dependencies = [ + "cfg-if", + "cmake", + "libc", + "version-compare", +] + +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/Cargo.toml b/Cargo.toml index 4ef7614..2166e81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,13 @@ version = "0.1.0" edition = "2021" [dependencies] -rand = "0.8.4" +image = { version = "0.24.6", default-features = false, features = ["jpeg", "png"] } +num-traits = "0.2.15" +packed_simd = "0.3.8" +rand = { version = "0.8.4", features = ["small_rng"] } +rayon = "1.7.0" +sdl2 = { version = "0.35.2", features = ["bundled"], optional = true } + +[features] +default = ["gui"] +gui = ["sdl2"] diff --git a/assets/earthmap.jpg b/assets/earthmap.jpg new file mode 100644 index 0000000..908c160 Binary files /dev/null and b/assets/earthmap.jpg differ diff --git a/src/aabb.rs b/src/aabb.rs new file mode 100644 index 0000000..52b4b33 --- /dev/null +++ b/src/aabb.rs @@ -0,0 +1,36 @@ +use crate::types::{Ray, Vec3}; + +#[derive(Debug, Copy, Clone)] +pub struct Aabb { + pub min: Vec3, + pub max: Vec3, +} + +impl Aabb { + pub const fn new(min: Vec3, max: Vec3) -> Self { + Self { min, max } + } + + pub fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> bool { + let min = (self.min - ray.origin) / ray.direction; + let max = (self.max - ray.origin) / ray.direction; + + let mins = min.min(max); + let maxs = min.max(max); + + let tmin = mins.max_element(t_min); + let tmax = maxs.min_element(t_max); + + tmax > tmin + } + + pub fn surrounding_box(box0: Aabb, box1: Aabb) -> Self { + let smol_box = Vec3::min(box0.min, box1.min); + let big_box = Vec3::max(box0.max, box1.max); + + Self { + min: smol_box, + max: big_box, + } + } +} diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..8a61ce6 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,90 @@ +use { + crate::types::{Ray, Vec3}, + rand::Rng, +}; + +pub struct Camera { + origin: Vec3, + horizontal: Vec3, + vertical: Vec3, + lower_left_corner: Vec3, + lens_radius: f64, + + // position vectors + u: Vec3, + v: Vec3, + w: Vec3, + + shutter_open: f64, + shutter_close: f64, +} + +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: f64, + aspect: f64, + aperture: f64, + focus_distance: f64, + shutter_open: f64, + shutter_close: f64, + ) -> Self { + // convert degree to radian + let angle = vertical_fov * std::f64::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 { + origin, + horizontal, + vertical, + lower_left_corner, + lens_radius, + u, + v, + w, + shutter_open, + shutter_close, + } + } + + pub fn get_ray(&self, u: f64, v: f64, rng: &mut R) -> Ray { + let rd = random_in_unit_disk(rng) * self.lens_radius; + let offset = self.u * rd.x() + self.v * rd.y(); + let time = rng.gen_range(self.shutter_open..=self.shutter_close); + 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 R) -> 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/checkered_motion_blur.rs b/src/demos/checkered_motion_blur.rs new file mode 100644 index 0000000..83330ea --- /dev/null +++ b/src/demos/checkered_motion_blur.rs @@ -0,0 +1,126 @@ +use crate::{ + demos::{Demo, ParallelHit}, + hitable::{ + shapes::{MovingSphere, Sphere}, + BvhNode, + }, + materials::{Dielectric, Lambertian, Metal}, + texture::{Checker, Solid}, + types::Vec3, + Camera, +}; +use rand::{rngs::SmallRng, Rng, SeedableRng}; +use std::sync::Arc; + +pub struct CheckeredMotionBlur {} + +impl Demo for CheckeredMotionBlur { + type DemoT = BvhNode>; + + fn name(&self) -> &'static str { + "checkered_motion_blur" + } + + fn get_background(&self) -> Vec3 { + Vec3::new(0.7, 0.8, 1.0) + } + + fn world(&self) -> Self::DemoT { + let mut world: Vec> = Vec::with_capacity(500); + + let mut rng = rand::thread_rng(); + let mut rng = SmallRng::from_rng(&mut rng).unwrap(); + + world.push(Arc::new(Sphere::new( + Vec3::new(0.0, -1000.0, 0.0), + 1000.0, + Lambertian::new(Checker::new( + Solid::new(Vec3::new(0.2, 0.3, 0.1)), + Solid::new(Vec3::new(0.9, 0.9, 0.9)), + )), + ))); + + let radius = 0.2; + let l = Vec3::new(4.0, 0.2, 0.0); + + for a in -10..10 { + let a = a as f64; + for b in -10..10 { + let b = b as f64; + 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(Arc::new(MovingSphere::new( + center, + center + Vec3::new(0.0, 0.5 * rng.gen::(), 0.0), + 0.0, + 1.0, + radius, + 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 + world.push(Arc::new(Sphere::new( + center, + radius, + 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(Arc::new(Sphere::new(center, radius, Dielectric::new(1.5)))); + } + } + } + } + + world.push(Arc::new(Sphere::new( + Vec3::new(0.0, 1.0, 0.0), + 1.0, + Dielectric::new(1.5), + ))); + world.push(Arc::new(Sphere::new( + Vec3::new(-4.0, 1.0, 0.0), + 1.0, + 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), + 1.0, + Metal::with_fuzz(Vec3::new(0.7, 0.6, 0.5), 0.0), + ))); + + BvhNode::new(&mut rng, &mut world, 0.0, 1.0) + } + + fn camera(&self, aspect_ratio: f64) -> Camera { + 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; + Camera::new( + lookfrom, + lookat, + Vec3::new(0.0, 1.0, 0.0), + 20.0, + aspect_ratio, + aperture, + focus_distance, + 0.0, + 1.0, + ) + } +} diff --git a/src/demos/cornell_box.rs b/src/demos/cornell_box.rs new file mode 100644 index 0000000..dd3c232 --- /dev/null +++ b/src/demos/cornell_box.rs @@ -0,0 +1,161 @@ +use std::sync::Arc; + +use rand::{prelude::SmallRng, Rng, SeedableRng}; + +use crate::{ + demos::Demo, + hitable::{ + hitable_list::HitableList, + shapes::{Cuboid, MovingSphere, RectBuilder, Sphere}, + volume::ConstantMedium, + Hitable, + }, + materials::{Dielectric, DiffuseLight, Isotropic, Lambertian, MaterialBuilder, Metal}, + texture::{ImageTexture, PerlinNoise, Solid}, + types::Vec3, + BvhNode, Camera, +}; + +pub struct CornellBox {} + +impl Demo for CornellBox { + type DemoT = HitableList; + + fn name(&self) -> &'static str { + "cornell_box" + } + + fn world(&self) -> Self::DemoT { + let mut rng = rand::thread_rng(); + let mut rng = SmallRng::from_rng(&mut rng).unwrap(); + + let mut ground_boxes = HitableList { list: Vec::new() }; + let ground = Lambertian::new(Solid::new(Vec3::new(0.48, 0.83, 0.53))); + + for i in 0..20 { + let i = i as f64; + for j in 0..20 { + let j = j as f64; + + let w = 100.0; + let x0 = -1000.0 + i * w; + let z0 = -1000.0 + j * w; + let y0 = 0.0; + + let x1 = x0 + w; + let y1 = rng.gen_range(1.0..=101.0); + let z1 = z0 + w; + + ground_boxes.push(Arc::new(Cuboid::new( + Vec3::new(x0, y0, z0), + Vec3::new(x1, y1, z1), + ground.clone(), + ))); + } + } + + let mut objects = HitableList { list: Vec::new() }; + objects.push(Arc::new(BvhNode::new( + &mut rng, + &mut ground_boxes.list, + 0.0, + 1.0, + ))); + + let light = DiffuseLight::new(Solid::new(Vec3::splat(7.0))); + objects.push(Arc::new( + RectBuilder + .x(123.0..=423.0) + .z(147.0..=412.0) + .y(554.0) + .material(light), + )); + + let center1 = Vec3::new(400.0, 400.0, 200.0); + let center2 = center1 + Vec3::new(30.0, 0.0, 0.0); + objects.push(Arc::new(MovingSphere::new( + center1, + center2, + 0.0, + 1.0, + 50.0, + Lambertian::new(Solid::new(Vec3::new(0.7, 0.3, 0.1))), + ))); + + objects.push(Arc::new(Sphere::new( + Vec3::new(260.0, 150.0, 45.0), + 50.0, + Dielectric::new(1.5), + ))); + + objects.push(Arc::new(Sphere::new( + Vec3::new(0.0, 150.0, 145.0), + 50.0, + Metal::with_fuzz(Vec3::new(0.8, 0.8, 0.9), 1.0), + ))); + + let boundary = Sphere::new(Vec3::new(360.0, 150.0, 145.0), 70.0, Dielectric::new(1.5)); + objects.push(Arc::new(boundary.clone())); + objects.push(Arc::new(ConstantMedium::new( + boundary, + Isotropic::new(Solid::new(Vec3::new(0.2, 0.4, 0.9))), + 0.2, + ))); + + objects.push(Arc::new(ConstantMedium::new( + Sphere::new(Vec3::splat(0.0), 5000.0, Dielectric::new(1.5)), + Isotropic::new(Solid::new(Vec3::splat(1.0))), + 0.0001, + ))); + + let earthmap = ImageTexture::from_filename("assets/earthmap.jpg") + .expect("error in reading assets/earthmap.jpg"); + objects.push(Arc::new(Sphere::new( + Vec3::new(400.0, 200.0, 400.0), + 100.0, + Lambertian::new(earthmap), + ))); + + objects.push(Arc::new(Sphere::new( + Vec3::new(220.0, 280.0, 300.0), + 80.0, + Lambertian::new(PerlinNoise::with_scale(&mut rng, 0.1)), + ))); + + let mut boxes2 = HitableList { list: Vec::new() }; + let white = Lambertian::new(Solid::new(Vec3::splat(0.73))); + for _ in 0..1000 { + boxes2.push(Arc::new(Sphere::new( + Vec3::random_in_range(&mut rng, 0.0..=165.0), + 10.0, + white.clone(), + ))); + } + + objects.push(Arc::new( + BvhNode::new(&mut rng, &mut boxes2.list, 0.0, 1.0) + .rotate_y(15.0) + .translate(Vec3::new(-100.0, 270.0, 395.0)), + )); + + objects + } + + fn camera(&self, aspect_ratio: f64) -> Camera { + let lookfrom = Vec3::new(478.0, 278.0, -600.0); + let lookat = Vec3::new(278.0, 278.0, 0.0); + let aperture = 0.1; + let focus_distance = 40.0; + Camera::new( + lookfrom, + lookat, + Vec3::new(0.0, 1.0, 0.0), + 40.0, + aspect_ratio, + aperture, + focus_distance, + 0.0, + 1.0, + ) + } +} diff --git a/src/demos/cornell_smoke_and_fog.rs b/src/demos/cornell_smoke_and_fog.rs new file mode 100644 index 0000000..d004590 --- /dev/null +++ b/src/demos/cornell_smoke_and_fog.rs @@ -0,0 +1,123 @@ +use std::sync::Arc; + +use rand::{prelude::SmallRng, SeedableRng}; + +use crate::{ + demos::{Demo, ParallelHit}, + hitable::{ + shapes::{Cuboid, RectBuilder}, + volume::ConstantMedium, + Hitable, + }, + materials::{DiffuseLight, Isotropic, Lambertian, MaterialBuilder}, + texture::Solid, + types::Vec3, + BvhNode, Camera, +}; + +pub struct CornellSmokeAndFog {} + +impl Demo for CornellSmokeAndFog { + type DemoT = BvhNode>; + + fn name(&self) -> &'static str { + "cornell_smoke_and_fog" + } + + fn world(&self) -> Self::DemoT { + let mut world: Vec> = Vec::with_capacity(8); + + let mut rng = rand::thread_rng(); + let mut rng = SmallRng::from_rng(&mut rng).unwrap(); + + let red = Lambertian::new(Solid::new(Vec3::new(0.65, 0.05, 0.05))); + let white = Lambertian::new(Solid::new(Vec3::splat(0.73))); + let green = Lambertian::new(Solid::new(Vec3::new(0.12, 0.45, 0.15))); + let light = DiffuseLight::new(Solid::new(Vec3::splat(7.0))); + + world.push(Arc::new( + RectBuilder + .y(0.0..=555.0) + .z(0.0..=555.0) + .x(555.0) + .material(green), + )); + world.push(Arc::new( + RectBuilder + .y(0.0..=555.0) + .z(0.0..=555.0) + .x(0.0) + .material(red), + )); + + world.push(Arc::new( + RectBuilder + .x(113.0..=443.0) + .z(127.0..=432.0) + .y(554.0) + .material(light), + )); + world.push(Arc::new( + RectBuilder + .x(0.0..=555.0) + .z(0.0..=555.0) + .y(0.0) + .material(white.clone()), + )); + + world.push(Arc::new( + RectBuilder + .x(0.0..=555.0) + .z(0.0..=555.0) + .y(555.0) + .material(white.clone()), + )); + world.push(Arc::new( + RectBuilder + .x(0.0..=555.0) + .y(0.0..=555.0) + .z(555.0) + .material(white.clone()), + )); + + // Add the two boxes + world.push(Arc::new(ConstantMedium::new( + Cuboid::new( + Vec3::splat(0.0), + Vec3::new(165.0, 330.0, 165.0), + white.clone(), + ) + .rotate_y(15.0) + .translate(Vec3::new(265.0, 0.0, 295.0)), + Isotropic::new(Solid::new(Vec3::splat(0.0))), + 0.01, + ))); + world.push(Arc::new(ConstantMedium::new( + Cuboid::new(Vec3::splat(0.0), Vec3::splat(165.0), white) + .rotate_y(-18.0) + .translate(Vec3::new(130.0, 0.0, 65.0)), + Isotropic::new(Solid::new(Vec3::splat(1.0))), + 0.01, + ))); + + BvhNode::new(&mut rng, &mut world, 0.0, 1.0) + } + + fn camera(&self, aspect_ratio: f64) -> Camera { + let lookfrom = Vec3::new(278.0, 278.0, -800.0); + let lookat = Vec3::new(278.0, 278.0, 0.0); + let aperture = 0.1; + let focus_distance = 40.0; + Camera::new( + lookfrom, + lookat, + Vec3::new(0.0, 1.0, 0.0), + 40.0, + aspect_ratio, + aperture, + focus_distance, + 0.0, + 1.0, + ) + } +} diff --git a/src/demos/image_texture.rs b/src/demos/image_texture.rs new file mode 100644 index 0000000..612c8bc --- /dev/null +++ b/src/demos/image_texture.rs @@ -0,0 +1,64 @@ +use std::sync::Arc; + +use rand::{prelude::SmallRng, SeedableRng}; + +use crate::{ + demos::{Demo, ParallelHit}, + hitable::shapes::Sphere, + materials::Lambertian, + texture::ImageTexture, + types::Vec3, + BvhNode, Camera, +}; + +pub struct ImageTextureDemo {} + +impl Demo for ImageTextureDemo { + type DemoT = BvhNode>; + + fn name(&self) -> &'static str { + "image_texture" + } + + fn get_background(&self) -> Vec3 { + Vec3::new(0.7, 0.8, 1.0) + } + + fn world(&self) -> Self::DemoT { + let mut world: Vec> = Vec::with_capacity(1); + + let mut rng = rand::thread_rng(); + let mut rng = SmallRng::from_rng(&mut rng).unwrap(); + + let earth_texture = match ImageTexture::from_filename("assets/earthmap.jpg") { + Ok(v) => v, + Err(e) => panic!("error in creating image texture: {}", e), + }; + + world.push(Arc::new(Sphere::new( + Vec3::new(0.0, 0.0, 0.0), + 2.0, + Lambertian::new(earth_texture), + ))); + + BvhNode::new(&mut rng, &mut world, 0.0, 1.0) + } + + fn camera(&self, aspect_ratio: f64) -> Camera { + 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 = 12.0; + Camera::new( + lookfrom, + lookat, + Vec3::new(0.0, 1.0, 0.0), + 20.0, + aspect_ratio, + aperture, + focus_distance, + 0.0, + 1.0, + ) + } +} diff --git a/src/demos/instances.rs b/src/demos/instances.rs new file mode 100644 index 0000000..7300d94 --- /dev/null +++ b/src/demos/instances.rs @@ -0,0 +1,117 @@ +use std::sync::Arc; + +use rand::{prelude::SmallRng, SeedableRng}; + +use crate::{ + demos::{Demo, ParallelHit}, + hitable::{ + shapes::{Cuboid, RectBuilder}, + Hitable, + }, + materials::{DiffuseLight, Lambertian, MaterialBuilder}, + texture::Solid, + types::Vec3, + BvhNode, Camera, +}; + +pub struct Instances {} + +impl Demo for Instances { + type DemoT = BvhNode>; + + fn name(&self) -> &'static str { + "instances" + } + + fn world(&self) -> Self::DemoT { + let mut world: Vec> = Vec::with_capacity(8); + + let mut rng = rand::thread_rng(); + let mut rng = SmallRng::from_rng(&mut rng).unwrap(); + + let red = Lambertian::new(Solid::new(Vec3::new(0.65, 0.05, 0.05))); + let white = Lambertian::new(Solid::new(Vec3::splat(0.73))); + let green = Lambertian::new(Solid::new(Vec3::new(0.12, 0.45, 0.15))); + let light = DiffuseLight::new(Solid::new(Vec3::splat(15.0))); + + world.push(Arc::new( + RectBuilder + .y(0.0..=555.0) + .z(0.0..=555.0) + .x(555.0) + .material(green), + )); + world.push(Arc::new( + RectBuilder + .y(0.0..=555.0) + .z(0.0..=555.0) + .x(0.0) + .material(red), + )); + world.push(Arc::new( + RectBuilder + .x(213.0..=343.0) + .z(227.0..=332.0) + .y(554.0) + .material(light), + )); + + world.push(Arc::new( + RectBuilder + .x(0.0..=555.0) + .z(0.0..=555.0) + .y(0.0) + .material(white.clone()), + )); + world.push(Arc::new( + RectBuilder + .x(0.0..=555.0) + .z(0.0..=555.0) + .y(555.0) + .material(white.clone()), + )); + world.push(Arc::new( + RectBuilder + .x(0.0..=555.0) + .y(0.0..=555.0) + .z(555.0) + .material(white.clone()), + )); + + // Add the two boxes + world.push(Arc::new( + Cuboid::new( + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(165.0, 330.0, 165.0), + white.clone(), + ) + .rotate_y(15.0) + .translate(Vec3::new(265.0, 0.0, 295.0)), + )); + world.push(Arc::new( + Cuboid::new(Vec3::new(0.0, 0.0, 0.0), Vec3::splat(165.0), white) + .rotate_y(-18.0) + .translate(Vec3::new(130.0, 0.0, 65.0)), + )); + + BvhNode::new(&mut rng, &mut world, 0.0, 1.0) + } + + fn camera(&self, aspect_ratio: f64) -> Camera { + let lookfrom = Vec3::new(278.0, 278.0, -800.0); + let lookat = Vec3::new(278.0, 278.0, 0.0); + let aperture = 0.1; + let focus_distance = 40.0; + Camera::new( + lookfrom, + lookat, + Vec3::new(0.0, 1.0, 0.0), + 40.0, + aspect_ratio, + aperture, + focus_distance, + 0.0, + 1.0, + ) + } +} diff --git a/src/demos/mod.rs b/src/demos/mod.rs new file mode 100644 index 0000000..374ff75 --- /dev/null +++ b/src/demos/mod.rs @@ -0,0 +1,234 @@ +use crate::{ + hitable::{hitable_list::HitableList, BvhNode, Hitable}, + types::{Color, Vec3}, + Camera, 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}, +}; + +mod checkered_motion_blur; +mod cornell_box; +mod cornell_smoke_and_fog; +mod image_texture; +mod instances; +mod perlin_noise_ball; +mod simple_light; +mod two_spheres; + +pub use checkered_motion_blur::CheckeredMotionBlur; +pub use cornell_box::CornellBox; +pub use cornell_smoke_and_fog::CornellSmokeAndFog; +pub use image_texture::ImageTextureDemo; +pub use instances::Instances; +pub use perlin_noise_ball::PerlinNoiseBall; +pub use simple_light::SimpleLight; +pub use two_spheres::TwoSpheres; + +#[derive(Debug)] +pub struct Chunk { + num: usize, + x: usize, + y: usize, + nx: usize, + ny: usize, + start_x: usize, + start_y: usize, + buffer: Vec, +} + +impl Display for Chunk { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!( + f, + "Chunk #{}: Start X = {} Start Y = {} Size X = {} Size = {}", + self.num, self.start_x, self.start_y, self.nx, self.ny + ) + } +} + +pub trait ParallelHit: Hitable + Send + Sync {} +impl ParallelHit for T {} + +pub trait Demo: Send + Sync { + type DemoT: Hitable + Send + Sync; + + fn name(&self) -> &'static str; + + fn world(&self) -> Self::DemoT; + + fn camera(&self, aspect_ratio: f64) -> Camera; + + fn get_background(&self) -> Vec3 { + Vec3::new(0.0, 0.0, 0.0) + } + + fn render_chunk(&self, chunk: &mut Chunk, camera: &Camera, world: &Self::DemoT, samples: u16) { + let &mut Chunk { + num: _, + x, + y, + nx, + ny, + start_x, + start_y, + ref mut buffer, + } = chunk; + let mut offset = 0; + let mut rng = rand::thread_rng(); + let mut rng = SmallRng::from_rng(&mut rng).unwrap(); + let background = self.get_background(); + + assert!(buffer.len() >= nx * ny * 4); + + (start_y..start_y + ny).for_each(|j| { + (start_x..start_x + nx).for_each(|i| { + 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, &mut rng); + color += ray.color(world, &mut rng, &background, 0); + } + + color /= samples as f64; + self.update_rgb(buffer, color, offset); + offset += 4; + }); + }); + } + + fn render(&self, buf: &mut Vec, x: usize, y: usize, samples: u16) { + let world = self.world(); + let delta_x = x / VERTICAL_PARTITION; + let delta_y = y / HORIZONTAL_PARTITION; + let remx = x % VERTICAL_PARTITION; + let remy = y % HORIZONTAL_PARTITION; + + // There can be tiny error here if the canvas height/width is not perfectly divisible + // by vertical/horizontal partitions in the chunks around the edges + // but umm, i'll just ignore those for now. + let camera = self.camera(delta_x as f64 / delta_y as f64); + 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 mut nx = delta_x; + let mut ny = delta_y; + let start_y = j * ny; + let start_x = i * nx; + + match (i + 1, j + 1) { + (HORIZONTAL_PARTITION, VERTICAL_PARTITION) => { + nx += remx; + ny += remy; + } + (HORIZONTAL_PARTITION, _) => nx += remx, + (_, VERTICAL_PARTITION) => ny += remy, + _ => (), + }; + + let mut chunk = Chunk { + num: j * HORIZONTAL_PARTITION + i, + x, + y, + nx, + ny, + start_x, + start_y, + buffer: vec![0; nx * ny * 4], + }; + + println!("{}", chunk); + self.render_chunk(&mut chunk, &camera, &world, 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; + } + + println!("Rendered {}", chunk); + }); + }); + } + + #[inline] + fn update_rgb(&self, buffer: &mut [u8], color: Vec3, offset: usize) { + let color: Color = color.into(); + + if let Some(pos) = buffer.get_mut(offset) { + *pos = color.0; + } + if let Some(pos) = buffer.get_mut(offset + 1) { + *pos = color.1 + } + if let Some(pos) = buffer.get_mut(offset + 2) { + *pos = color.2; + } + } + + fn save_as_ppm(&self, buf: &[u8], width: usize, height: usize, samples: u16) { + let header = format!("P3\n{} {}\n255\n", width, height); + + let mut file = match File::create(&format!( + "{}-{}x{}_{}.ppm", + self.name(), + width, + height, + samples, + )) { + 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), + } + } + } +} + +pub enum DemoWrapper { + HitableList(Box>), + BVHNode(Box>>>), +} + +impl DemoWrapper { + pub fn name(&self) -> &'static str { + match self { + DemoWrapper::HitableList(v) => v.name(), + DemoWrapper::BVHNode(v) => v.name(), + } + } + + pub fn save_as_ppm(&self, buf: &[u8], width: usize, height: usize, samples: u16) { + match self { + DemoWrapper::HitableList(v) => v.save_as_ppm(buf, width, height, samples), + DemoWrapper::BVHNode(v) => v.save_as_ppm(buf, width, height, samples), + } + } + + pub fn render(&self, buf: &mut Vec, x: usize, y: usize, samples: u16) { + match self { + DemoWrapper::HitableList(v) => v.render(buf, x, y, samples), + DemoWrapper::BVHNode(v) => v.render(buf, x, y, samples), + } + } +} diff --git a/src/demos/perlin_noise_ball.rs b/src/demos/perlin_noise_ball.rs new file mode 100644 index 0000000..299e88a --- /dev/null +++ b/src/demos/perlin_noise_ball.rs @@ -0,0 +1,65 @@ +use std::sync::Arc; + +use rand::{prelude::SmallRng, SeedableRng}; + +use crate::{ + demos::{Demo, ParallelHit}, + hitable::{shapes::Sphere, BvhNode}, + materials::Lambertian, + texture::PerlinNoise, + types::Vec3, + Camera, +}; + +pub struct PerlinNoiseBall {} + +impl Demo for PerlinNoiseBall { + type DemoT = BvhNode>; + + fn name(&self) -> &'static str { + "perlin_noise" + } + + fn get_background(&self) -> Vec3 { + Vec3::new(0.7, 0.8, 1.0) + } + + fn world(&self) -> Self::DemoT { + let mut world: Vec> = Vec::with_capacity(2); + + let mut rng = rand::thread_rng(); + let mut rng = SmallRng::from_rng(&mut rng).unwrap(); + + world.push(Arc::new(Sphere::new( + Vec3::new(0.0, -1000.0, 0.0), + 1000.0, + Lambertian::new(PerlinNoise::with_scale(&mut rng, 4.0)), + ))); + + world.push(Arc::new(Sphere::new( + Vec3::new(0.0, 2.0, 0.0), + 2.0, + Lambertian::new(PerlinNoise::with_scale(&mut rng, 4.0)), + ))); + + BvhNode::new(&mut rng, &mut world, 0.0, 1.0) + } + + fn camera(&self, aspect_ratio: f64) -> Camera { + 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; + Camera::new( + lookfrom, + lookat, + Vec3::new(0.0, 1.0, 0.0), + 20.0, + aspect_ratio, + aperture, + focus_distance, + 0.0, + 1.0, + ) + } +} diff --git a/src/demos/simple_light.rs b/src/demos/simple_light.rs new file mode 100644 index 0000000..9c67786 --- /dev/null +++ b/src/demos/simple_light.rs @@ -0,0 +1,80 @@ +use std::sync::Arc; + +use rand::{prelude::SmallRng, SeedableRng}; + +use crate::{ + demos::{Demo, ParallelHit}, + hitable::{ + shapes::{RectBuilder, Sphere}, + BvhNode, + }, + materials::{DiffuseLight, Lambertian, MaterialBuilder}, + texture::{PerlinNoise, Solid}, + types::Vec3, + Camera, +}; + +pub struct SimpleLight {} + +impl Demo for SimpleLight { + type DemoT = BvhNode>; + + fn name(&self) -> &'static str { + "simple_light" + } + + fn world(&self) -> Self::DemoT { + let mut world: Vec> = Vec::with_capacity(5); + let mut rng = rand::thread_rng(); + let mut rng = SmallRng::from_rng(&mut rng).unwrap(); + + world.push(Arc::new(Sphere::new( + Vec3::new(0.0, -1000.0, 0.0), + 1000.0, + Lambertian::new(PerlinNoise::with_scale(&mut rng, 4.0)), + ))); + world.push(Arc::new(Sphere::new( + Vec3::new(0.0, 2.0, 0.0), + 2.0, + Lambertian::new(PerlinNoise::with_scale(&mut rng, 4.0)), + ))); + + world.push(Arc::new( + RectBuilder + .x(3.0..=5.0) + .y(1.0..=3.0) + .z(-2.0) + .material(DiffuseLight::new(Solid::new(Vec3::new(4.0, 4.0, 4.0)))), + )); + world.push(Arc::new(Sphere::new( + Vec3::new(0.0, 7.0, 0.0), + 2.0, + DiffuseLight::new(Solid::new(Vec3::new(4.0, 4.0, 4.0))), + ))); + world.push(Arc::new(Sphere::new( + Vec3::new(-40.0, 2.0, 5.0), + 1.0, + DiffuseLight::new(Solid::new(Vec3::new(4.0, 4.0, 4.0))), + ))); + + BvhNode::new(&mut rng, &mut world, 0.0, 1.0) + } + + fn camera(&self, aspect_ratio: f64) -> crate::Camera { + let lookfrom = Vec3::new(26.0, 3.0, 6.0); + let lookat = Vec3::new(0.0, 2.0, 0.0); + let aperture = 0.0; + let focus_distance = 10.0; + Camera::new( + lookfrom, + lookat, + Vec3::new(0.0, 1.0, 0.0), + 20.0, + aspect_ratio, + aperture, + focus_distance, + 0.0, + 1.0, + ) + } +} diff --git a/src/demos/two_spheres.rs b/src/demos/two_spheres.rs new file mode 100644 index 0000000..76dfe3e --- /dev/null +++ b/src/demos/two_spheres.rs @@ -0,0 +1,71 @@ +use std::sync::Arc; + +use rand::{prelude::SmallRng, SeedableRng}; + +use crate::{ + demos::{Demo, ParallelHit}, + hitable::{shapes::Sphere, BvhNode}, + materials::Lambertian, + texture::{Checker, Solid}, + types::Vec3, + Camera, +}; + +pub struct TwoSpheres {} + +impl Demo for TwoSpheres { + type DemoT = BvhNode>; + + fn name(&self) -> &'static str { + "two_checkered_sphere" + } + + fn get_background(&self) -> Vec3 { + Vec3::new(0.7, 0.8, 1.0) + } + + fn world(&self) -> Self::DemoT { + let mut world: Vec> = Vec::with_capacity(2); + + let mut rng = rand::thread_rng(); + let mut rng = SmallRng::from_rng(&mut rng).unwrap(); + + world.push(Arc::new(Sphere::new( + Vec3::new(0.0, -10.0, 0.0), + 10.0, + Lambertian::new(Checker::new( + Solid::new(Vec3::new(0.2, 0.3, 0.1)), + Solid::new(Vec3::new(0.9, 0.9, 0.9)), + )), + ))); + + world.push(Arc::new(Sphere::new( + Vec3::new(0.0, 10.0, 0.0), + 10.0, + Lambertian::new(Checker::new( + Solid::new(Vec3::new(0.2, 0.3, 0.1)), + Solid::new(Vec3::new(0.9, 0.9, 0.9)), + )), + ))); + + BvhNode::new(&mut rng, &mut world, 0.0, 1.0) + } + + fn camera(&self, aspect_ratio: f64) -> Camera { + 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; + Camera::new( + lookfrom, + lookat, + Vec3::new(0.0, 1.0, 0.0), + 20.0, + aspect_ratio, + aperture, + focus_distance, + 0.0, + 1.0, + ) + } +} diff --git a/src/hitable/bvh.rs b/src/hitable/bvh.rs new file mode 100644 index 0000000..fa4b6f9 --- /dev/null +++ b/src/hitable/bvh.rs @@ -0,0 +1,139 @@ +use std::cmp::Ordering; + +use rand::{prelude::SliceRandom, Rng}; + +use crate::{ + hitable::{HitRecord, Hitable}, + types::Ray, + Aabb, +}; + +pub struct BvhNode { + bounding_box: Aabb, + left: HitNode, + right: HitNode, +} + +impl BvhNode { + pub fn new(rng: &mut R, objects: &mut [T], t0: f64, t1: f64) -> Self { + let comparator = [ + Self::box_x_compare, + Self::box_y_compare, + Self::box_z_compare, + ] + .choose(rng) + .unwrap(); + + let (left, right) = match objects.len() { + 1 => ( + HitNode::Direct(objects[0].clone()), + HitNode::Direct(objects[0].clone()), + ), + 2 => match comparator(&objects[0], &objects[1]) { + Ordering::Greater => ( + HitNode::Direct(objects[1].clone()), + HitNode::Direct(objects[0].clone()), + ), + _ => ( + HitNode::Direct(objects[0].clone()), + HitNode::Direct(objects[1].clone()), + ), + }, + + n => { + objects.sort_by(comparator); + let (l, r) = objects.split_at_mut(n / 2); + ( + HitNode::Bvh(Box::new(BvhNode::new(rng, l, t0, t1))), + HitNode::Bvh(Box::new(BvhNode::new(rng, r, t0, t1))), + ) + } + }; + + let left_box = left + .bounding_box(t0, t1) + .expect("missing bounding box for left BVH Node"); + let right_box = right + .bounding_box(t0, t1) + .expect("missing bounding box for right BVH Node"); + + Self { + left, + right, + bounding_box: Aabb::surrounding_box(left_box, right_box), + } + } + + fn box_x_compare(obj1: &T, obj2: &T) -> Ordering { + if let (Some(bbox_a), Some(bbox_b)) = + (obj1.bounding_box(0.0, 0.0), obj2.bounding_box(0.0, 0.0)) + { + return bbox_a.min.x().partial_cmp(&bbox_b.min.x()).unwrap(); + } + + panic!("No bounding box for this BVH Node!!") + } + + fn box_y_compare(obj1: &T, obj2: &T) -> Ordering { + if let (Some(bbox_a), Some(bbox_b)) = + (obj1.bounding_box(0.0, 0.0), obj2.bounding_box(0.0, 0.0)) + { + return bbox_a.min.y().partial_cmp(&bbox_b.min.y()).unwrap(); + } + + panic!("No bounding box for this BVH Node!!") + } + + fn box_z_compare(obj1: &T, obj2: &T) -> Ordering { + if let (Some(bbox_a), Some(bbox_b)) = + (obj1.bounding_box(0.0, 0.0), obj2.bounding_box(0.0, 0.0)) + { + return bbox_a.min.z().partial_cmp(&bbox_b.min.z()).unwrap(); + } + + panic!("No bounding box for this BVH Node!!") + } +} + +impl Hitable for BvhNode { + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option { + if !self.bounding_box.hit(ray, t_min, t_max) { + return None; + } + + let hbox_left = self.left.hit(ray, t_min, t_max); + + let hbox_right = if let Some(ref hleft) = hbox_left { + self.right.hit(ray, t_min, hleft.t) + } else { + self.right.hit(ray, t_min, t_max) + }; + + hbox_right.or(hbox_left) + } + + fn bounding_box(&self, _t_min: f64, _t_max: f64) -> Option { + Some(self.bounding_box) + } +} + +enum HitNode { + Bvh(Box>), + Direct(T), +} + +impl HitNode { + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option { + match self { + HitNode::Bvh(node) => node.hit(ray, t_min, t_max), + HitNode::Direct(node) => node.hit(ray, t_min, t_max), + } + } + + fn bounding_box(&self, t0: f64, t1: f64) -> Option { + match self { + HitNode::Bvh(node) => node.bounding_box(t0, t1), + HitNode::Direct(node) => node.bounding_box(t0, t1), + } + } +} diff --git a/src/hitable/hitable_list.rs b/src/hitable/hitable_list.rs new file mode 100644 index 0000000..4eff7b6 --- /dev/null +++ b/src/hitable/hitable_list.rs @@ -0,0 +1,54 @@ +use std::sync::Arc; + +use crate::{ + demos::ParallelHit, + hitable::{HitRecord, Hitable}, + types::Ray, + Aabb, +}; + +pub struct HitableList { + pub list: Vec>, +} + +impl Hitable for HitableList { + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> 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 + } + + fn bounding_box(&self, t0: f64, t1: f64) -> Option { + if self.list.is_empty() { + return None; + } + + let mut output_box = None; + + for obj in self.list.iter() { + if let Some(bbox) = obj.bounding_box(t0, t1) { + if let Some(ref mut opbox) = output_box { + *opbox = Aabb::surrounding_box(*opbox, bbox); + } else { + output_box = Some(bbox); + } + } else { + return output_box; + } + } + + output_box + } +} + +impl HitableList { + pub fn push(&mut self, obj: Arc) { + self.list.push(obj); + } +} diff --git a/src/hitable/mod.rs b/src/hitable/mod.rs new file mode 100644 index 0000000..efd0bb1 --- /dev/null +++ b/src/hitable/mod.rs @@ -0,0 +1,117 @@ +pub mod bvh; +pub mod hitable_list; +mod rotate; +pub mod shapes; +mod translate; +pub mod volume; + +pub use bvh::*; +pub use translate::*; + +use std::sync::Arc; + +use crate::{ + hitable::rotate::Rotate, + types::{Ray, Vec3}, + Aabb, Material, X, Y, Z, +}; + +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: &'a dyn Material, + + /// texture coordinates for an object + pub u: f64, + pub v: f64, + + pub front_face: bool, +} + +impl<'a> HitRecord<'a> { + pub fn new( + t: f64, + p: Vec3, + normal: Vec3, + material: &'a dyn Material, + (u, v): (f64, f64), + ) -> Self { + Self { + t, + p, + normal, + material, + u, + v, + front_face: false, + } + } + + pub fn set_face_normal(&mut self, ray: &Ray) { + self.front_face = ray.direction.dot(&self.normal) < 0.0; + + self.normal = if self.front_face { + self.normal + } else { + -self.normal + } + } +} + +pub trait Hitable { + fn hit(&self, _ray: &Ray, _t_min: f64, _t_max: f64) -> Option; + + fn bounding_box(&self, _t0: f64, _t1: f64) -> Option; + + fn translate(self, offset: impl Into) -> Translate + where + Self: Sized, + { + Translate::new(self, offset.into()) + } + + fn rotate_x(self, angle: f64) -> Rotate + where + Self: Sized, + { + Rotate::new(self, angle) + } + + fn rotate_y(self, angle: f64) -> Rotate + where + Self: Sized, + { + Rotate::new(self, angle) + } + + fn rotate_z(self, angle: f64) -> Rotate + where + Self: Sized, + { + Rotate::new(self, angle) + } +} + +impl Hitable for Arc { + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option { + self.as_ref().hit(ray, t_min, t_max) + } + fn bounding_box(&self, t0: f64, t1: f64) -> Option { + self.as_ref().bounding_box(t0, t1) + } +} diff --git a/src/hitable/rotate.rs b/src/hitable/rotate.rs new file mode 100644 index 0000000..60deb4d --- /dev/null +++ b/src/hitable/rotate.rs @@ -0,0 +1,130 @@ +use std::marker::PhantomData; + +use crate::{ + hitable::{HitRecord, Hitable}, + types::{Ray, Vec3}, + Aabb, Dimension, +}; + +pub struct Rotate { + hitable: T, + sin_theta: f64, + cos_theta: f64, + bbox: Option, + + _tag: PhantomData<(D1, D2, D3)>, +} + +impl Rotate +where + D1: Dimension, + D2: Dimension, + D3: Dimension, + T: Hitable, +{ + pub fn new(object: T, angle: f64) -> Rotate { + let radians = angle.to_radians(); + let sin_theta = radians.sin(); + let cos_theta = radians.cos(); + + let mut min = Vec3::splat(f64::MAX); + let mut max = Vec3::splat(f64::MIN); + + let bbox = if let Some(bbox) = object.bounding_box(0.0, 1.0) { + for i in 0..2 { + let i = i as f64; + for j in 0..2 { + let j = j as f64; + for k in 0..2 { + let k = k as f64; + + // D1 will be the axis about which we are rotating + let d1 = i * bbox.max.get::() + (1.0 - i) * bbox.min.get::(); + + let d2 = j * bbox.max.get::() + (1.0 - j) * bbox.min.get::(); + let d3 = k * bbox.max.get::() + (1.0 - k) * bbox.min.get::(); + + let new_d2 = cos_theta * d2 + sin_theta * d3; + let new_d3 = -sin_theta * d2 + cos_theta * d3; + + let tester = Vec3::splat(0.0) + .set::(d1) + .set::(new_d2) + .set::(new_d3); + + min = Vec3::min(tester, min); + max = Vec3::max(tester, max); + } + } + } + + Aabb::new(min, max) + } else { + Aabb::new(min, max) + }; + + Rotate { + hitable: object, + sin_theta, + cos_theta, + bbox: Some(bbox), + _tag: PhantomData, + } + } +} + +impl Hitable for Rotate +where + D1: Dimension, + D2: Dimension, + D3: Dimension, + T: Hitable, +{ + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option { + let origin = ray + .origin + .set::( + self.cos_theta * ray.origin.get::() - self.sin_theta * ray.origin.get::(), + ) + .set::( + self.sin_theta * ray.origin.get::() + self.cos_theta * ray.origin.get::(), + ); + + let direction = ray + .direction + .set::( + self.cos_theta * ray.direction.get::() + - self.sin_theta * ray.direction.get::(), + ) + .set::( + self.sin_theta * ray.direction.get::() + + self.cos_theta * ray.direction.get::(), + ); + + let rotated_ray = Ray::new(origin, direction, ray.time()); + + let mut hit = self.hitable.hit(&rotated_ray, t_min, t_max)?; + + hit.p = hit + .p + .set::(self.cos_theta * hit.p.get::() + self.sin_theta * hit.p.get::()) + .set::(-self.sin_theta * hit.p.get::() + self.cos_theta * hit.p.get::()); + + hit.normal = hit + .normal + .set::( + self.cos_theta * hit.normal.get::() + self.sin_theta * hit.normal.get::(), + ) + .set::( + -self.sin_theta * hit.normal.get::() + self.cos_theta * hit.normal.get::(), + ); + + hit.set_face_normal(&rotated_ray); + + Some(hit) + } + + fn bounding_box(&self, _t0: f64, _t1: f64) -> Option { + self.bbox + } +} diff --git a/src/hitable/shapes/cuboid.rs b/src/hitable/shapes/cuboid.rs new file mode 100644 index 0000000..767a690 --- /dev/null +++ b/src/hitable/shapes/cuboid.rs @@ -0,0 +1,87 @@ +use std::sync::Arc; + +use crate::{ + hitable::{hitable_list::HitableList, shapes::RectBuilder, HitRecord, Hitable}, + materials::{Material, MaterialBuilder}, + types::{Ray, Vec3}, + Aabb, +}; + +pub struct Cuboid { + min: Vec3, + max: Vec3, + sides: HitableList, +} + +impl Cuboid { + pub fn new(p0: Vec3, p1: Vec3, mat: impl Material + Clone + 'static) -> Self { + Self { + min: p0, + max: p1, + sides: Self::build_cuboid(p0, p1, mat), + } + } + + fn build_cuboid(p0: Vec3, p1: Vec3, mat: impl Material + Clone + 'static) -> HitableList { + let mut sides = HitableList { + list: Vec::with_capacity(6), + }; + + sides.push(Arc::new( + RectBuilder + .x(p0.x()..=p1.x()) + .y(p0.y()..=p1.y()) + .z(p1.z()) + .material(mat.clone()), + )); + sides.push(Arc::new( + RectBuilder + .x(p0.x()..=p1.x()) + .y(p0.y()..=p1.y()) + .z(p0.z()) + .material(mat.clone()), + )); + + sides.push(Arc::new( + RectBuilder + .x(p0.x()..=p1.x()) + .z(p0.z()..=p1.z()) + .y(p1.y()) + .material(mat.clone()), + )); + sides.push(Arc::new( + RectBuilder + .x(p0.x()..=p1.x()) + .z(p0.z()..=p1.z()) + .y(p0.y()) + .material(mat.clone()), + )); + + sides.push(Arc::new( + RectBuilder + .y(p0.y()..=p1.y()) + .z(p0.z()..=p1.z()) + .x(p1.x()) + .material(mat.clone()), + )); + sides.push(Arc::new( + RectBuilder + .y(p0.y()..=p1.y()) + .z(p0.z()..=p1.z()) + .x(p0.x()) + .material(mat), + )); + + sides + } +} + +impl Hitable for Cuboid { + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option { + self.sides.hit(ray, t_min, t_max) + } + + fn bounding_box(&self, _t0: f64, _t1: f64) -> Option { + Some(Aabb::new(self.min, self.max)) + } +} diff --git a/src/hitable/shapes/mod.rs b/src/hitable/shapes/mod.rs new file mode 100644 index 0000000..2e0a28a --- /dev/null +++ b/src/hitable/shapes/mod.rs @@ -0,0 +1,9 @@ +mod cuboid; +mod moving_sphere; +mod rectangle; +mod sphere; + +pub use cuboid::Cuboid; +pub use moving_sphere::MovingSphere; +pub use rectangle::RectBuilder; +pub use sphere::Sphere; diff --git a/src/hitable/shapes/moving_sphere.rs b/src/hitable/shapes/moving_sphere.rs new file mode 100644 index 0000000..45ac5d4 --- /dev/null +++ b/src/hitable/shapes/moving_sphere.rs @@ -0,0 +1,92 @@ +use crate::{ + hitable::{HitRecord, Hitable}, + types::{Ray, Vec3}, + Aabb, Material, +}; + +pub struct MovingSphere { + radius: f64, + center_start: Vec3, + center_end: Vec3, + time_start: f64, + time_end: f64, + material: T, +} + +impl MovingSphere { + pub fn new( + center_start: Vec3, + center_end: Vec3, + time_start: f64, + time_end: f64, + radius: f64, + material: T, + ) -> Self { + Self { + radius, + center_start, + center_end, + time_start, + time_end, + material, + } + } + + fn center(&self, time: f64) -> Vec3 { + self.center_start + + (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 { + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> 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 mut root = (-b - discriminant_root) / a; + if root < t_min || root > t_max { + root = (-b + discriminant_root) / a; + } + if root > t_min && root < t_max { + let p = ray.point_at_parameter(root); + let normal = (p - self.center(ray.time())) / self.radius; + + let mut hit_rec = + HitRecord::new(root, p, normal, &self.material, Self::get_uv(normal)); + + hit_rec.set_face_normal(ray); + + return Some(hit_rec); + } + } + None + } + + fn bounding_box(&self, t0: f64, t1: f64) -> Option { + let radius = Vec3::new(self.radius, self.radius, self.radius); + let box_smol = Aabb::new(self.center(t0) - radius, self.center(t0) + radius); + let box_big = Aabb::new(self.center(t1) - radius, self.center(t1) + radius); + + Some(Aabb::surrounding_box(box_smol, box_big)) + } +} diff --git a/src/hitable/shapes/rectangle.rs b/src/hitable/shapes/rectangle.rs new file mode 100644 index 0000000..f26ffd4 --- /dev/null +++ b/src/hitable/shapes/rectangle.rs @@ -0,0 +1,175 @@ +use std::{marker::PhantomData, ops::RangeInclusive}; + +use crate::{ + hitable::{HitRecord, Hitable}, + materials::MaterialBuilder, + types::{Ray, Vec3}, + Aabb, Dimension, Material, X, Y, Z, +}; + +type DimRange = RangeInclusive; + +pub struct Rectangle { + d1_range: DimRange, + d2_range: DimRange, + d3: f64, + material: T, + tag: PhantomData<(D1, D2, D3)>, +} + +impl Hitable for Rectangle +where + T: Material, + D1: Dimension, + D2: Dimension, + D3: Dimension, +{ + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option { + let t = (self.d3 - ray.origin.get::()) / ray.direction.get::(); + + if t < t_min || t > t_max { + return None; + } + + let d1 = ray.origin.get::() + t * ray.direction.get::(); + let d2 = ray.origin.get::() + t * ray.direction.get::(); + + if !self.d1_range.contains(&d1) || !self.d2_range.contains(&d2) { + return None; + } + + let u = (d1 - self.d1_range.start()) / (self.d1_range.end() - self.d1_range.start()); + let v = (d2 - self.d2_range.start()) / (self.d2_range.end() - self.d2_range.start()); + + let mut hit_rec = HitRecord::new( + t, + ray.point_at_parameter(t), + Vec3::splat(0.0).set::(1.0), + &self.material, + (u, v), + ); + + hit_rec.set_face_normal(ray); + + Some(hit_rec) + } + + fn bounding_box(&self, _t0: f64, _t1: f64) -> Option { + // Since this is a axis aligned Rectangle and we are using AABB BVH, Gap between the rectangle and + // the bounding box will be infinitely small + + let (&d1_0, &d1_1) = (self.d1_range.start(), self.d1_range.end()); + let (&d2_0, &d2_1) = (self.d2_range.start(), self.d2_range.end()); + + let min = Vec3::splat(self.d3 - 0.0001) + .set::(d1_0) + .set::(d2_0); + let max = Vec3::splat(self.d3 + 0.0001) + .set::(d1_1) + .set::(d2_1); + + Some(Aabb::new(min, max)) + } +} + +// taken from, https://github.com/Globidev/toy-rt/blob/master/trt-core/src/hit/rect.rs#L74 +// because it's amazing! + +pub struct RectBuilder; + +macro_rules! builder { + ($name:ident, $dim:ty) => { + pub fn $name(self, range: RangeInclusive) -> OneBoundedRectBuilder<$dim> { + OneBoundedRectBuilder { + range, + tag: PhantomData, + } + } + }; +} + +pub struct OneBoundedRectBuilder { + range: DimRange, + tag: PhantomData, +} + +impl RectBuilder { + builder!(x, X); + builder!(y, Y); + builder!(z, Z); +} + +macro_rules! one_bounded_rect_builder { + ($name:ident, $dim1: ty, $dim2: ty) => { + pub fn $name(self, d2_range: DimRange) -> TwoBoundedRectBuilder<$dim1, $dim2> { + TwoBoundedRectBuilder { + d1_range: self.range, + d2_range, + tag: PhantomData, + } + } + }; +} + +impl OneBoundedRectBuilder { + one_bounded_rect_builder!(y, X, Y); + one_bounded_rect_builder!(z, X, Z); +} +impl OneBoundedRectBuilder { + one_bounded_rect_builder!(x, Y, X); + one_bounded_rect_builder!(z, Y, Z); +} +impl OneBoundedRectBuilder { + one_bounded_rect_builder!(x, Z, X); + one_bounded_rect_builder!(y, Z, Y); +} + +pub struct TwoBoundedRectBuilder { + d1_range: DimRange, + d2_range: DimRange, + tag: PhantomData<(D1, D2)>, +} + +macro_rules! two_bounded_rect_builder { + ($name:ident, $dim1: ty, $dim2: ty, $dim3: ty) => { + pub fn $name(self, $name: f64) -> ThreeBoundedRectBuilder<$dim1, $dim2, $dim3> { + ThreeBoundedRectBuilder { + d1_range: self.d1_range, + d2_range: self.d2_range, + d3: $name, + tag: PhantomData, + } + } + }; +} + +impl TwoBoundedRectBuilder { + two_bounded_rect_builder!(z, X, Y, Z); +} +impl TwoBoundedRectBuilder { + two_bounded_rect_builder!(y, X, Z, Y); +} +impl TwoBoundedRectBuilder { + two_bounded_rect_builder!(x, Y, Z, X); +} + +pub struct ThreeBoundedRectBuilder { + d1_range: DimRange, + d2_range: DimRange, + d3: f64, + tag: PhantomData<(D1, D2, D3)>, +} + +impl MaterialBuilder for ThreeBoundedRectBuilder { + type Finished = Rectangle; + + fn material(self, material: T) -> Self::Finished { + Rectangle { + d1_range: self.d1_range, + d2_range: self.d2_range, + d3: self.d3, + material, + tag: PhantomData, + } + } +} diff --git a/src/hitable/shapes/sphere.rs b/src/hitable/shapes/sphere.rs new file mode 100644 index 0000000..c4b03e7 --- /dev/null +++ b/src/hitable/shapes/sphere.rs @@ -0,0 +1,77 @@ +use crate::{ + hitable::{HitRecord, Hitable}, + types::{Ray, Vec3}, + Aabb, Material, +}; + +#[derive(Clone)] +pub struct Sphere { + center: Vec3, + radius: f64, + material: T, +} + +impl Sphere { + pub fn new(center: Vec3, radius: f64, material: T) -> Self { + Self { + center, + radius, + 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 { + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> 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 mut root = (-b - discriminant_root) / a; + if root < t_min || root > t_max { + root = (-b + discriminant_root) / a; + } + if root > t_min && root < t_max { + let p = ray.point_at_parameter(root); + let normal = (p - self.center) / self.radius; + + let mut hit_rec = + HitRecord::new(root, p, normal, &self.material, Self::get_uv(normal)); + + hit_rec.set_face_normal(ray); + + return Some(hit_rec); + } + } + None + } + + fn bounding_box(&self, _t0: f64, _t1: f64) -> Option { + let radius = Vec3::new(self.radius, self.radius, self.radius); + Some(Aabb::new(self.center - radius, self.center + radius)) + } +} diff --git a/src/hitable/translate.rs b/src/hitable/translate.rs new file mode 100644 index 0000000..5ad11d0 --- /dev/null +++ b/src/hitable/translate.rs @@ -0,0 +1,37 @@ +use crate::{ + hitable::{HitRecord, Hitable}, + types::{Ray, Vec3}, + Aabb, +}; + +pub struct Translate { + object: T, + offset: Vec3, +} + +impl Translate { + pub const fn new(object: T, offset: Vec3) -> Self { + Self { object, offset } + } +} + +impl Hitable for Translate { + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option { + let moved_ray = Ray::new(ray.origin - self.offset, ray.direction, ray.time()); + + if let Some(mut hit) = self.object.hit(&moved_ray, t_min, t_max) { + hit.p += self.offset; + hit.set_face_normal(&moved_ray); + + Some(hit) + } else { + None + } + } + + fn bounding_box(&self, t0: f64, t1: f64) -> Option { + self.object + .bounding_box(t0, t1) + .map(|bbox| Aabb::new(bbox.min + self.offset, bbox.max + self.offset)) + } +} diff --git a/src/hitable/volume/constant_medium.rs b/src/hitable/volume/constant_medium.rs new file mode 100644 index 0000000..ecf0c98 --- /dev/null +++ b/src/hitable/volume/constant_medium.rs @@ -0,0 +1,63 @@ +use crate::{ + hitable::{HitRecord, Hitable}, + types::{Ray, Vec3}, + Aabb, Material, +}; + +pub struct ConstantMedium { + neg_inv_density: f64, + boundary: A, + phase_function: B, +} + +impl ConstantMedium { + pub fn new(boundary: A, phase_function: B, d: f64) -> Self { + Self { + boundary, + phase_function, + neg_inv_density: -1.0 / d, + } + } +} + +impl Hitable for ConstantMedium { + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option { + let mut hit1 = self.boundary.hit(ray, f64::MIN, f64::MAX)?; + let mut hit2 = self.boundary.hit(ray, hit1.t + 0.0001, f64::MAX)?; + + hit1.t = hit1.t.max(t_min); + hit2.t = hit2.t.min(t_max); + + if hit1.t >= hit2.t { + return None; + }; + + hit1.t = hit1.t.max(0.0); + + let ray_length = ray.direction.length(); + let distance_inside_boundary = (hit2.t - hit1.t) * ray_length; + let hit_distance = self.neg_inv_density * rand::random::().ln(); + + if hit_distance > distance_inside_boundary { + return None; + } + + let t = hit1.t + hit_distance / ray_length; + + Some(HitRecord { + t, + p: ray.point_at_parameter(t), + material: &self.phase_function, + u: 0.0, + v: 0.0, + + // Arbitrary + front_face: true, + normal: Vec3::new(1.0, 0.0, 0.0), + }) + } + + fn bounding_box(&self, t0: f64, t1: f64) -> Option { + self.boundary.bounding_box(t0, t1) + } +} diff --git a/src/hitable/volume/mod.rs b/src/hitable/volume/mod.rs new file mode 100644 index 0000000..e97ff87 --- /dev/null +++ b/src/hitable/volume/mod.rs @@ -0,0 +1,3 @@ +mod constant_medium; + +pub use constant_medium::ConstantMedium; diff --git a/src/main.rs b/src/main.rs index 76d6d58..9929a80 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,195 @@ -use rand::Rng; +#![allow(clippy::suspicious_arithmetic_impl)] -#[inline] -fn pdf(x: f64) -> f64 { - 3.0 * x * x / 8.0 +mod aabb; +mod camera; +mod demos; +mod hitable; +mod materials; +mod texture; +mod types; + +pub use aabb::Aabb; +pub use camera::Camera; +pub use materials::Material; +pub use texture::Texture; +pub use types::{Dimension, X, Y, Z}; + +use crate::hitable::BvhNode; +use demos::DemoWrapper; + +use std::time::Instant; + +pub trait Asf64: num_traits::AsPrimitive {} +impl> Asf64 for T {} + +const NUM_SAMPLES: u16 = 500; +const VERTICAL_PARTITION: usize = 30; +const HORIZONTAL_PARTITION: usize = 30; +const WIDTH: usize = 800; +const HEIGHT: usize = 800; + +fn main() -> Result<(), String> { + run(WIDTH, HEIGHT) } -fn main() { - let mut rng = rand::thread_rng(); - const N: u64 = 100000; - let mut sum = 0.0; +#[cfg(feature = "gui")] +fn run(mut width: usize, mut height: usize) -> Result<(), String> { + use sdl2::{ + event::{Event, WindowEvent}, + keyboard::Keycode, + pixels::PixelFormatEnum, + }; - for _ in 0..N { - let x: f64 = rng.gen_range(0.0f64..=8.0).powf(1.0 / 3.0); + let sdl_ctx = sdl2::init()?; + let video_subsys = sdl_ctx.video()?; + let window = video_subsys + .window("Ray tracing the Next Week", width as u32, height as u32) + .position_centered() + .build() + .map_err(|e| e.to_string())?; - sum += x * x / pdf(x); + 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())?; + + let mut active_demo = DemoWrapper::HitableList(Box::new(demos::CornellBox {})); + 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, NUM_SAMPLES); + should_update = false; + } + Some(Keycode::Num1) => { + active_demo = + DemoWrapper::BVHNode(Box::new(demos::CheckeredMotionBlur {})); + should_update = true; + } + Some(Keycode::Num2) => { + active_demo = DemoWrapper::BVHNode(Box::new(demos::TwoSpheres {})); + should_update = true; + } + Some(Keycode::Num3) => { + active_demo = DemoWrapper::BVHNode(Box::new(demos::PerlinNoiseBall {})); + should_update = true; + } + Some(Keycode::Num4) => { + active_demo = + DemoWrapper::BVHNode(Box::new(demos::ImageTextureDemo {})); + should_update = true; + } + Some(Keycode::Num5) => { + active_demo = DemoWrapper::BVHNode(Box::new(demos::SimpleLight {})); + should_update = true; + } + Some(Keycode::Num6) => { + active_demo = DemoWrapper::BVHNode(Box::new(demos::Instances {})); + should_update = true; + } + Some(Keycode::Num7) => { + active_demo = + DemoWrapper::BVHNode(Box::new(demos::CornellSmokeAndFog {})); + should_update = true; + } + Some(Keycode::Num8) => { + active_demo = DemoWrapper::HitableList(Box::new(demos::CornellBox {})); + should_update = true; + } + 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_f64() + ); + texture.update(None, &buffer, width * 4).unwrap(); + canvas.copy(&texture, None, None).unwrap(); + canvas.present(); + should_update = false; + } + } +} + +#[cfg(not(feature = "gui"))] +fn run(width: usize, height: usize) -> Result<(), String> { + let demos: [DemoWrapper; 8] = [ + DemoWrapper::BVHNode(Box::new(demos::CheckeredMotionBlur {})), + DemoWrapper::BVHNode(Box::new(demos::TwoSpheres {})), + DemoWrapper::BVHNode(Box::new(demos::PerlinNoiseBall {})), + DemoWrapper::BVHNode(Box::new(demos::ImageTextureDemo {})), + DemoWrapper::BVHNode(Box::new(demos::SimpleLight {})), + DemoWrapper::BVHNode(Box::new(demos::Instances {})), + DemoWrapper::BVHNode(Box::new(demos::CornellSmokeAndFog {})), + DemoWrapper::HitableList(Box::new(demos::CornellBox {})), + ]; + + for demo in demos.iter() { + run_and_save_demo(demo, width, height) } - println!("answer = {}", sum / N as f64); + Ok(()) +} + +#[cfg(not(feature = "gui"))] +fn run_and_save_demo(demo: &DemoWrapper, width: usize, height: usize) { + let mut buffer = vec![0; width * height * 4]; + + println!( + "Starting {} at {}x{} with {} samples", + demo.name(), + width, + height, + NUM_SAMPLES + ); + + let now = Instant::now(); + demo.render(&mut buffer, width, height, NUM_SAMPLES); + println!( + "Rendered Demo {}. Time Taken(s) = {}", + demo.name(), + now.elapsed().as_secs_f64() + ); + + demo.save_as_ppm(&buffer, width, height, NUM_SAMPLES); } diff --git a/src/materials/dielectric.rs b/src/materials/dielectric.rs new file mode 100644 index 0000000..f99bd5e --- /dev/null +++ b/src/materials/dielectric.rs @@ -0,0 +1,62 @@ +use rand::{prelude::SmallRng, Rng}; + +use crate::{ + hitable::HitRecord, + materials::{reflect, refract, schlick}, + types::{Ray, Vec3}, + Material, +}; + +#[derive(Clone)] +pub struct Dielectric { + refraction_index: f64, +} + +impl Dielectric { + pub fn new(refraction_index: f64) -> Self { + Self { refraction_index } + } +} + +impl Material for Dielectric { + fn scatter( + &self, + ray_in: &Ray, + hit_rec: &HitRecord, + rng: &mut SmallRng, + ) -> (Vec3, Option) { + // Glass absorbs nothing! So, Attenuation is always going to be 1.0 for this + let attenuation = Vec3::splat(1.0); + + let refraction_ratio = if hit_rec.front_face { + 1.0 / self.refraction_index + } else { + self.refraction_index + }; + + let unit_direction = ray_in.direction.unit_vector(); + let cosine = (-unit_direction).dot(&hit_rec.normal).min(1.0); + let sin_theta = (1.0 - cosine * cosine).sqrt(); + + let cannot_refract = refraction_ratio * sin_theta > 1.0; + + if cannot_refract || schlick(cosine, refraction_ratio) > rng.gen::() { + let direction = reflect(unit_direction, hit_rec.normal); + ( + attenuation, + Some(Ray::new(hit_rec.p, direction, ray_in.time())), + ) + } else if let Some(direction) = refract(unit_direction, hit_rec.normal, refraction_ratio) { + ( + attenuation, + Some(Ray::new(hit_rec.p, direction, ray_in.time())), + ) + } else { + let direction = reflect(unit_direction, hit_rec.normal); + ( + attenuation, + Some(Ray::new(hit_rec.p, direction, ray_in.time())), + ) + } + } +} diff --git a/src/materials/diffuse_light.rs b/src/materials/diffuse_light.rs new file mode 100644 index 0000000..c84795b --- /dev/null +++ b/src/materials/diffuse_light.rs @@ -0,0 +1,18 @@ +use crate::{types::Vec3, Material, Texture}; + +#[derive(Clone)] +pub struct DiffuseLight { + emit: T, +} + +impl DiffuseLight { + pub fn new(emit: T) -> Self { + Self { emit } + } +} + +impl Material for DiffuseLight { + fn emit(&self, u: f64, v: f64, p: Vec3) -> Vec3 { + self.emit.value(u, v, p) + } +} diff --git a/src/materials/isotropic.rs b/src/materials/isotropic.rs new file mode 100644 index 0000000..8440c75 --- /dev/null +++ b/src/materials/isotropic.rs @@ -0,0 +1,31 @@ +use rand::prelude::SmallRng; + +use crate::{ + hitable::HitRecord, + materials::random_point_in_unit_sphere, + types::{Ray, Vec3}, + Material, Texture, +}; + +pub struct Isotropic { + texture: T, +} + +impl Isotropic { + pub fn new(texture: T) -> Self { + Self { texture } + } +} + +impl Material for Isotropic { + fn scatter(&self, ray: &Ray, hit_rec: &HitRecord, rng: &mut SmallRng) -> (Vec3, Option) { + ( + self.texture.value(hit_rec.u, hit_rec.v, hit_rec.p), + Some(Ray::new( + hit_rec.p, + random_point_in_unit_sphere(rng), + ray.time(), + )), + ) + } +} diff --git a/src/materials/lambertian.rs b/src/materials/lambertian.rs new file mode 100644 index 0000000..ff46239 --- /dev/null +++ b/src/materials/lambertian.rs @@ -0,0 +1,31 @@ +use rand::prelude::SmallRng; + +use crate::{ + hitable::HitRecord, + materials::random_point_in_unit_sphere, + types::{Ray, Vec3}, + Material, Texture, +}; + +#[derive(Clone)] +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, hit_rec.p), + Some(scattered_ray), + ) + } +} diff --git a/src/materials/metal.rs b/src/materials/metal.rs new file mode 100644 index 0000000..0023a63 --- /dev/null +++ b/src/materials/metal.rs @@ -0,0 +1,46 @@ +use rand::prelude::SmallRng; + +use crate::{ + hitable::HitRecord, + materials::{random_point_in_unit_sphere, reflect}, + types::{Ray, Vec3}, + Material, +}; + +#[derive(Clone)] +pub struct Metal { + albedo: Vec3, + fuzz: f64, +} + +impl Metal { + #[allow(dead_code)] + 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..9904cd0 --- /dev/null +++ b/src/materials/mod.rs @@ -0,0 +1,72 @@ +mod dielectric; +mod diffuse_light; +mod isotropic; +mod lambertian; +mod metal; + +pub use dielectric::Dielectric; +pub use diffuse_light::DiffuseLight; +pub use isotropic::Isotropic; +pub use lambertian::Lambertian; +pub use metal::Metal; +use rand::{prelude::SmallRng, Rng}; + +use crate::{ + hitable::HitRecord, + types::{Ray, Vec3}, +}; + +pub trait Material: Send + Sync { + // scatter returns the attenuation and the scattered ray. + // Attenuation is ignored completely if there is no scattered ray + fn scatter( + &self, + _ray: &Ray, + _hit_rec: &HitRecord, + _rng: &mut SmallRng, + ) -> (Vec3, Option) { + (Vec3::splat(0.0), None) + } + + fn emit(&self, _u: f64, _v: f64, _p: Vec3) -> Vec3 { + Vec3::splat(0.0) + } +} + +// 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::random(rng) * 2.0 - Vec3::splat(1.0); + while point.sq_len() >= 1.0 { + point = Vec3::random(rng) * 2.0 - Vec3::splat(1.0); + } + point +} + +pub trait MaterialBuilder { + type Finished; + + fn material(self, material: T) -> Self::Finished; +} diff --git a/src/texture/checker.rs b/src/texture/checker.rs new file mode 100644 index 0000000..57e5ab8 --- /dev/null +++ b/src/texture/checker.rs @@ -0,0 +1,25 @@ +use crate::{types::Vec3, Texture}; + +#[derive(Clone)] +pub struct Checker { + odd: T, + even: T, +} + +impl Checker { + pub fn new(even: T, odd: T) -> Self { + Self { odd, even } + } +} + +impl Texture for Checker { + fn value(&self, u: f64, v: f64, p: Vec3) -> Vec3 { + let sine_wave = f64::sin(10.0 * p.x()) * f64::sin(10.0 * p.y()) * f64::sin(10.0 * p.z()); + + if sine_wave < 0.0 { + self.odd.value(u, v, p) + } else { + self.even.value(u, v, p) + } + } +} diff --git a/src/texture/image_texture.rs b/src/texture/image_texture.rs new file mode 100644 index 0000000..15c2ec4 --- /dev/null +++ b/src/texture/image_texture.rs @@ -0,0 +1,56 @@ +use image::{error::ImageError, io::Reader as ImageReader}; + +use crate::{types::Vec3, Texture}; + +#[derive(Clone)] +pub struct ImageTexture { + image: Vec, + // (width, height) + dimensions: (u32, u32), + bytes_per_scanline: u32, + bytes_per_pixel: u32, +} + +impl ImageTexture { + #[allow(dead_code)] + pub fn from_filename(filename: &str) -> Result { + let img = ImageReader::open(filename)?.decode()?; + let img = img.to_rgb8(); + + let (width, _) = img.dimensions(); + + let bytes_per_pixel = 3; + + Ok(Self { + image: img.to_vec(), + dimensions: img.dimensions(), + bytes_per_scanline: bytes_per_pixel * width, + bytes_per_pixel, + }) + } +} + +impl Texture for ImageTexture { + fn value(&self, u: f64, v: f64, _p: Vec3) -> Vec3 { + let (width, height) = self.dimensions; + + let u = u.clamp(0.0, 1.0); + let v = 1.0 - v.clamp(0.0, 1.0); + + let i = (u * width as f64) as u32; + let j = (v * height as f64) as u32; + + let i = i.clamp(0, width - 1); + let j = j.clamp(0, height - 1); + + let color_scale = 1.0 / 255.0; + + let pixel = (j * self.bytes_per_scanline + i * self.bytes_per_pixel) as usize; + + Vec3::new( + color_scale * (self.image[pixel] as f64), + color_scale * (self.image[pixel + 1] as f64), + color_scale * (self.image[pixel + 2] as f64), + ) + } +} diff --git a/src/texture/mod.rs b/src/texture/mod.rs new file mode 100644 index 0000000..7ec8242 --- /dev/null +++ b/src/texture/mod.rs @@ -0,0 +1,17 @@ +mod checker; +mod image_texture; +mod perlin; +mod perlin_noise; +mod solid; + +pub use checker::Checker; +pub use image_texture::ImageTexture; +pub use perlin::Perlin; +pub use perlin_noise::PerlinNoise; +pub use solid::Solid; + +use crate::types::Vec3; + +pub trait Texture { + fn value(&self, u: f64, v: f64, p: Vec3) -> Vec3; +} diff --git a/src/texture/perlin.rs b/src/texture/perlin.rs new file mode 100644 index 0000000..16c1814 --- /dev/null +++ b/src/texture/perlin.rs @@ -0,0 +1,120 @@ +use crate::types::Vec3; +use rand::Rng; + +const POINT_COUNT: usize = 256; + +#[derive(Clone)] +pub struct Perlin { + points: Vec, + + permute_x: Vec, + permute_y: Vec, + permute_z: Vec, +} + +impl Perlin { + pub fn new(rng: &mut R) -> Self { + let points = (0..POINT_COUNT) + .map(|_| Vec3::random(rng).unit_vector()) + .collect::>(); + + let permute_x = Self::perlin_generate_permutation(rng); + let permute_y = Self::perlin_generate_permutation(rng); + let permute_z = Self::perlin_generate_permutation(rng); + + Self { + points, + permute_x, + permute_y, + permute_z, + } + } + + fn perlin_generate_permutation(rng: &mut R) -> Vec { + let mut p = (0..POINT_COUNT).collect::>(); + permute(rng, &mut p); + p + } + + pub fn noise(&self, p: Vec3) -> f64 { + let mut smooth_grid = [[[Vec3::new(0.0, 0.0, 0.0); 2]; 2]; 2]; + + { + let i = p.x().floor() as i32; + let j = p.y().floor() as i32; + let k = p.z().floor() as i32; + for (di, a) in smooth_grid.iter_mut().enumerate() { + let di = di as i32; + for (dj, b) in a.iter_mut().enumerate() { + let dj = dj as i32; + for (dk, c) in b.iter_mut().enumerate() { + let dk = dk as i32; + + *c = self.points[self.permute_x[((i + di) & 255) as usize] + ^ self.permute_y[((j + dj) & 255) as usize] + ^ self.permute_z[((k + dk) & 255) as usize]] + } + } + } + } + + let u = p.x() - p.x().floor(); + let v = p.y() - p.y().floor(); + let w = p.z() - p.z().floor(); + + perlin_interpolate(smooth_grid, u, v, w) + } + + pub fn turbulence(&self, p: Vec3, depth: u32) -> f64 { + let mut acc = 0.0f64; + let mut weight = 1.0; + let mut temp_p = p; + + for _i in 0..depth { + acc += weight * self.noise(temp_p); + weight *= 0.5; + temp_p *= 2.0; + } + + acc.abs() + } +} + +fn perlin_interpolate(smooth_grid: [[[Vec3; 2]; 2]; 2], u: f64, v: f64, w: f64) -> f64 { + // Hermitian smoothing so we don't see obvious grid features in the picture + // Those features show up when we interpolate colors. Those features are + // also called mach bands + let uu = u * u * (3.0 - 2.0 * u); + let vv = v * v * (3.0 - 2.0 * v); + let ww = w * w * (3.0 - 2.0 * w); + + let mut acc = 0.0; + + for (di, a) in smooth_grid.iter().enumerate() { + let di = di as f64; + for (dj, b) in a.iter().enumerate() { + let dj = dj as f64; + for (dk, c) in b.iter().enumerate() { + let dk = dk as f64; + + let wt = Vec3::new(u - di, v - dj, w - dk); + + acc += (di * uu + (1.0 - di) * (1.0 - uu)) + * (dj * vv + (1.0 - dj) * (1.0 - vv)) + * (dk * ww + (1.0 - dk) * (1.0 - ww)) + * c.dot(&wt); + } + } + } + + acc +} + +fn permute(rng: &mut R, p: &mut [usize]) { + let l = p.len(); + + for i in (0..l).rev() { + let r = rng.gen_range(0..=i); + p.swap(i, r); + } +} diff --git a/src/texture/perlin_noise.rs b/src/texture/perlin_noise.rs new file mode 100644 index 0000000..06615ca --- /dev/null +++ b/src/texture/perlin_noise.rs @@ -0,0 +1,34 @@ +use rand::Rng; + +use crate::{texture::Perlin, types::Vec3, Texture}; + +#[derive(Clone)] +pub struct PerlinNoise { + noise: Perlin, + scale: f64, +} + +impl PerlinNoise { + #[allow(dead_code)] + pub fn new(rng: &mut R) -> Self { + Self { + noise: Perlin::new(rng), + scale: 1.0, + } + } + + pub fn with_scale(rng: &mut R, scale: f64) -> Self { + Self { + noise: Perlin::new(rng), + scale, + } + } +} + +impl Texture for PerlinNoise { + fn value(&self, _u: f64, _v: f64, p: Vec3) -> Vec3 { + Vec3::new(1.0, 1.0, 1.0) + * 0.5 + * (1.0 + (self.scale * p.z() + 10.0 * self.noise.turbulence(p, 7)).sin()) + } +} diff --git a/src/texture/solid.rs b/src/texture/solid.rs new file mode 100644 index 0000000..65f55f8 --- /dev/null +++ b/src/texture/solid.rs @@ -0,0 +1,18 @@ +use crate::{types::Vec3, Texture}; + +#[derive(Clone)] +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, _p: Vec3) -> Vec3 { + self.color + } +} diff --git a/src/types/color.rs b/src/types/color.rs new file mode 100644 index 0000000..938d08d --- /dev/null +++ b/src/types/color.rs @@ -0,0 +1,12 @@ +use crate::types::Vec3; + +pub struct Color(pub u8, pub u8, pub u8); + +impl From for Color { + fn from(v: Vec3) -> Self { + let v = v.sqrt() * 255.99; + let (r, g, b) = (v.x(), v.y(), v.z()); + + Self(r as u8, g as u8, b as u8) + } +} diff --git a/src/types/dimension.rs b/src/types/dimension.rs new file mode 100644 index 0000000..89b8937 --- /dev/null +++ b/src/types/dimension.rs @@ -0,0 +1,21 @@ +// Taken from, https://github.com/Globidev/toy-rt/tree/master/trt-core/src/dimension.rs + +pub trait Dimension { + const INDEX: usize; +} + +pub struct X; +pub struct Y; +pub struct Z; + +impl Dimension for X { + const INDEX: usize = 0; +} + +impl Dimension for Y { + const INDEX: usize = 1; +} + +impl Dimension for Z { + const INDEX: usize = 2; +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..9f3c95b --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,17 @@ +mod color; +mod dimension; +mod ray; + +pub use color::Color; +pub use dimension::{Dimension, X, Y, Z}; +pub use ray::Ray; + +#[cfg(not(target_arch = "x86_64"))] +mod vec3; +#[cfg(not(target_arch = "x86_64"))] +pub use vec3::Vec3; + +#[cfg(target_arch = "x86_64")] +mod simd_vec3; +#[cfg(target_arch = "x86_64")] +pub use simd_vec3::Vec3; diff --git a/src/types/ray.rs b/src/types/ray.rs new file mode 100644 index 0000000..d38d153 --- /dev/null +++ b/src/types/ray.rs @@ -0,0 +1,54 @@ +use rand::prelude::SmallRng; + +use crate::{hitable::Hitable, types::Vec3}; + +pub struct Ray { + pub origin: Vec3, + pub direction: Vec3, + time: f64, +} + +impl Ray { + pub fn new(origin: Vec3, direction: Vec3, time: f64) -> Ray { + Ray { + origin, + direction, + time, + } + } + + #[inline] + pub fn point_at_parameter(&self, t: f64) -> Vec3 { + self.origin + self.direction * t + } + #[inline] + pub const fn time(&self) -> f64 { + self.time + } + + pub fn color( + &self, + world: &T, + rng: &mut SmallRng, + background: &Vec3, + depth: u32, + ) -> Vec3 { + if let Some(hit_rec) = world.hit(self, 0.001, std::f64::MAX) { + if depth >= 50 { + Vec3::splat(0.0f64) + } else { + let material = hit_rec.material; + let emitted_color = hit_rec.material.emit(hit_rec.u, hit_rec.v, hit_rec.p); + + if let (attenuation, Some(scattered_ray)) = material.scatter(self, &hit_rec, rng) { + emitted_color + + attenuation * scattered_ray.color(world, rng, background, depth + 1) + } else { + emitted_color + } + } + } else { + *background + } + } +} diff --git a/src/types/simd_vec3.rs b/src/types/simd_vec3.rs new file mode 100644 index 0000000..51ba16a --- /dev/null +++ b/src/types/simd_vec3.rs @@ -0,0 +1,214 @@ +use std::{ + fmt::{Display, Formatter, Result as FmtResult}, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, RangeInclusive, Sub, SubAssign}, +}; + +use rand::Rng; + +use crate::{Asf64, Dimension, X, Y, Z}; + +use packed_simd::{f64x4, shuffle}; + +#[derive(Default, Debug, Copy, Clone)] +pub struct Vec3(f64x4); + +impl Vec3 { + #[inline] + pub fn new(a: impl Asf64, b: impl Asf64, c: impl Asf64) -> Vec3 { + Self(f64x4::new(a.as_(), b.as_(), c.as_(), 0.0)) + } + + pub fn splat(xyz: impl Asf64) -> Self { + Self::new(xyz, xyz, xyz) + } + + pub fn random(rng: &mut R) -> Self { + Self(f64x4::from_slice_unaligned(&rng.gen::<[f64; 4]>())) + } + + pub fn random_in_range(rng: &mut R, range: RangeInclusive) -> Self { + Vec3::new( + rng.gen_range(range.clone()), + rng.gen_range(range.clone()), + rng.gen_range(range), + ) + } + + #[inline] + pub fn x(&self) -> f64 { + self.get::() + } + #[inline] + pub fn y(&self) -> f64 { + self.get::() + } + #[inline] + pub fn z(&self) -> f64 { + self.get::() + } + + pub fn get(&self) -> f64 { + unsafe { self.0.extract_unchecked(D::INDEX) } + } + + pub fn set(self, value: f64) -> Self { + Self(unsafe { self.0.replace_unchecked(D::INDEX, value) }) + } + + #[inline] + pub fn length(&self) -> f64 { + self.sq_len().sqrt() + } + + #[inline] + pub fn sq_len(&self) -> f64 { + (self.0 * self.0).sum() + } + + #[inline] + pub fn dot(&self, v: &Vec3) -> f64 { + (self.0 * v.0).sum() + } + + #[inline] + pub fn cross(&self, v: &Vec3) -> Vec3 { + // https://web.archive.org/web/20210412192227/https://geometrian.com/programming/tutorials/cross-product/index.php + let tmp0: f64x4 = shuffle!(self.0, [1, 2, 0, 3]); + let tmp1: f64x4 = shuffle!(v.0, [2, 0, 1, 3]); + let tmp2: f64x4 = shuffle!(self.0, [2, 0, 1, 3]); + let tmp3: f64x4 = shuffle!(v.0, [1, 2, 0, 3]); + + Vec3(tmp0 * tmp1 - tmp2 * tmp3) + } + + #[inline] + pub fn unit_vector(self) -> Vec3 { + self / self.length() + } + + pub fn min(self, other: Self) -> Vec3 { + Self(self.0.min(other.0)) + } + + pub fn max(self, other: Self) -> Vec3 { + Self(self.0.max(other.0)) + } + + pub fn min_element(self, other: f64) -> f64 { + unsafe { self.0.replace_unchecked(3, other).min_element() } + } + + pub fn max_element(self, other: f64) -> f64 { + unsafe { self.0.replace_unchecked(3, other).max_element() } + } + + #[inline] + pub fn sqrt(self) -> Self { + Self(self.0.sqrt()) + } +} + +impl Add for Vec3 { + type Output = Vec3; + + fn add(self, o: Vec3) -> Vec3 { + Vec3(self.0 + o.0) + } +} + +impl AddAssign for Vec3 { + fn add_assign(&mut self, o: Vec3) { + self.0 += o.0 + } +} + +impl Sub for Vec3 { + type Output = Vec3; + + fn sub(self, o: Vec3) -> Vec3 { + Vec3(self.0 - o.0) + } +} + +impl SubAssign for Vec3 { + fn sub_assign(&mut self, o: Vec3) { + self.0 -= o.0; + } +} + +impl Neg for Vec3 { + type Output = Vec3; + + fn neg(self) -> Vec3 { + Vec3(-self.0) + } +} + +impl MulAssign for Vec3 { + fn mul_assign(&mut self, o: Vec3) { + self.0 *= o.0 + } +} + +impl MulAssign for Vec3 { + fn mul_assign(&mut self, o: f64) { + self.0 *= o + } +} + +impl Mul for Vec3 { + type Output = Vec3; + fn mul(self, o: f64) -> Vec3 { + Vec3(self.0 * o) + } +} + +impl Mul for Vec3 { + type Output = Vec3; + fn mul(self, o: Vec3) -> Vec3 { + Vec3(self.0 * o.0) + } +} + +impl Div for Vec3 { + type Output = Vec3; + + fn div(self, o: Vec3) -> Vec3 { + Vec3(self.0 / o.0) + } +} + +impl Div for Vec3 { + type Output = Vec3; + + fn div(self, o: f64) -> Vec3 { + let o = 1.0 / o; + + Vec3(self.0 * o) + } +} + +impl DivAssign for Vec3 { + fn div_assign(&mut self, o: f64) { + let o = 1.0 / o; + + self.0 *= o + } +} + +impl Display for Vec3 { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + f.write_fmt(format_args!( + "{} {} {}", + self.get::(), + self.get::(), + self.get::() + )) + } +} + +impl From<(A, B, C)> for Vec3 { + fn from((x, y, z): (A, B, C)) -> Self { + Self::new(x, y, z) + } +} diff --git a/src/types/vec3.rs b/src/types/vec3.rs new file mode 100644 index 0000000..9ba86fc --- /dev/null +++ b/src/types/vec3.rs @@ -0,0 +1,244 @@ +use std::{ + fmt::{Display, Formatter, Result as FmtResult}, + ops::{ + Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, RangeInclusive, Sub, + SubAssign, + }, +}; + +use rand::Rng; + +use crate::{Asf64, Dimension, X, Y, Z}; + +#[derive(Default, Debug, Copy, Clone)] +pub struct Vec3([f64; 3]); + +impl Vec3 { + #[inline] + pub fn new(a: impl Asf64, b: impl Asf64, c: impl Asf64) -> Vec3 { + Self([a.as_(), b.as_(), c.as_()]) + } + + pub fn splat(xyz: impl Asf64) -> Self { + Self::new(xyz, xyz, xyz) + } + + pub fn random(rng: &mut R) -> Self { + Self(rng.gen()) + } + + pub fn random_in_range(rng: &mut R, range: RangeInclusive) -> Self { + Vec3::new( + rng.gen_range(range.clone()), + rng.gen_range(range.clone()), + rng.gen_range(range), + ) + } + + #[inline] + pub fn x(&self) -> f64 { + self.get::() + } + #[inline] + pub fn y(&self) -> f64 { + self.get::() + } + #[inline] + pub fn z(&self) -> f64 { + self.get::() + } + + pub fn get(&self) -> f64 { + self.0[D::INDEX] + } + + pub fn set(mut self, value: f64) -> Self { + self.0[D::INDEX] = value; + self + } + + #[inline] + pub fn length(&self) -> f64 { + self.sq_len().sqrt() + } + + #[inline] + pub fn sq_len(&self) -> f64 { + self.x() * self.x() + self.y() * self.y() + self.z() * self.z() + } + + #[inline] + pub fn dot(&self, v: &Vec3) -> f64 { + self.x() * v.x() + self.y() * v.y() + self.z() * v.z() + } + + #[inline] + pub fn cross(&self, v: &Vec3) -> Vec3 { + Vec3([ + self.y() * v.z() - self.z() * v.y(), + self.z() * v.x() - self.x() * v.z(), + self.x() * v.y() - self.y() * v.x(), + ]) + } + + #[inline] + pub fn unit_vector(self) -> Vec3 { + self / self.length() + } + + pub fn min(self, other: Self) -> Vec3 { + Self([ + self.x().min(other.x()), + self.y().min(other.y()), + self.z().min(other.z()), + ]) + } + + pub fn max(self, other: Self) -> Vec3 { + Self([ + self.x().max(other.x()), + self.y().max(other.y()), + self.z().max(other.z()), + ]) + } + + pub fn min_element(self, other: f64) -> f64 { + self.x().min(self.y()).min(self.z()).min(other) + } + + pub fn max_element(self, other: f64) -> f64 { + self.x().max(self.y()).max(self.z()).max(other) + } + + #[inline] + pub fn sqrt(self) -> Self { + Vec3::new(self.x().sqrt(), self.y().sqrt(), self.z().sqrt()) + } +} + +impl Add for Vec3 { + type Output = Vec3; + + fn add(self, o: Vec3) -> Vec3 { + Vec3([self.x() + o.x(), self.y() + o.y(), self.z() + o.z()]) + } +} + +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.x() - o.x(), self.y() - o.y(), self.z() - o.z()]) + } +} + +impl SubAssign for Vec3 { + fn sub_assign(&mut self, o: Vec3) { + self.0[0] -= o.0[0]; + self.0[1] -= o.0[1]; + self.0[2] -= o.0[2]; + } +} + +impl Neg for Vec3 { + type Output = Vec3; + + fn neg(self) -> Vec3 { + Vec3([-self.x(), -self.y(), -self.z()]) + } +} + +impl MulAssign for Vec3 { + fn mul_assign(&mut self, o: Vec3) { + self.0[0] *= o.0[0]; + self.0[1] *= o.0[1]; + self.0[2] *= o.0[2]; + } +} + +impl MulAssign for Vec3 { + fn mul_assign(&mut self, o: f64) { + self.0[0] *= o; + self.0[1] *= o; + self.0[2] *= o; + } +} + +impl Mul for Vec3 { + type Output = Vec3; + fn mul(self, o: f64) -> Vec3 { + Vec3([self.x() * o, self.y() * o, self.z() * o]) + } +} + +impl Mul for Vec3 { + type Output = Vec3; + fn mul(self, o: Vec3) -> Vec3 { + Vec3([self.x() * o.x(), self.y() * o.y(), self.z() * o.z()]) + } +} + +impl Div for Vec3 { + type Output = Vec3; + + fn div(self, o: Vec3) -> Vec3 { + Vec3([self.x() / o.x(), self.y() / o.y(), self.z() / o.z()]) + } +} + +impl Div for Vec3 { + type Output = Vec3; + + fn div(self, o: f64) -> Vec3 { + let o = 1.0 / o; + Vec3([self.x() * o, self.y() * o, self.z() * o]) + } +} + +impl DivAssign for Vec3 { + fn div_assign(&mut self, o: f64) { + let o = 1.0 / o; + self.0[0] *= o; + self.0[1] *= o; + self.0[2] *= o; + } +} + +impl Index for Vec3 { + type Output = f64; + + fn index(&self, q: usize) -> &f64 { + &self.0[q] + } +} + +impl IndexMut for Vec3 { + fn index_mut(&mut self, q: usize) -> &mut f64 { + &mut self.0[q] + } +} + +impl Display for Vec3 { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + f.write_fmt(format_args!( + "{} {} {}", + self.get::(), + self.get::(), + self.get::() + )) + } +} + +impl From<(A, B, C)> for Vec3 { + fn from((x, y, z): (A, B, C)) -> Self { + Self::new(x, y, z) + } +}