use std; use std::fmt; use std::time::Instant; use rand; use rand::Rng; use crate::aabb::surrounding_box; use crate::aabb::AABB; use crate::hitable::Hit; use crate::hitable::HitRecord; use crate::ray::Ray; enum BVHNode { Leaf(Box), Branch { left: Box, right: Box, bbox: Option, }, } impl fmt::Display for BVHNode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { BVHNode::Leaf(ref hit) => write!( f, "Leaf: {}", hit.bounding_box(0., 0.) .map_or("NO BBOX".to_owned(), |bb| bb.to_string()) ), BVHNode::Branch { bbox, .. } => write!( f, "Branch: {}", // TODO(wathiede): removing this .clone() results in a complaint about moving out // of a borrow. bbox.clone() .map_or("NO BBOX".to_owned(), |bb| bb.to_string()) ), } } } // Lint is wrong when dealing with Box #[cfg_attr(feature = "cargo-clippy", allow(borrowed_box))] fn box_x_compare(ah: &Box, bh: &Box) -> std::cmp::Ordering { match (ah.bounding_box(0., 0.), bh.bounding_box(0., 0.)) { (Some(box_left), Some(box_right)) => { box_left.min().x.partial_cmp(&box_right.min().x).unwrap() } _ => panic!("hit missing bounding box"), } } #[cfg_attr(feature = "cargo-clippy", allow(borrowed_box))] fn box_y_compare(ah: &Box, bh: &Box) -> std::cmp::Ordering { match (ah.bounding_box(0., 0.), bh.bounding_box(0., 0.)) { (Some(box_left), Some(box_right)) => { box_left.min().y.partial_cmp(&box_right.min().y).unwrap() } _ => panic!("hit missing bounding box"), } } #[cfg_attr(feature = "cargo-clippy", allow(borrowed_box))] fn box_z_compare(ah: &Box, bh: &Box) -> std::cmp::Ordering { match (ah.bounding_box(0., 0.), bh.bounding_box(0., 0.)) { (Some(box_left), Some(box_right)) => { box_left.min().z.partial_cmp(&box_right.min().z).unwrap() } _ => panic!("hit missing bounding box"), } } // Return the first element from the vector, which should be the only element. // TODO(wathiede): we really want a .first_into() (.first() doesn't work because it // returns a reference.) fn vec_first_into(v: Vec) -> T { if v.len() != 1 { panic!(format!( "vec_first_into called for vector length != 1, length {}", v.len() )); } v.into_iter().next().unwrap() } fn vec_split_into(v: Vec, offset: usize) -> (Vec, Vec) { let mut left_half = Vec::new(); let mut right_half = Vec::new(); v.into_iter().enumerate().for_each(|(i, h)| { if i < offset { left_half.push(h); } else { right_half.push(h); } }); (left_half, right_half) } impl BVHNode { fn new(l: Vec>, t_min: f32, t_max: f32) -> BVHNode { if l.len() == 1 { BVHNode::Leaf(vec_first_into(l)) } else { let mut l: Vec> = l.into_iter().collect(); let mut rng = rand::thread_rng(); match rng.gen_range::(0, 3) { 0 => l.sort_by(box_x_compare), 1 => l.sort_by(box_y_compare), 2 => l.sort_by(box_z_compare), val => panic!("unknown axis {}", val), } let half = l.len() / 2; let (left_half, right_half) = vec_split_into(l, half); let left = Box::new(BVHNode::new(left_half, t_min, t_max)); let right = Box::new(BVHNode::new(right_half, t_min, t_max)); let bbox = BVHNode::surrounding_box(left.as_ref(), right.as_ref(), t_min, t_max); BVHNode::Branch { left, right, bbox } } } fn surrounding_box(left: &Hit, right: &Hit, t_min: f32, t_max: f32) -> Option { match ( left.bounding_box(t_min, t_max), right.bounding_box(t_min, t_max), ) { (Some(left_bbox), Some(right_bbox)) => Some(surrounding_box(&left_bbox, &right_bbox)), (None, Some(right_bbox)) => Some(right_bbox), (Some(left_bbox), None) => Some(left_bbox), (None, None) => None, } } fn volume(&self) -> f32 { let bbox = self.bounding_box(0., 0.).unwrap(); (bbox.min().x - bbox.max().x).abs() * (bbox.min().y - bbox.max().y).abs() * (bbox.min().z - bbox.max().z).abs() } } impl Hit for BVHNode { fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option { match self { BVHNode::Leaf(ref hit) => hit.hit(r, t_min, t_max), BVHNode::Branch { ref left, ref right, ref bbox, } => match bbox { Some(ref bbox) => { if bbox.hit(r, t_min, t_max) { match (left.hit(r, t_min, t_max), right.hit(r, t_min, t_max)) { (Some(hit_left), Some(hit_right)) => if hit_left.t < hit_right.t { Some(hit_left) } else { Some(hit_right) }, (Some(hit_left), None) => Some(hit_left), (None, Some(hit_right)) => Some(hit_right), (None, None) => None, }; } None } None => None, }, } } fn bounding_box(&self, t_min: f32, t_max: f32) -> Option { match self { BVHNode::Leaf(ref hit) => hit.bounding_box(t_min, t_max), BVHNode::Branch { ref bbox, .. } => bbox.clone(), } } } pub struct BVH { root: BVHNode, } impl BVH { pub fn new(l: Vec>, t_min: f32, t_max: f32) -> BVH { let count = l.len(); let start = Instant::now(); let bvh = BVH { root: BVHNode::new(l, t_min, t_max), }; let runtime = start.elapsed(); info!( "BVH build time {}.{} seconds for {} hitables", runtime.as_secs(), runtime.subsec_millis(), count ); bvh } } fn print_tree(f: &mut fmt::Formatter, depth: usize, bvhn: &BVHNode) -> fmt::Result { let vol = bvhn.volume(); writeln!(f, "{:.*}{}{}", 2, vol, " ".repeat(depth * 2), bvhn)?; if let BVHNode::Branch { left, right, .. } = bvhn { print_tree(f, depth + 1, left)?; print_tree(f, depth + 1, right)?; } Ok(()) } impl fmt::Display for BVH { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "Root")?; print_tree(f, 1, &self.root) } } impl Hit for BVH { fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option { self.root.hit(r, t_min, t_max) } fn bounding_box(&self, t_min: f32, t_max: f32) -> Option { self.root.bounding_box(t_min, t_max) } } #[cfg(test)] mod tests { use crate::aabb::AABB; use crate::material::Lambertian; use crate::material::Metal; use crate::sphere::Sphere; use crate::texture::ConstantTexture; use crate::vec3::Vec3; use super::*; #[test] fn bbox_two_spheres() { let two_spheres_bvh = BVH::new( vec![ Box::new(Sphere::new( Vec3::new(0., 0., 0.), 0.5, Lambertian::new(ConstantTexture::new(Vec3::new(0.1, 0.2, 0.5))), )), Box::new(Sphere::new( Vec3::new(1., 0., 0.), 0.5, Metal::new(Vec3::new(0.6, 0.6, 0.6), 0.2), )), ], 0., 1., ); assert_eq!( AABB::new(Vec3::new(-0.5, -0.5, -0.5), Vec3::new(1.5, 0.5, 0.5)), two_spheres_bvh.bounding_box(0., 1.).unwrap(), "BVH:\n{}", two_spheres_bvh ); } #[test] fn bbox_three_spheres() { let three_spheres_bvh = BVH::new( vec![ Box::new(Sphere::new( Vec3::new(0., 0., 0.), 0.5, Lambertian::new(ConstantTexture::new(Vec3::new(0.1, 0.2, 0.5))), )), Box::new(Sphere::new( Vec3::new(1., 0., 0.), 0.5, Metal::new(Vec3::new(0.6, 0.6, 0.6), 0.2), )), Box::new(Sphere::new( Vec3::new(0., 1., 0.), 0.5, Metal::new(Vec3::new(0.6, 0.6, 0.6), 0.2), )), ], 0., 1., ); assert_eq!( AABB::new(Vec3::new(-0.5, -0.5, -0.5), Vec3::new(1.5, 1.5, 0.5)), three_spheres_bvh.bounding_box(0., 1.).unwrap(), "BVH:\n{}", three_spheres_bvh ); } }