From ee2a44c8f52303f43132a9f78fb43604559c1bf7 Mon Sep 17 00:00:00 2001 From: Valery Meleshkin Date: Sat, 29 Jan 2022 16:36:40 +0100 Subject: [PATCH 1/7] Motion blur. My implementatin of a moving sphere is very differrent from the one in the book: instead of duplicating an object, it relies on the instancing approach, described later in the book. End of Chapter 2. --- src/main.rs | 44 ++++++++++++++++++++++++++++------------ src/math.rs | 7 +++++-- src/math/ray.rs | 5 +++-- src/rt/camera.rs | 9 ++++++++- src/rt/geometry.rs | 50 +++++++++++++++++++++++++++++++++++++++++++--- src/rt/material.rs | 6 +++--- 6 files changed, 97 insertions(+), 24 deletions(-) diff --git a/src/main.rs b/src/main.rs index c7a1f97..927f23c 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,30 +79,38 @@ 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 = 128; + let image_width = 1280u32; let image_height = (image_width as f32 / aspect_ratio) as u32; // World @@ -107,6 +124,7 @@ fn main() { Point3::Y, 20.0, aspect_ratio, + 0.0..1.0, 0.1, Some(10.0), ); 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..11aeb08 100644 --- a/src/math/ray.rs +++ b/src/math/ray.rs @@ -4,11 +4,12 @@ use super::*; pub struct Ray { pub orig: Point3, pub dir: Vec3, + pub time: f32, } 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} } pub fn at(&self, t: f32) -> Point3 { 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..de50393 100644 --- a/src/rt/geometry.rs +++ b/src/rt/geometry.rs @@ -1,3 +1,5 @@ +use std::ops::Range; + use super::hittable::*; use super::material::*; use crate::math::*; @@ -8,9 +10,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, } } @@ -40,3 +58,29 @@ impl Hittable for Sphere { None } } + +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, + radius: _, + material: _, + }, + 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)) + } +} diff --git a/src/rt/material.rs b/src/rt/material.rs index 26c8189..d83c7e4 100644 --- a/src/rt/material.rs +++ b/src/rt/material.rs @@ -35,7 +35,7 @@ impl Material { scatter_dir = hit.normal; } Some(MaterialResponse { - new_ray: Ray::new(hit.point, scatter_dir), + new_ray: Ray::new(hit.point, scatter_dir, r_in.time), attenuation: *albedo, }) } @@ -45,7 +45,7 @@ impl Material { 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), + new_ray: Ray::new(hit.point, reflected + *roughness * random_unit_vec, r_in.time), attenuation: f }) } else { @@ -74,7 +74,7 @@ impl Material { }; Some(MaterialResponse { - new_ray: Ray::new(hit.point, scattered), + new_ray: Ray::new(hit.point, scattered, r_in.time), attenuation: color, }) } From 0e7a2f561c8e104f05cf5b1181589346fca8dfb2 Mon Sep 17 00:00:00 2001 From: Valery Meleshkin Date: Tue, 1 Feb 2022 10:57:01 +0100 Subject: [PATCH 2/7] Axis aligned bounding boxes. --- src/main.rs | 12 ++++++---- src/rt.rs | 2 ++ src/rt/aabb.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++++ src/rt/geometry.rs | 18 +++++++++++++++ src/rt/hittable.rs | 30 ++++++++++++++++++++++-- 5 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 src/rt/aabb.rs diff --git a/src/main.rs b/src/main.rs index 927f23c..bb98c06 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,13 +63,15 @@ fn random_scene(rng: &mut impl Rng) -> Vec> { 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, + world.push(Box::new(AabbCache::new( + geometry::moving_sphere(geometry::sphere(center, 0.2, mat), center2, 0.0..1.0), 0.0..1.0, ))); } else { - world.push(Box::new(geometry::sphere(center, 0.2, mat))); + world.push(Box::new(AabbCache::new( + geometry::sphere(center, 0.2, mat), + 0.0..1.0, + ))); } } } @@ -109,7 +111,7 @@ fn random_scene(rng: &mut impl Rng) -> Vec> { fn main() { // Image let aspect_ratio = 16.0 / 9.0; - let samples_per_pixel = 128; + let samples_per_pixel = 16; let image_width = 1280u32; let image_height = (image_width as f32 / aspect_ratio) as u32; diff --git a/src/rt.rs b/src/rt.rs index 8449783..4ad79fd 100644 --- a/src/rt.rs +++ b/src/rt.rs @@ -2,10 +2,12 @@ mod camera; pub mod geometry; mod hittable; mod material; +mod aabb; pub use camera::*; pub use hittable::*; pub use material::*; +pub use aabb::*; use crate::color_output::*; use crate::math::*; diff --git a/src/rt/aabb.rs b/src/rt/aabb.rs new file mode 100644 index 0000000..3ecab6f --- /dev/null +++ b/src/rt/aabb.rs @@ -0,0 +1,57 @@ +use crate::math::*; + +use super::Hittable; + +#[derive(Clone, Copy)] +pub struct Aabb { + pub min: Point3, + pub max: Point3, +} + +impl Aabb { + pub fn surrounding_box(self, other: Aabb) -> Aabb { + Aabb { + min: self.min.min(other.min), + max: self.max.max(other.max), + } + } + + pub fn hit(&self, r: &Ray, t_min: f32, t_max: f32) -> bool { + 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 + } +} + +pub struct AabbCache { + pub object: T, + aabb: Option, +} + +impl AabbCache { + pub fn new(object: T, time_interval: std::ops::Range) -> Self { + let mut res = AabbCache { object, aabb: None }; + res.aabb = res.object.bounding_box(time_interval); + res + } +} + +impl Hittable for AabbCache { + fn hit(&self, r: &Ray, t_min: f32, t_max: f32) -> Option { + self.object.hit(r, t_min, t_max) + } + + fn bounding_box(&self, _time_interval: std::ops::Range) -> Option { + self.aabb + } +} diff --git a/src/rt/geometry.rs b/src/rt/geometry.rs index de50393..a8f4ad1 100644 --- a/src/rt/geometry.rs +++ b/src/rt/geometry.rs @@ -1,5 +1,6 @@ use std::ops::Range; +use super::aabb::*; use super::hittable::*; use super::material::*; use crate::math::*; @@ -57,6 +58,13 @@ impl Hittable for Sphere { None } + + fn bounding_box(&self, _time_interval: Range) -> Option { + Some(Aabb { + min: self.center - Vec3::splat(self.radius), + max: self.center + Vec3::splat(self.radius), + }) + } } impl Hittable for MovingSphere { @@ -83,4 +91,14 @@ impl Hittable for MovingSphere { .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) -> Option { + let offest_aabb = Aabb { + min: self.center1 - Vec3::splat(self.sphere.radius), + max: self.center1 + Vec3::splat(self.sphere.radius), + }; + self.sphere + .bounding_box(time_interval) + .map(|aabb| aabb.surrounding_box(offest_aabb)) + } } diff --git a/src/rt/hittable.rs b/src/rt/hittable.rs index 7f06a9c..ba1d06d 100644 --- a/src/rt/hittable.rs +++ b/src/rt/hittable.rs @@ -1,5 +1,8 @@ -use crate::math::*; +use std::ops::Range; + +use super::aabb::*; use super::material::*; +use crate::math::*; #[derive(PartialEq)] pub enum Face { @@ -38,12 +41,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) -> Option { + None + } } 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) -> Option { + (**self).bounding_box(time_interval) + } } impl Hittable for [T] { @@ -52,6 +63,13 @@ impl Hittable for [T] { let mut closest_hit = t_max; for o in self { + if !o + .bounding_box(r.time..r.time) + .map(|aabb| aabb.hit(&r, t_min, t_max)) + .unwrap_or(true) + { + continue; + } let current_hit = o.hit(r, t_min, closest_hit); if let Some(ref hit) = current_hit { closest_hit = hit.t; @@ -61,4 +79,12 @@ impl Hittable for [T] { final_hit } -} \ No newline at end of file + + fn bounding_box(&self, time_interval: Range) -> Option { + self.iter() + .map(|h| h.bounding_box(time_interval.clone())) + .filter(|aabb| aabb.is_some()) + .map(|aabb| aabb.unwrap()) + .reduce(Aabb::surrounding_box) + } +} From d2a4c578cc04132b52b2dec994db42d9edf07821 Mon Sep 17 00:00:00 2001 From: Valery Meleshkin Date: Wed, 2 Feb 2022 22:09:46 +0100 Subject: [PATCH 3/7] First shot at BVH. Apparently simplifications I made in SAH sweep backfired :( --- src/main.rs | 14 ++-- src/rt.rs | 2 + src/rt/aabb.rs | 47 +++++++++-- src/rt/bvh.rs | 197 +++++++++++++++++++++++++++++++++++++++++++++ src/rt/geometry.rs | 29 +++---- src/rt/hittable.rs | 17 ++-- 6 files changed, 265 insertions(+), 41 deletions(-) create mode 100644 src/rt/bvh.rs diff --git a/src/main.rs b/src/main.rs index bb98c06..e316eca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,15 +63,13 @@ fn random_scene(rng: &mut impl Rng) -> Vec> { if mat_prob < 0.8 { let center2 = center + Vec3::new(0.0, rng.sample::(StandardDist) * 0.5, 0.0); - world.push(Box::new(AabbCache::new( - geometry::moving_sphere(geometry::sphere(center, 0.2, mat), center2, 0.0..1.0), - 0.0..1.0, - ))); - } else { - world.push(Box::new(AabbCache::new( + 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))); } } } @@ -117,7 +115,7 @@ fn main() { // 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( @@ -158,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(r, &world, &mut rng); } output_color(color * color_scale) }) diff --git a/src/rt.rs b/src/rt.rs index 4ad79fd..d49448f 100644 --- a/src/rt.rs +++ b/src/rt.rs @@ -3,11 +3,13 @@ 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::*; diff --git a/src/rt/aabb.rs b/src/rt/aabb.rs index 3ecab6f..8013672 100644 --- a/src/rt/aabb.rs +++ b/src/rt/aabb.rs @@ -2,21 +2,54 @@ use crate::math::*; use super::Hittable; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub struct Aabb { pub min: Point3, pub max: Point3, + infinite: bool, } impl Aabb { - pub fn surrounding_box(self, other: Aabb) -> 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; @@ -35,12 +68,16 @@ impl Aabb { pub struct AabbCache { pub object: T, - aabb: Option, + aabb: Aabb, } impl AabbCache { + #[allow(dead_code)] pub fn new(object: T, time_interval: std::ops::Range) -> Self { - let mut res = AabbCache { object, aabb: None }; + let mut res = AabbCache { + object, + aabb: Aabb::infinite(), + }; res.aabb = res.object.bounding_box(time_interval); res } @@ -51,7 +88,7 @@ impl Hittable for AabbCache { self.object.hit(r, t_min, t_max) } - fn bounding_box(&self, _time_interval: std::ops::Range) -> Option { + fn bounding_box(&self, _time_interval: std::ops::Range) -> Aabb { self.aabb } } diff --git a/src/rt/bvh.rs b/src/rt/bvh.rs new file mode 100644 index 0000000..b5bcec4 --- /dev/null +++ b/src/rt/bvh.rs @@ -0,0 +1,197 @@ +use std::ops::Range; + +use crate::math::*; + +use super::Aabb; +use super::Hittable; + +pub struct Bvh { + objects: Vec, + nodes: BvhNode, +} + +#[derive(Debug)] +enum BvhNode { + Node { + aabb: Aabb, + leafs_count: usize, + children: Vec, + }, + Leaf { + aabb: Aabb, + object_idx: usize, + }, +} + +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::Node { + children, + leafs_count, + aabb: root_box, + } + } + + fn objects_count(&self) -> usize { + match self { + Self::Leaf { .. } => 1, + Self::Node { leafs_count, .. } => *leafs_count, + } + } + + fn aabb(&self) -> Aabb { + match self { + Self::Leaf { aabb, .. } => *aabb, + Self::Node { aabb, .. } => *aabb, + } + } + + fn sah_sweep_split(mut self) -> Self { + let mut nodes_to_split: Vec<&mut BvhNode> = vec![&mut self]; + loop { + let current = match nodes_to_split.pop() { + None => break, + Some(cur) => cur, + }; + match current { + BvhNode::Leaf { .. } => {} + BvhNode::Node { aabb, children, .. } => { + if children.len() < 3 { + continue; + } + let centroids = aabb.doubled_centroid(); + // sweep only across the longes axis, + // it's faster and simplifies the code a lot + let axis = (0..2) + .map(|axis| (axis, centroids[axis])) + .filter(|c| !c.1.is_nan()) + .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap()) + .map(|(axis, _)| axis) + .unwrap_or(0 as usize); + + 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 SA + 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()]; + 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; + } + 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; + } + } + let pivot = sa_sums + .iter() + .enumerate() + .filter(|(_, &sa)| !sa.is_nan()) + .min_by(|(_, sa1), (_, sa2)| sa1.partial_cmp(sa2).unwrap()) + .map(|(min_idx, _)| min_idx) + .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 right = children.split_off(pivot + 1); + if right.is_empty() { + // current node couldn't be split, leave it alone + // otherwise it'll cause an infinite loop + continue; + } + let right_node = BvhNode::new_node(right); + let left_node = BvhNode::new_node(children.drain(..).collect()); + children.push(left_node); + children.push(right_node); + nodes_to_split.extend(children.iter_mut()); + } + } + } + // println!("{:#?}", self); + 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::Leaf { + aabb: o.bounding_box(time_interval.clone()), + 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![&self.nodes]; + + loop { + let current = match nodes_to_try.pop() { + None => break, + Some(cur) => cur, + }; + match current { + BvhNode::Node { aabb, children, .. } => { + if aabb.hit(r, t_min, t_max) { + nodes_to_try.extend(children.iter().map(|c| c)) + } + } + BvhNode::Leaf { aabb, object_idx } => { + if aabb.hit(r, t_min, t_max) { + 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/geometry.rs b/src/rt/geometry.rs index a8f4ad1..c776584 100644 --- a/src/rt/geometry.rs +++ b/src/rt/geometry.rs @@ -59,11 +59,11 @@ impl Hittable for Sphere { None } - fn bounding_box(&self, _time_interval: Range) -> Option { - Some(Aabb { - min: self.center - Vec3::splat(self.radius), - max: self.center + Vec3::splat(self.radius), - }) + fn bounding_box(&self, _time_interval: Range) -> Aabb { + Aabb::new( + self.center - Vec3::splat(self.radius), + self.center + Vec3::splat(self.radius), + ) } } @@ -76,12 +76,7 @@ impl Hittable for MovingSphere { start: time0, end: time1, }, - sphere: - Sphere { - center, - radius: _, - material: _, - }, + sphere: Sphere { center, .. }, center1, } => ((r.time - time0) / (time1 - time0)) * (center1 - center), }; @@ -92,13 +87,13 @@ impl Hittable for MovingSphere { .map(|h| Hit::new(h.point + motion, h.normal + motion, h.material, h.t)) } - fn bounding_box(&self, time_interval: Range) -> Option { - let offest_aabb = Aabb { - min: self.center1 - Vec3::splat(self.sphere.radius), - max: self.center1 + Vec3::splat(self.sphere.radius), - }; + 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) - .map(|aabb| aabb.surrounding_box(offest_aabb)) + .surrounding_box(offest_aabb) } } diff --git a/src/rt/hittable.rs b/src/rt/hittable.rs index ba1d06d..c61325f 100644 --- a/src/rt/hittable.rs +++ b/src/rt/hittable.rs @@ -42,8 +42,8 @@ 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) -> Option { - None + fn bounding_box(&self, _time_interval: Range) -> Aabb { + Aabb::infinite() } } @@ -52,7 +52,7 @@ impl Hittable for Box { (**self).hit(r, t_min, t_max) } - fn bounding_box(&self, time_interval: Range) -> Option { + fn bounding_box(&self, time_interval: Range) -> Aabb { (**self).bounding_box(time_interval) } } @@ -63,11 +63,7 @@ impl Hittable for [T] { let mut closest_hit = t_max; for o in self { - if !o - .bounding_box(r.time..r.time) - .map(|aabb| aabb.hit(&r, t_min, t_max)) - .unwrap_or(true) - { + 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); @@ -80,11 +76,10 @@ impl Hittable for [T] { final_hit } - fn bounding_box(&self, time_interval: Range) -> Option { + fn bounding_box(&self, time_interval: Range) -> Aabb { self.iter() .map(|h| h.bounding_box(time_interval.clone())) - .filter(|aabb| aabb.is_some()) - .map(|aabb| aabb.unwrap()) .reduce(Aabb::surrounding_box) + .unwrap_or(Aabb::infinite()) } } From bb595ced1c77377e76e4bd2fe019cb6d04e20bf1 Mon Sep 17 00:00:00 2001 From: Valery Meleshkin Date: Wed, 2 Feb 2022 22:41:42 +0100 Subject: [PATCH 4/7] Sweeping across all axes and picking the best split improves traversal more than 2x. Build phase could use some optimization though. I'm going to call it done for now. End of Chapter 3. --- src/main.rs | 2 +- src/rt/aabb.rs | 31 +--------- src/rt/bvh.rs | 159 +++++++++++++++++++++++++++---------------------- 3 files changed, 91 insertions(+), 101 deletions(-) diff --git a/src/main.rs b/src/main.rs index e316eca..932085e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -109,7 +109,7 @@ fn random_scene(rng: &mut impl Rng) -> Vec> { fn main() { // Image let aspect_ratio = 16.0 / 9.0; - let samples_per_pixel = 16; + let samples_per_pixel = 32; let image_width = 1280u32; let image_height = (image_width as f32 / aspect_ratio) as u32; diff --git a/src/rt/aabb.rs b/src/rt/aabb.rs index 8013672..2404ce7 100644 --- a/src/rt/aabb.rs +++ b/src/rt/aabb.rs @@ -1,7 +1,5 @@ use crate::math::*; -use super::Hittable; - #[derive(Clone, Copy, Debug)] pub struct Aabb { pub min: Point3, @@ -64,31 +62,4 @@ impl Aabb { t_max_1 > t_min_1 } -} - -pub struct AabbCache { - pub object: T, - aabb: Aabb, -} - -impl AabbCache { - #[allow(dead_code)] - pub fn new(object: T, time_interval: std::ops::Range) -> Self { - let mut res = AabbCache { - object, - aabb: Aabb::infinite(), - }; - res.aabb = res.object.bounding_box(time_interval); - res - } -} - -impl Hittable for AabbCache { - fn hit(&self, r: &Ray, t_min: f32, t_max: f32) -> Option { - self.object.hit(r, t_min, t_max) - } - - fn bounding_box(&self, _time_interval: std::ops::Range) -> Aabb { - self.aabb - } -} +} \ No newline at end of file diff --git a/src/rt/bvh.rs b/src/rt/bvh.rs index b5bcec4..77a3871 100644 --- a/src/rt/bvh.rs +++ b/src/rt/bvh.rs @@ -10,7 +10,7 @@ pub struct Bvh { nodes: BvhNode, } -#[derive(Debug)] +#[derive(Debug, Clone)] enum BvhNode { Node { aabb: Aabb, @@ -54,6 +54,20 @@ impl BvhNode { } } + /* + 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![&mut self]; loop { @@ -61,80 +75,85 @@ impl BvhNode { None => break, Some(cur) => cur, }; - match current { - BvhNode::Leaf { .. } => {} - BvhNode::Node { aabb, children, .. } => { - if children.len() < 3 { - continue; - } - let centroids = aabb.doubled_centroid(); - // sweep only across the longes axis, - // it's faster and simplifies the code a lot - let axis = (0..2) - .map(|axis| (axis, centroids[axis])) - .filter(|c| !c.1.is_nan()) - .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap()) - .map(|(axis, _)| axis) - .unwrap_or(0 as usize); - - 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 SA - 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()]; - 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; - } - 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 + if let BvhNode::Node { children, .. } = current { + 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; } - } - let pivot = sa_sums - .iter() - .enumerate() - .filter(|(_, &sa)| !sa.is_nan()) - .min_by(|(_, sa1), (_, sa2)| sa1.partial_cmp(sa2).unwrap()) - .map(|(min_idx, _)| min_idx) - .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 right = children.split_off(pivot + 1); - if right.is_empty() { - // current node couldn't be split, leave it alone - // otherwise it'll cause an infinite loop - continue; - } - let right_node = BvhNode::new_node(right); - let left_node = BvhNode::new_node(children.drain(..).collect()); - children.push(left_node); - children.push(right_node); - nodes_to_split.extend(children.iter_mut()); + // 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()); } } - // println!("{:#?}", self); self } } From a4b849935e4489919b6ef81964debca680983276 Mon Sep 17 00:00:00 2001 From: Valery Meleshkin Date: Fri, 11 Feb 2022 16:15:03 +0100 Subject: [PATCH 5/7] A bugfix: BVH traversal should use "closest hit" instead of "t_max" as it makes no sense to check nodes further than a hit we've already found. --- src/rt/bvh.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rt/bvh.rs b/src/rt/bvh.rs index 77a3871..93ad058 100644 --- a/src/rt/bvh.rs +++ b/src/rt/bvh.rs @@ -190,12 +190,12 @@ impl Hittable for Bvh { }; match current { BvhNode::Node { aabb, children, .. } => { - if aabb.hit(r, t_min, t_max) { + if aabb.hit(r, t_min, closest_hit) { nodes_to_try.extend(children.iter().map(|c| c)) } } BvhNode::Leaf { aabb, object_idx } => { - if aabb.hit(r, t_min, t_max) { + if aabb.hit(r, t_min, closest_hit) { let object = &self.objects[*object_idx]; let current_hit = object.hit(r, t_min, closest_hit); if let Some(ref hit) = current_hit { From ab6b70c6d3e1a2021e4753f3a066932979f87479 Mon Sep 17 00:00:00 2001 From: Valery Meleshkin Date: Sat, 9 Jul 2022 14:34:02 +0200 Subject: [PATCH 6/7] Updating dependencies and setting aggressive optimization. --- Cargo.lock | 303 ++++++++++++++++++++++++++++++++++++++++++++++++----- Cargo.toml | 8 +- 2 files changed, 279 insertions(+), 32 deletions(-) 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"] } From 69616edc1bb951c7d4382e161c16b9699d3a7ab7 Mon Sep 17 00:00:00 2001 From: Valery Meleshkin Date: Fri, 8 Jul 2022 22:56:14 +0200 Subject: [PATCH 7/7] - Reduce allocations during BVH traversal - Make color part of a ray, to make its propagation straightforward. --- src/main.rs | 2 +- src/math/ray.rs | 30 ++++++++++++-- src/rt.rs | 20 ++------- src/rt/bvh.rs | 101 +++++++++++++++++++++++---------------------- src/rt/hittable.rs | 3 +- src/rt/material.rs | 62 +++++++++++++++------------- 6 files changed, 117 insertions(+), 101 deletions(-) diff --git a/src/main.rs b/src/main.rs index 932085e..5bb5335 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,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, &mut rng); + color += ray_color(&mut r, &world, &mut rng); } output_color(color * color_scale) }) diff --git a/src/math/ray.rs b/src/math/ray.rs index 11aeb08..8e43882 100644 --- a/src/math/ray.rs +++ b/src/math/ray.rs @@ -1,18 +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, time: f32) -> Ray { - Ray {orig, dir, time} + 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 d49448f..a888891 100644 --- a/src/rt.rs +++ b/src/rt.rs @@ -15,28 +15,16 @@ 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; } } @@ -45,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/bvh.rs b/src/rt/bvh.rs index 93ad058..565d12e 100644 --- a/src/rt/bvh.rs +++ b/src/rt/bvh.rs @@ -11,46 +11,45 @@ pub struct Bvh { } #[derive(Debug, Clone)] -enum BvhNode { +enum BvhNodeValue { Node { - aabb: Aabb, leafs_count: usize, children: Vec, }, Leaf { - aabb: Aabb, 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()) + .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::Node { - children, - leafs_count, + BvhNode { aabb: root_box, + value: BvhNodeValue::Node { + children, + leafs_count, + }, } } fn objects_count(&self) -> usize { - match self { - Self::Leaf { .. } => 1, - Self::Node { leafs_count, .. } => *leafs_count, - } - } - - fn aabb(&self) -> Aabb { - match self { - Self::Leaf { aabb, .. } => *aabb, - Self::Node { aabb, .. } => *aabb, + match self.value { + BvhNodeValue::Leaf { .. } => 1, + BvhNodeValue::Node { leafs_count, .. } => leafs_count, } } @@ -60,22 +59,24 @@ impl BvhNode { - 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 + - 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: + 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 + - 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![&mut 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 BvhNode::Node { children, .. } = current { + if let BvhNodeValue::Node { ref mut children, .. } = current.value { if children.len() < 3 { continue; } @@ -85,8 +86,8 @@ impl BvhNode { 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]; + 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 @@ -98,7 +99,7 @@ impl BvhNode { }) }); // Merging an AABB with itself won't change its surface area - let mut aabb_acc = children.first().map(|c| c.aabb()).unwrap(); + 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 @@ -106,27 +107,27 @@ impl BvhNode { // 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()); + 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, + // 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(); + 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()); + 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 + // Find the split with the smallest (on this axis) + // associated surface area sums let (pivot, &sa_lr) = sa_sums .iter() .enumerate() @@ -164,9 +165,9 @@ impl Bvh { let leafs: Vec = objects .iter() .enumerate() - .map(|(idx, o)| BvhNode::Leaf { + .map(|(idx, o)| BvhNode { aabb: o.bounding_box(time_interval.clone()), - object_idx: idx, + value: BvhNodeValue::Leaf { object_idx: idx }, }) .collect(); @@ -181,27 +182,27 @@ 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![&self.nodes]; + 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, }; - match current { - BvhNode::Node { aabb, children, .. } => { - if aabb.hit(r, t_min, closest_hit) { - nodes_to_try.extend(children.iter().map(|c| c)) - } + 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)) } - BvhNode::Leaf { aabb, object_idx } => { - if aabb.hit(r, t_min, closest_hit) { - 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; - } + 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; } } } @@ -211,6 +212,6 @@ impl Hittable for Bvh { } fn bounding_box(&self, _time_interval: std::ops::Range) -> Aabb { - self.nodes.aabb() + self.nodes.aabb } } diff --git a/src/rt/hittable.rs b/src/rt/hittable.rs index c61325f..5953442 100644 --- a/src/rt/hittable.rs +++ b/src/rt/hittable.rs @@ -4,12 +4,13 @@ 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, diff --git a/src/rt/material.rs b/src/rt/material.rs index d83c7e4..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, r_in.time), - 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, r_in.time), - 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, r_in.time), - attenuation: color, - }) + Ray { + orig: hit.point, + dir: scattered, + ..*r_in + } + .attenuate(color) } } }