diff --git a/Cargo.lock b/Cargo.lock index 930d94c..9a8d63c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,12 +20,24 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "bit_field" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + [[package]] name = "bytemuck" version = "1.7.3" @@ -105,12 +117,11 @@ dependencies = [ [[package]] name = "deflate" -version = "0.8.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" dependencies = [ "adler32", - "byteorder", ] [[package]] @@ -119,15 +130,68 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "exr" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cc0e06fb5f67e5d6beadf3a382fec9baca1aa751c6d5368fdeee7e5932c215" +dependencies = [ + "bit_field", + "deflate", + "flume", + "half", + "inflate", + "lebe", + "smallvec", + "threadpool", +] + +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843c03199d0c0ca54bc1ea90ac0d507274c28abcc4f691ae8b4eaa375087c76a" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + [[package]] name = "getrandom" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -142,9 +206,15 @@ dependencies = [ [[package]] name = "glam" -version = "0.20.2" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "781aa11be58ef14b0cd7326618afcbd9cdb5ba686bdab7193d87cdc322cd7033" + +[[package]] +name = "half" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4fa84eead97d5412b2a20aed4d66612a97a9e41e08eababdb9ae2bf88667490" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "hermit-abi" @@ -157,13 +227,14 @@ dependencies = [ [[package]] name = "image" -version = "0.23.14" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +checksum = "28edd9d7bc256be2502e325ac0628bde30b7001b9b52e0abe31a1a9dc2701212" dependencies = [ "bytemuck", "byteorder", "color_quant", + "exr", "gif", "jpeg-decoder", "num-iter", @@ -174,26 +245,68 @@ dependencies = [ "tiff", ] +[[package]] +name = "inflate" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" +dependencies = [ + "adler32", +] + [[package]] name = "jpeg-decoder" -version = "0.1.22" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b" dependencies = [ "rayon", ] +[[package]] +name = "js-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efd1d698db0759e6ef11a7cd44407407399a910c774dd804c64c032da7826ff" + [[package]] name = "libc" -version = "0.2.112" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "lock_api" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] [[package]] name = "memoffset" @@ -206,21 +319,20 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.3.7" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" dependencies = [ - "adler32", + "adler", ] [[package]] -name = "miniz_oxide" -version = "0.4.4" +name = "nanorand" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "adler", - "autocfg", + "getrandom", ] [[package]] @@ -246,9 +358,9 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.3.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" dependencies = [ "autocfg", "num-integer", @@ -274,16 +386,36 @@ dependencies = [ "libc", ] +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "png" -version = "0.16.8" +version = "0.17.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" dependencies = [ "bitflags", "crc32fast", "deflate", - "miniz_oxide 0.3.7", + "miniz_oxide", ] [[package]] @@ -292,6 +424,24 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "proc-macro2" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rand" version = "0.8.4" @@ -379,23 +529,118 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "spin" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" +dependencies = [ + "lock_api", +] + +[[package]] +name = "syn" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "tiff" -version = "0.6.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +checksum = "7cfada0986f446a770eca461e8c6566cb879682f7d687c8348aa0c857bd52286" dependencies = [ + "flate2", "jpeg-decoder", - "miniz_oxide 0.4.4", "weezl", ] +[[package]] +name = "unicode-ident" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wasm-bindgen" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" + [[package]] name = "weezl" version = "0.1.5" diff --git a/Cargo.toml b/Cargo.toml index 7b4fc44..f319fd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,14 @@ version = "0.1.0" edition = "2021" [profile.release] +lto = "fat" debug = 1 +opt-level = 3 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -image = "0.23.14" -glam = "0.20.2" +image = "0.24.2" +glam = { version = "0.21.2", features = ["fast-math"] } rayon = "1.5.1" -rand = { version = "0.8.4", features = ["small_rng"] } \ No newline at end of file +rand = { version = "0.8.4", features = ["small_rng"] } diff --git a/src/main.rs b/src/main.rs index c7a1f97..5bb5335 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,6 @@ mod rt; use color_output::*; use math::*; -use rand::distributions::{Standard as StandardDist}; use rand::prelude::*; use rayon::prelude::*; use rt::geometry; @@ -20,18 +19,18 @@ fn report_progress(scanline: u32) { io::stderr().flush().unwrap(); } -fn random_scene(rng: &mut impl Rng) -> Vec { +fn random_scene(rng: &mut impl Rng) -> Vec> { let n = 22; - let mut world = Vec::with_capacity(n); + let mut world: Vec> = Vec::with_capacity(n); let mat_ground = rt::Material::Lambertian { albedo: Color::splat(0.5), }; - world.push(geometry::Sphere::new( + world.push(Box::new(geometry::sphere( Point3::new(0.0, -1000.0, 0.0), 1000.0, mat_ground, - )); + ))); let upper_m: i64 = n as i64 / 2; let lower_m: i64 = -upper_m; @@ -61,7 +60,17 @@ fn random_scene(rng: &mut impl Rng) -> Vec { } }; - world.push(geometry::Sphere::new(center, 0.2, mat)); + if mat_prob < 0.8 { + let center2 = + center + Vec3::new(0.0, rng.sample::(StandardDist) * 0.5, 0.0); + world.push(Box::new(geometry::moving_sphere( + geometry::sphere(center, 0.2, mat), + center2, + 0.0..1.0, + ))); + } else { + world.push(Box::new(geometry::sphere(center, 0.2, mat))); + } } } @@ -70,35 +79,43 @@ fn random_scene(rng: &mut impl Rng) -> Vec { roughness: 0.01, ior: 1.5, }; - world.push(geometry::Sphere::new(Point3::new(0.0, 1.0, 0.0), 1.0, mat1)); + world.push(Box::new(geometry::sphere( + Point3::new(0.0, 1.0, 0.0), + 1.0, + mat1, + ))); let mat2 = rt::Material::Lambertian { albedo: Color::new(0.4, 0.2, 0.1), }; - world.push(geometry::Sphere::new( + world.push(Box::new(geometry::sphere( Point3::new(-4.0, 1.0, 0.0), 1.0, mat2, - )); + ))); let mat3 = rt::Material::Metallic { albedo: Color::new(0.7, 0.6, 0.5), roughness: 0.01, }; - world.push(geometry::Sphere::new(Point3::new(4.0, 1.0, 0.0), 1.0, mat3)); + world.push(Box::new(geometry::sphere( + Point3::new(4.0, 1.0, 0.0), + 1.0, + mat3, + ))); world } fn main() { // Image - let aspect_ratio = 3.0 / 2.0; - let samples_per_pixel = 256; - let image_width = 1200u32; + let aspect_ratio = 16.0 / 9.0; + let samples_per_pixel = 32; + let image_width = 1280u32; let image_height = (image_width as f32 / aspect_ratio) as u32; // World let mut world_rng = SmallRng::seed_from_u64(0xEDADBEEF); - let world = random_scene(&mut world_rng); + let world = Bvh::new(random_scene(&mut world_rng), 0.0..1.0); // Camera let camera = Camera::new( @@ -107,6 +124,7 @@ fn main() { Point3::Y, 20.0, aspect_ratio, + 0.0..1.0, 0.1, Some(10.0), ); @@ -138,7 +156,7 @@ fn main() { let v = (j as f32 + v_offset) / (image_height - 1) as f32; r = camera.get_ray(u, v, &mut rng); - color += ray_color(r, world.as_slice(), &mut rng); + color += ray_color(&mut r, &world, &mut rng); } output_color(color * color_scale) }) diff --git a/src/math.rs b/src/math.rs index 7ecd30d..d4f8473 100644 --- a/src/math.rs +++ b/src/math.rs @@ -2,15 +2,18 @@ pub type Point3 = glam::Vec3A; pub type Vec3 = glam::Vec3A; mod ray; + pub use ray::Ray; use rand::prelude::*; use crate::color_output::Color; +pub use rand::distributions::Standard as StandardDist; + #[inline(always)] pub fn random_vec3(rng: &mut impl Rng, min: f32, max: f32) -> Vec3 { - let values: [f32; 3] = rng.sample(rand::distributions::Standard); + let values: [f32; 3] = rng.sample(StandardDist); (max - min) * Vec3::from_slice(&values) + Vec3::splat(min) } @@ -21,7 +24,7 @@ pub fn random_on_unit_sphere(rng: &mut impl Rng) -> Vec3 { pub fn random_in_unit_disk(rng: &mut impl Rng) -> Vec3 { loop { - let values: [f32; 2] = rng.sample(rand::distributions::Standard); + let values: [f32; 2] = rng.sample(StandardDist); let vec = Vec3::new(values[0], values[1], 0.0); if vec.length_squared() >= 1.0 { continue; diff --git a/src/math/ray.rs b/src/math/ray.rs index fa4885d..8e43882 100644 --- a/src/math/ray.rs +++ b/src/math/ray.rs @@ -1,17 +1,40 @@ use super::*; -#[derive(Copy, Clone)] +#[derive(Debug, Copy, Clone)] pub struct Ray { pub orig: Point3, pub dir: Vec3, + pub time: f32, + pub color: Color, } impl Ray { - pub fn new(orig: Point3, dir: Vec3) -> Ray { - Ray {orig, dir} + pub fn new(orig: Point3, dir: Vec3, time: f32) -> Ray { + Ray { + orig, + dir, + time, + ..Default::default() + } } pub fn at(&self, t: f32) -> Point3 { - self.orig + t*self.dir + self.orig + t * self.dir } -} \ No newline at end of file + + pub fn attenuate(mut self, incoming_color: Color) -> Self { + self.color = attenuate(self.color, incoming_color); + self + } +} + +impl Default for Ray { + fn default() -> Self { + Ray { + orig: Vec3::ZERO, + dir: Vec3::ONE, + time: 0.0, + color: Color::ONE - Color::splat(0.001), + } + } +} diff --git a/src/rt.rs b/src/rt.rs index 8449783..a888891 100644 --- a/src/rt.rs +++ b/src/rt.rs @@ -2,37 +2,29 @@ mod camera; pub mod geometry; mod hittable; mod material; +mod aabb; +mod bvh; pub use camera::*; pub use hittable::*; pub use material::*; +pub use aabb::*; +pub use bvh::*; use crate::color_output::*; use crate::math::*; use rand::prelude::*; -pub fn ray_color(mut r: Ray, world: &T, rng: &mut impl Rng) -> Color { +pub fn ray_color(r: &mut Ray, world: &T, rng: &mut impl Rng) -> Color { let white = Color::splat(1.0); let skyblue = Color::new(0.5, 0.7, 1.0); - let mut color = Color::ONE; let mut bounces = 0; - while let Some(hit) = world.hit(&r, 0.001, f32::INFINITY) { - if let Some(MaterialResponse { - attenuation: a, - new_ray, - }) = hit.material.scatter(&r, &hit, rng) - { - color = attenuate(color, a); - r = new_ray; - } else { - color = Color::ZERO; - break; - } + while let Some(hit) = world.hit(r, 0.001, f32::INFINITY) { + *r = hit.material.scatter(r, &hit, rng); bounces += 1; if bounces > 50 { - color = Color::ZERO; break; } } @@ -41,5 +33,5 @@ pub fn ray_color(mut r: Ray, world: &T, rng: &mut impl Rng let t = 0.5 * (unit_dir.y + 1.0); let env_color = white.lerp(skyblue, t); - color * env_color + attenuate(r.color, env_color) } diff --git a/src/rt/aabb.rs b/src/rt/aabb.rs new file mode 100644 index 0000000..2404ce7 --- /dev/null +++ b/src/rt/aabb.rs @@ -0,0 +1,65 @@ +use crate::math::*; + +#[derive(Clone, Copy, Debug)] +pub struct Aabb { + pub min: Point3, + pub max: Point3, + infinite: bool, +} + +impl Aabb { + pub fn new(min: Point3, max: Point3) -> Self { + Aabb { + min, + max, + infinite: false, + } + } + + pub fn infinite() -> Self { + Aabb { + min: Vec3::splat(f32::NEG_INFINITY), + max: Vec3::splat(f32::INFINITY), + infinite: true, + } + } + + pub fn surrounding_box(self, other: Self) -> Self { + Self { + min: self.min.min(other.min), + max: self.max.max(other.max), + infinite: self.infinite || other.infinite, + } + } + + pub fn doubled_centroid(&self) -> Vec3 { + self.max - self.min + } + + pub fn surface_area(&self) -> f32 { + let measurements = self.max - self.min; + 2.0 * (measurements.x * measurements.y + + measurements.y * measurements.z + + measurements.z * measurements.x) + } + + pub fn hit(&self, r: &Ray, t_min: f32, t_max: f32) -> bool { + if self.infinite { + return true; + } + + let inv_d = 1.0 / r.dir; + let t00 = (self.min - r.orig) * inv_d; + let t01 = (self.max - r.orig) * inv_d; + + // swap t0_i and t1_i if inv_d_i is negative and thus t0_i > t1_i + let ltz = inv_d.cmplt(Vec3::ZERO); + let t0 = Vec3::select(ltz, t01, t00); + let t1 = Vec3::select(ltz, t00, t01); + + let t_min_1 = t_min.max(t0.max_element()); + let t_max_1 = t_max.min(t1.min_element()); + + t_max_1 > t_min_1 + } +} \ No newline at end of file diff --git a/src/rt/bvh.rs b/src/rt/bvh.rs new file mode 100644 index 0000000..565d12e --- /dev/null +++ b/src/rt/bvh.rs @@ -0,0 +1,217 @@ +use std::ops::Range; + +use crate::math::*; + +use super::Aabb; +use super::Hittable; + +pub struct Bvh { + objects: Vec, + nodes: BvhNode, +} + +#[derive(Debug, Clone)] +enum BvhNodeValue { + Node { + leafs_count: usize, + children: Vec, + }, + Leaf { + object_idx: usize, + }, +} + +#[derive(Debug, Clone)] +struct BvhNode { + aabb: Aabb, + value: BvhNodeValue, +} + +impl BvhNode { + fn new_node(children: Vec) -> Self { + let root_box = children + .iter() + .map(|x| x.aabb) + .reduce(|a, b| a.surrounding_box(b)) + .unwrap_or_else(|| Aabb::infinite()); + + let leafs_count = children.iter().fold(0, |acc, c| acc + c.objects_count()); + + BvhNode { + aabb: root_box, + value: BvhNodeValue::Node { + children, + leafs_count, + }, + } + } + + fn objects_count(&self) -> usize { + match self.value { + BvhNodeValue::Leaf { .. } => 1, + BvhNodeValue::Node { leafs_count, .. } => leafs_count, + } + } + + /* + This function splits the BvhNode according to the Surface Area Heuristic (SAH) + The key ideas are: + - Probability of a ray hitting a node is proportional to its surface area + - Cost of traversing a node depends on the number of objects in its leaves + - To split a node, find the hyperplane that minimizes SA(L)*N(L) + SA(R)*N(R) where: + - SA(L) and SA(R) are the surface areas of the AABBs that enclose objects whose + centroids are on the left/right of the split hyperplane. + - N(L) and N(R) are the counts of objects left and right of the split hyperplane + + For details see: + - https://graphics.cg.uni-saarland.de/courses/cg1-2018/slides/Building_good_BVHs.pdf + - https://www.cg.tuwien.ac.at/courses/Rendering/2020/slides/01_spatial_acceleration.pdf + */ + fn sah_sweep_split(mut self) -> Self { + let mut nodes_to_split: Vec<&mut BvhNode> = Vec::with_capacity(self.objects_count() / 2); + nodes_to_split.push(&mut self); + + loop { + let current = match nodes_to_split.pop() { + None => break, + Some(cur) => cur, + }; + if let BvhNodeValue::Node { ref mut children, .. } = current.value { + if children.len() < 3 { + continue; + } + // This loop does a bunch of allocations and re-sorts per axis, + // but should do for now (100-1000 of objects) + // For substantially larger scenes it'd be better to switch to binning. + let (_sa, left, right) = (0..2) + .map(|axis| { + children.sort_unstable_by(|a, b| { + let al = a.aabb.doubled_centroid()[axis]; + let bl = b.aabb.doubled_centroid()[axis]; + al.partial_cmp(&bl).unwrap_or_else(|| { + if al.is_nan() && bl.is_nan() { + core::cmp::Ordering::Equal + } else if al.is_nan() { + core::cmp::Ordering::Greater + } else { + core::cmp::Ordering::Less + } + }) + }); + // Merging an AABB with itself won't change its surface area + let mut aabb_acc = children.first().map(|c| c.aabb).unwrap(); + let mut n_acc = 0.0; + let mut sa_sums = vec![0.0; children.len()]; + // sweep left-to-right and compute running + // weighted surface area sums SA(L)*N(L) + // of object i and the objects left of it. + for i in 0..children.len() { + n_acc += children[i].objects_count() as f32; + aabb_acc = aabb_acc.surrounding_box(children[i].aabb); + sa_sums[i] += aabb_acc.surface_area() * n_acc; + } + // Sweep right-to-left and compute running + // weighted surface area sums + // of objects right of an i-th object, + // excluding the i-th object itself. + // SA(R)*N(R) + aabb_acc = children.last().map(|c| c.aabb).unwrap(); + n_acc = 0.0; + for i in (0..children.len()).rev() { + let j = i + 1; + if j < children.len() { + n_acc += children[j].objects_count() as f32; + aabb_acc = aabb_acc.surrounding_box(children[j].aabb); + // add SA of all objects to the right from the i-th place + sa_sums[i] += aabb_acc.surface_area() * n_acc; + } + } + // Find the split with the smallest (on this axis) + // associated surface area sums + let (pivot, &sa_lr) = sa_sums + .iter() + .enumerate() + .filter(|(_, &sa)| !sa.is_nan()) + .min_by(|(_, sa1), (_, sa2)| sa1.partial_cmp(sa2).unwrap()) + .unwrap(); + // Since pivot is an index in the children array, it's always < children.len() + // Therefore pivot + 1 could be at most children.len(). + // It will produce an empty right side though. + let (left, right) = children.split_at_mut(pivot + 1); + (sa_lr, left.to_vec(), right.to_vec()) + }) + // Find the best split across all 3 axes + .min_by(|(sa1, _, _), (sa2, _, _)| sa1.partial_cmp(sa2).unwrap()) + .unwrap(); + + if right.is_empty() { + // current node couldn't be split, leave it alone + // otherwise it'll cause an infinite loop + continue; + } + children.clear(); + children.push(BvhNode::new_node(left)); + children.push(BvhNode::new_node(right)); + nodes_to_split.extend(children.iter_mut()); + } + } + self + } +} + +impl Bvh { + pub fn new(scene_objects: Vec, time_interval: Range) -> Self { + let objects = scene_objects; + let leafs: Vec = objects + .iter() + .enumerate() + .map(|(idx, o)| BvhNode { + aabb: o.bounding_box(time_interval.clone()), + value: BvhNodeValue::Leaf { object_idx: idx }, + }) + .collect(); + + Bvh { + objects, + nodes: BvhNode::new_node(leafs).sah_sweep_split(), + } + } +} + +impl Hittable for Bvh { + fn hit(&self, r: &Ray, t_min: f32, t_max: f32) -> Option { + let mut final_hit = None; + let mut closest_hit = t_max; + let mut nodes_to_try: Vec<&BvhNode> = Vec::with_capacity(10); + nodes_to_try.push(&self.nodes); + + loop { + let current = match nodes_to_try.pop() { + None => break, + Some(cur) => cur, + }; + if !current.aabb.hit(r, t_min, closest_hit) { + continue; + } + match current.value { + BvhNodeValue::Node { ref children, .. } => { + nodes_to_try.extend(children.iter().map(|c| c)) + } + BvhNodeValue::Leaf { object_idx } => { + let object = &self.objects[object_idx]; + let current_hit = object.hit(r, t_min, closest_hit); + if let Some(ref hit) = current_hit { + closest_hit = hit.t; + final_hit = current_hit; + } + } + } + } + + final_hit + } + + fn bounding_box(&self, _time_interval: std::ops::Range) -> Aabb { + self.nodes.aabb + } +} diff --git a/src/rt/camera.rs b/src/rt/camera.rs index 5b61191..9feaaa1 100644 --- a/src/rt/camera.rs +++ b/src/rt/camera.rs @@ -1,5 +1,7 @@ +use std::ops::Range; + use crate::math::*; -use rand::prelude::*; +use rand::{distributions::Uniform, prelude::*}; pub struct Camera { origin: Point3, @@ -9,6 +11,7 @@ pub struct Camera { lens_radius: f32, u: Vec3, v: Vec3, + time_distribution: Uniform, } impl Camera { @@ -18,6 +21,7 @@ impl Camera { vup: Vec3, fov: f32, aspect_ratio: f32, + time_interval: Range, aperture: f32, focus_dist: Option, ) -> Self { @@ -38,6 +42,7 @@ impl Camera { vertical, u, v, + time_distribution: Uniform::new(time_interval.start, time_interval.end), lens_radius: aperture / 2.0, upper_left_corner: origin - horizontal / 2.0 - vertical / 2.0 - w * focus_dist, } @@ -46,9 +51,11 @@ impl Camera { pub fn get_ray(&self, s: f32, t: f32, rng: &mut impl Rng) -> Ray { let rd = self.lens_radius * random_in_unit_disk(rng); let offset = self.u * rd.x + self.v * rd.y; + let time = rng.sample(self.time_distribution); Ray::new( self.origin + offset, self.upper_left_corner + s * self.horizontal + t * self.vertical - self.origin - offset, + time, ) } } diff --git a/src/rt/geometry.rs b/src/rt/geometry.rs index 648bc39..c776584 100644 --- a/src/rt/geometry.rs +++ b/src/rt/geometry.rs @@ -1,3 +1,6 @@ +use std::ops::Range; + +use super::aabb::*; use super::hittable::*; use super::material::*; use crate::math::*; @@ -8,9 +11,25 @@ pub struct Sphere { material: Material, } -impl Sphere { - pub fn new(center: Point3, radius: f32, material: Material) -> Sphere { - Sphere { center, radius, material } +pub struct MovingSphere { + center1: Point3, + sphere: Sphere, + time_interval: Range, +} + +pub fn sphere(center: Point3, radius: f32, material: Material) -> Sphere { + Sphere { + center, + radius, + material, + } +} + +pub fn moving_sphere(sphere: Sphere, center1: Point3, time_interval: Range) -> MovingSphere { + MovingSphere { + sphere, + center1, + time_interval, } } @@ -39,4 +58,42 @@ impl Hittable for Sphere { None } + + fn bounding_box(&self, _time_interval: Range) -> Aabb { + Aabb::new( + self.center - Vec3::splat(self.radius), + self.center + Vec3::splat(self.radius), + ) + } +} + +impl Hittable for MovingSphere { + fn hit(&self, r: &Ray, t_min: f32, t_max: f32) -> Option { + let motion = match *self { + MovingSphere { + time_interval: + Range { + start: time0, + end: time1, + }, + sphere: Sphere { center, .. }, + center1, + } => ((r.time - time0) / (time1 - time0)) * (center1 - center), + }; + + let r1 = Ray::new(r.orig - motion, r.dir, r.time); + self.sphere + .hit(&r1, t_min, t_max) + .map(|h| Hit::new(h.point + motion, h.normal + motion, h.material, h.t)) + } + + fn bounding_box(&self, time_interval: Range) -> Aabb { + let offest_aabb = Aabb::new( + self.center1 - Vec3::splat(self.sphere.radius), + self.center1 + Vec3::splat(self.sphere.radius), + ); + self.sphere + .bounding_box(time_interval) + .surrounding_box(offest_aabb) + } } diff --git a/src/rt/hittable.rs b/src/rt/hittable.rs index 7f06a9c..5953442 100644 --- a/src/rt/hittable.rs +++ b/src/rt/hittable.rs @@ -1,12 +1,16 @@ -use crate::math::*; +use std::ops::Range; + +use super::aabb::*; use super::material::*; +use crate::math::*; -#[derive(PartialEq)] +#[derive(PartialEq, Clone, Copy, Debug)] pub enum Face { Front, Back, } +#[derive(Debug, Clone, Copy)] pub struct Hit<'a> { pub point: Point3, pub normal: Vec3, @@ -38,12 +42,20 @@ impl Hit<'_> { pub trait Hittable: Send + Sync { fn hit(&self, r: &Ray, t_min: f32, t_max: f32) -> Option; + + fn bounding_box(&self, _time_interval: Range) -> Aabb { + Aabb::infinite() + } } impl Hittable for Box { fn hit(&self, r: &Ray, t_min: f32, t_max: f32) -> Option { (**self).hit(r, t_min, t_max) } + + fn bounding_box(&self, time_interval: Range) -> Aabb { + (**self).bounding_box(time_interval) + } } impl Hittable for [T] { @@ -52,6 +64,9 @@ impl Hittable for [T] { let mut closest_hit = t_max; for o in self { + if !o.bounding_box(r.time..r.time).hit(&r, t_min, t_max) { + continue; + } let current_hit = o.hit(r, t_min, closest_hit); if let Some(ref hit) = current_hit { closest_hit = hit.t; @@ -61,4 +76,11 @@ impl Hittable for [T] { final_hit } -} \ No newline at end of file + + fn bounding_box(&self, time_interval: Range) -> Aabb { + self.iter() + .map(|h| h.bounding_box(time_interval.clone())) + .reduce(Aabb::surrounding_box) + .unwrap_or(Aabb::infinite()) + } +} diff --git a/src/rt/material.rs b/src/rt/material.rs index 26c8189..0859b33 100644 --- a/src/rt/material.rs +++ b/src/rt/material.rs @@ -4,12 +4,7 @@ use crate::math::*; use rand::prelude::*; -pub struct MaterialResponse { - pub new_ray: Ray, - pub attenuation: Color, -} - -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Material { Lambertian { albedo: Color, @@ -26,7 +21,7 @@ pub enum Material { } impl Material { - pub fn scatter(&self, r_in: &Ray, hit: &Hit, rng: &mut impl Rng) -> Option { + pub fn scatter(&self, r_in: &Ray, hit: &Hit, rng: &mut impl Rng) -> Ray { let random_unit_vec = random_on_unit_sphere(rng); match self { Material::Lambertian { albedo } => { @@ -34,22 +29,28 @@ impl Material { if near_zero(scatter_dir) { scatter_dir = hit.normal; } - Some(MaterialResponse { - new_ray: Ray::new(hit.point, scatter_dir), - attenuation: *albedo, - }) + Ray { + orig: hit.point, + dir: scatter_dir, + ..*r_in + } + .attenuate(*albedo) } Material::Metallic { albedo, roughness } => { let reflected = reflect(r_in.dir.normalize_or_zero(), hit.normal); let cos_theta = f32::min(Vec3::dot(-r_in.dir, hit.normal), 1.0); let f = reflectance(cos_theta, *albedo); - if Vec3::dot(reflected, hit.normal) >= 0.0 { - Some(MaterialResponse { - new_ray: Ray::new(hit.point, reflected + *roughness * random_unit_vec), - attenuation: f - }) + let r = Ray { + orig: hit.point, + dir: reflected + *roughness * random_unit_vec, + ..*r_in + } + .attenuate(f); + + if Vec3::dot(reflected, hit.normal) < 0.0 { + Ray { color: Color::ZERO, ..r } } else { - None + r } } Material::Dielectric { @@ -64,19 +65,22 @@ impl Material { }; let (refracted, reflectance, valid) = refract(r_in.dir.normalize_or_zero(), hit.normal, refraction_ratio); - let (scattered, color) = - if !valid || reflectance.max_element() > rng.sample(rand::distributions::Standard) { - let reflected = reflect(r_in.dir.normalize_or_zero(), hit.normal) - + *roughness * random_unit_vec; - (reflected, Color::ONE) - } else { - (refracted + *roughness * random_unit_vec, *albedo) - }; + let (scattered, color) = if !valid + || reflectance.max_element() > rng.sample(rand::distributions::Standard) + { + let reflected = reflect(r_in.dir.normalize_or_zero(), hit.normal) + + *roughness * random_unit_vec; + (reflected, Color::ONE) + } else { + (refracted + *roughness * random_unit_vec, *albedo) + }; - Some(MaterialResponse { - new_ray: Ray::new(hit.point, scattered), - attenuation: color, - }) + Ray { + orig: hit.point, + dir: scattered, + ..*r_in + } + .attenuate(color) } } }