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.
This commit is contained in:
parent
3ca8bc14e4
commit
699c308782
|
|
@ -32,7 +32,8 @@ fn simple_scene(bencher: &mut Criterion) {
|
|||
reflection_strength: 0.9,
|
||||
}),
|
||||
)
|
||||
.unwrap(),
|
||||
.unwrap()
|
||||
.as_mut_slice(),
|
||||
))],
|
||||
};
|
||||
b.iter(|| {
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
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<dyn std::error::Error>> {
|
|||
}),
|
||||
)?;
|
||||
println!("Building BVH...");
|
||||
let model_bvh: Box<dyn Aggregate<_>> = Box::new(BoundingVolumeHierarchy::build(model_object));
|
||||
let model_bvh: Box<dyn Aggregate<_>> =
|
||||
Box::new(BoundingVolumeHierarchy::build(model_object.as_mut_slice()));
|
||||
println!("Constructing Scene...");
|
||||
|
||||
let scene = Scene {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ mod wavefront_obj {
|
|||
pub fn load_obj<T: Real>(
|
||||
filename: &Path,
|
||||
material: Arc<dyn Material<T>>,
|
||||
) -> Result<Vec<Box<dyn Primitive<T>>>>
|
||||
) -> Result<Vec<Arc<dyn Primitive<T>>>>
|
||||
where
|
||||
T: SupersetOf<f32>,
|
||||
{
|
||||
|
|
@ -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<dyn Primitive<T>>)
|
||||
.map(|triangle| Arc::new(triangle) as Arc<dyn Primitive<T>>)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T> = BinaryTree<BoundingBox<T>, Box<dyn Primitive<T>>>;
|
||||
use std::cmp::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Stores a set of [Primitives](Primitive) and accelerates raycasting
|
||||
///
|
||||
|
|
@ -21,8 +17,16 @@ type Tree<T> = BinaryTree<BoundingBox<T>, Box<dyn Primitive<T>>>;
|
|||
/// 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<T: Real> {
|
||||
tree: Tree<T>,
|
||||
pub enum BoundingVolumeHierarchy<T: Real> {
|
||||
Node {
|
||||
bounds: BoundingBox<T>,
|
||||
left: Box<BoundingVolumeHierarchy<T>>,
|
||||
right: Box<BoundingVolumeHierarchy<T>>,
|
||||
},
|
||||
Leaf {
|
||||
bounds: BoundingBox<T>,
|
||||
primitives: Vec<Arc<dyn Primitive<T>>>,
|
||||
},
|
||||
}
|
||||
|
||||
fn centre<T: Real>(bounds: &BoundingBox<T>) -> Point3<T> {
|
||||
|
|
@ -34,66 +38,44 @@ fn centre<T: Real>(bounds: &BoundingBox<T>) -> Point3<T> {
|
|||
)
|
||||
}
|
||||
|
||||
struct PrimitiveInfo<T: Real>(BoundingBox<T>, Option<Box<dyn Primitive<T>>>);
|
||||
fn heuristic_split<T: Real>(
|
||||
primitives: &mut [Arc<dyn Primitive<T>>],
|
||||
bounds: &BoundingBox<T>,
|
||||
) -> 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<T: Real> BoundingVolumeHierarchy<T> {
|
||||
pub fn build<'a, I>(primitives: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = Box<dyn Primitive<T>>>,
|
||||
{
|
||||
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<dyn Primitive<T>>]) -> Self {
|
||||
BoundingVolumeHierarchy::build_from_slice(primitives)
|
||||
}
|
||||
|
||||
fn from_node_vec(nodes: Vec<PrimitiveInfo<T>>) -> Tree<T> {
|
||||
let overall_bounds = nodes
|
||||
pub fn build_from_slice(primitives: &mut [Arc<dyn Primitive<T>>]) -> 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<T>]) -> Tree<T> {
|
||||
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<T>) -> BoundingBox<T> {
|
||||
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<T: Real>(
|
|||
a: Option<IntersectionInfo<T>>,
|
||||
b: Option<IntersectionInfo<T>>,
|
||||
) -> Option<IntersectionInfo<T>> {
|
||||
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<T: Real> Intersect<T> for Tree<T> {
|
||||
fn intersect<'a>(&'a self, ray: &Ray<T>) -> Option<IntersectionInfo<T>> {
|
||||
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<T: Real> Intersect<T> for BoundingVolumeHierarchy<T> {
|
||||
fn intersect<'a>(&'a self, ray: &Ray<T>) -> Option<IntersectionInfo<T>> {
|
||||
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<T: Real> HasBoundingBox<T> for BoundingVolumeHierarchy<T> {
|
||||
fn bounding_box(&self) -> BoundingBox<T> {
|
||||
Self::get_bounds(&self.tree)
|
||||
BoundingBox::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Real> Aggregate<T> for BoundingVolumeHierarchy<T> {}
|
||||
|
||||
#[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<T: Arbitrary + Real> Arbitrary for Sphere<T> {
|
||||
fn arbitrary<G: Gen>(g: &mut G) -> Sphere<T> {
|
||||
let centre = <Point3<T> as Arbitrary>::arbitrary(g);
|
||||
let radius = <T as Arbitrary>::arbitrary(g);
|
||||
Sphere::new(centre, radius, Arc::new(LambertianMaterial::new_dummy()))
|
||||
}
|
||||
}
|
||||
|
||||
fn sphere_vec_to_primitive_box_vec<T: Real>(
|
||||
spheres: &Vec<Sphere<T>>,
|
||||
) -> Vec<Box<dyn Primitive<T>>> {
|
||||
let mut prims: Vec<Box<dyn Primitive<T>>> = 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<Sphere<f32>>) -> bool {
|
||||
let target = BoundingVolumeHierarchy::build(sphere_vec_to_primitive_box_vec(&spheres));
|
||||
|
||||
target.tree.count_leaves() == spheres.len()
|
||||
}
|
||||
}
|
||||
mod test {}
|
||||
|
|
|
|||
|
|
@ -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<f32> + SubsetOf<f32> + NormalizedToU32 {}
|
||||
pub trait Real: RealField + SupersetOf<f32> + SubsetOf<f32> + NormalizedToU32 + PartialOrd {}
|
||||
|
||||
impl NormalizedToU32 for f32 {
|
||||
fn normalized_to_u32(self, num_bits: usize) -> u32 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue