From 699c308782c5e08b59a961530adf2407f851e873 Mon Sep 17 00:00:00 2001 From: Matthew Gordon Date: Tue, 11 Aug 2020 23:59:03 -0400 Subject: [PATCH] Rewrite BoundingVolumeHierarchy New BVH has much cleaner design and is also using a much better heuristic for dividing the scene. Massive speedup (~28x!), presumable from having a heuristic that actually works. This is still a simple heuristic (sort by bounding box centres along largest dimension, and then divide into equal halves) which can definitely be improved. --- benches/simple_scene.rs | 3 +- src/main.rs | 5 +- src/mesh.rs | 4 +- src/raycasting/bounding_volume_hierarchy.rs | 205 ++++++++------------ src/realtype.rs | 4 +- 5 files changed, 85 insertions(+), 136 deletions(-) diff --git a/benches/simple_scene.rs b/benches/simple_scene.rs index 36256b3..23aae6b 100644 --- a/benches/simple_scene.rs +++ b/benches/simple_scene.rs @@ -32,7 +32,8 @@ fn simple_scene(bencher: &mut Criterion) { reflection_strength: 0.9, }), ) - .unwrap(), + .unwrap() + .as_mut_slice(), ))], }; b.iter(|| { diff --git a/src/main.rs b/src/main.rs index 3311a07..1e17276 100644 --- a/src/main.rs +++ b/src/main.rs @@ -129,7 +129,7 @@ pub fn main() -> Result<(), Box> { let model_file_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test_data/stanford_bunny.obj"); println!("Loading object..."); - let model_object = load_obj( + let mut model_object = load_obj( &model_file_path, Arc::new(ReflectiveMaterial { colour: ColourRgbF::from_named(NamedColour::Yellow), @@ -138,7 +138,8 @@ pub fn main() -> Result<(), Box> { }), )?; println!("Building BVH..."); - let model_bvh: Box> = Box::new(BoundingVolumeHierarchy::build(model_object)); + let model_bvh: Box> = + Box::new(BoundingVolumeHierarchy::build(model_object.as_mut_slice())); println!("Constructing Scene..."); let scene = Scene { diff --git a/src/mesh.rs b/src/mesh.rs index 9fda9c4..89fc0c7 100644 --- a/src/mesh.rs +++ b/src/mesh.rs @@ -68,7 +68,7 @@ mod wavefront_obj { pub fn load_obj( filename: &Path, material: Arc>, - ) -> Result>>> + ) -> Result>>> where T: SupersetOf, { @@ -80,7 +80,7 @@ mod wavefront_obj { .flat_map(|object| object.groups.iter()) .flat_map(|group| group.polys.iter()) .flat_map(|poly| get_triangles(poly, &obj.position, &obj.normal, material.clone())) - .map(|triangle| Box::new(triangle) as Box>) + .map(|triangle| Arc::new(triangle) as Arc>) .collect()) } } diff --git a/src/raycasting/bounding_volume_hierarchy.rs b/src/raycasting/bounding_volume_hierarchy.rs index 5053445..f97f3cd 100644 --- a/src/raycasting/bounding_volume_hierarchy.rs +++ b/src/raycasting/bounding_volume_hierarchy.rs @@ -2,16 +2,12 @@ use super::{ Aggregate, BoundingBox, HasBoundingBox, Intersect, IntersectP, IntersectionInfo, Primitive, Ray, }; -use crate::util::binary_tree::BinaryTree; -use crate::util::morton::morton_order_value_3d; -use crate::util::normalizer::Point3Normalizer; use crate::Real; use nalgebra::{convert, Point3}; -use std::mem::swap; - -type Tree = BinaryTree, Box>>; +use std::cmp::Ordering; +use std::sync::Arc; /// Stores a set of [Primitives](Primitive) and accelerates raycasting /// @@ -21,8 +17,16 @@ type Tree = BinaryTree, Box>>; /// Each node knows the overall bounds of all it's children, which means that a ray that /// doesn't intersect the [BoundingBox](BoundingBox) of the node doesn't intersect any of /// the primitives stored in it's children. -pub struct BoundingVolumeHierarchy { - tree: Tree, +pub enum BoundingVolumeHierarchy { + Node { + bounds: BoundingBox, + left: Box>, + right: Box>, + }, + Leaf { + bounds: BoundingBox, + primitives: Vec>>, + }, } fn centre(bounds: &BoundingBox) -> Point3 { @@ -34,66 +38,44 @@ fn centre(bounds: &BoundingBox) -> Point3 { ) } -struct PrimitiveInfo(BoundingBox, Option>>); +fn heuristic_split( + primitives: &mut [Arc>], + bounds: &BoundingBox, +) -> usize { + let largest_dimension = bounds.largest_dimension(); + primitives.sort_unstable_by(|a, b| { + centre(&a.bounding_box())[largest_dimension] + .partial_cmp(¢re(&b.bounding_box())[largest_dimension]) + .unwrap_or(Ordering::Equal) + }); + primitives.len() / 2 +} impl BoundingVolumeHierarchy { - pub fn build<'a, I>(primitives: I) -> Self - where - I: IntoIterator>>, - { - let tree = Self::from_node_vec( - primitives - .into_iter() - .map(|primitive| PrimitiveInfo(primitive.bounding_box(), Some(primitive))) - .collect(), - ); - Self { tree } + pub fn build(primitives: &mut [Arc>]) -> Self { + BoundingVolumeHierarchy::build_from_slice(primitives) } - fn from_node_vec(nodes: Vec>) -> Tree { - let overall_bounds = nodes + pub fn build_from_slice(primitives: &mut [Arc>]) -> Self { + let bounds = primitives .iter() - .fold(BoundingBox::empty(), |a, PrimitiveInfo(b, _)| a.union(b)); - let normalizer = Point3Normalizer::new(overall_bounds); - let mut nodes = nodes; - nodes.sort_by(|PrimitiveInfo(a, _), PrimitiveInfo(b, _)| { - morton_order_value_3d(normalizer.normalize(centre(a))) - .cmp(&morton_order_value_3d(normalizer.normalize(centre(b)))) - }); - Self::from_sorted_nodes(nodes.as_mut_slice()) - } - - fn from_sorted_nodes(nodes: &mut [PrimitiveInfo]) -> Tree { - if nodes.len() >= 2 { - let midpoint = nodes.len() / 2; - let left = Box::new(Self::from_sorted_nodes(&mut nodes[..midpoint])); - let right = Box::new(Self::from_sorted_nodes(&mut nodes[midpoint..])); - let bounds = Self::get_bounds(&left).union(&Self::get_bounds(&right)); - Tree::Branch { - value: bounds, + .fold(BoundingBox::empty(), |acc, p| acc.union(&p.bounding_box())); + if primitives.len() <= 1 { + let primitives = primitives.iter().cloned().collect(); + BoundingVolumeHierarchy::Leaf { bounds, primitives } + } else { + let pivot = heuristic_split(primitives, &bounds); + let left = Box::new(BoundingVolumeHierarchy::build_from_slice( + &mut primitives[0..pivot], + )); + let right = Box::new(BoundingVolumeHierarchy::build_from_slice( + &mut primitives[pivot..], + )); + BoundingVolumeHierarchy::Node { + bounds, left, right, } - } else if nodes.len() == 1 { - let PrimitiveInfo(_, ref mut primitive_src) = nodes[0]; - let mut primitive = None; - swap(primitive_src, &mut primitive); - let primitive = primitive.unwrap(); - Tree::Leaf { value: primitive } - } else { - Tree::None - } - } - - pub fn get_bounds(tree: &Tree) -> BoundingBox { - match tree { - Tree::Branch { - value, - left: _, - right: _, - } => *value, - Tree::Leaf { value } => value.bounding_box(), - Tree::None => BoundingBox::empty(), } } } @@ -102,89 +84,54 @@ fn closest_intersection( a: Option>, b: Option>, ) -> Option> { - match (a, b) { - (Some(a), Some(b)) => { - if a.distance < b.distance { - Some(a) + match a { + None => b, + Some(a_info) => match b { + None => Some(a_info), + Some(b_info) => Some(if a_info.distance < b_info.distance { + a_info } else { - Some(b) - } - } - (Some(a), None) => Some(a), - (None, Some(b)) => Some(b), - (None, None) => None, - } -} - -impl Intersect for Tree { - fn intersect<'a>(&'a self, ray: &Ray) -> Option> { - match self { - Tree::Branch { - value: bounds, - left, - right, - } => { - if bounds.intersect(ray) { - closest_intersection(left.intersect(ray), right.intersect(ray)) - } else { - None - } - } - Tree::Leaf { value: primitive } => primitive.intersect(ray), - Tree::None => None, - } + b_info + }), + }, } } impl Intersect for BoundingVolumeHierarchy { fn intersect<'a>(&'a self, ray: &Ray) -> Option> { - self.tree.intersect(ray) + match self { + BoundingVolumeHierarchy::Node { + bounds, + left, + right, + } => { + if bounds.intersect(&ray) { + closest_intersection(left.intersect(&ray), right.intersect(&ray)) + } else { + None + } + } + BoundingVolumeHierarchy::Leaf { bounds, primitives } => { + if bounds.intersect(&ray) { + primitives + .iter() + .map(|elem| elem.intersect(&ray)) + .fold(None, |acc, elem| closest_intersection(acc, elem)) + } else { + None + } + } + } } } impl HasBoundingBox for BoundingVolumeHierarchy { fn bounding_box(&self) -> BoundingBox { - Self::get_bounds(&self.tree) + BoundingBox::empty() } } impl Aggregate for BoundingVolumeHierarchy {} #[cfg(test)] -mod test { - - use quickcheck::{Arbitrary, Gen}; - use quickcheck_macros::quickcheck; - - use super::*; - use crate::materials::LambertianMaterial; - use crate::raycasting::Sphere; - use nalgebra::Point3; - - use std::sync::Arc; - - impl Arbitrary for Sphere { - fn arbitrary(g: &mut G) -> Sphere { - let centre = as Arbitrary>::arbitrary(g); - let radius = ::arbitrary(g); - Sphere::new(centre, radius, Arc::new(LambertianMaterial::new_dummy())) - } - } - - fn sphere_vec_to_primitive_box_vec( - spheres: &Vec>, - ) -> Vec>> { - let mut prims: Vec>> = Vec::with_capacity(spheres.len()); - for sphere in spheres { - prims.push(Box::new(sphere.clone())); - } - prims - } - - #[quickcheck] - fn contains_expected_number_of_primitives(spheres: Vec>) -> bool { - let target = BoundingVolumeHierarchy::build(sphere_vec_to_primitive_box_vec(&spheres)); - - target.tree.count_leaves() == spheres.len() - } -} +mod test {} diff --git a/src/realtype.rs b/src/realtype.rs index 4c5dc95..def9adf 100644 --- a/src/realtype.rs +++ b/src/realtype.rs @@ -1,11 +1,11 @@ -use simba::scalar::{SubsetOf,SupersetOf}; use nalgebra::RealField; +use simba::scalar::{SubsetOf, SupersetOf}; pub trait NormalizedToU32 { fn normalized_to_u32(self, num_bits: usize) -> u32; } -pub trait Real: RealField + SupersetOf + SubsetOf + NormalizedToU32 {} +pub trait Real: RealField + SupersetOf + SubsetOf + NormalizedToU32 + PartialOrd {} impl NormalizedToU32 for f32 { fn normalized_to_u32(self, num_bits: usize) -> u32 {