From 7ec30d855707061078fa43476fb2baf9e8a01d21 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Sun, 12 Feb 2023 13:04:08 -0800 Subject: [PATCH] rtiow: BVHTriangles faster BVH traversal. --- rtiow/renderer/src/bvh_triangles.rs | 257 ++++++++++++++++------------ 1 file changed, 150 insertions(+), 107 deletions(-) diff --git a/rtiow/renderer/src/bvh_triangles.rs b/rtiow/renderer/src/bvh_triangles.rs index e169fbb..1a9441e 100644 --- a/rtiow/renderer/src/bvh_triangles.rs +++ b/rtiow/renderer/src/bvh_triangles.rs @@ -29,7 +29,7 @@ impl BVHNode { } } -#[derive(PartialEq)] +#[derive(Clone, PartialEq)] pub struct Triangle { centroid: Vec3, verts: [Vec3; 3], @@ -170,10 +170,11 @@ where info!(" Predict: {}", bvh.bvh_nodes.capacity()); info!(" Actual: {}", bvh.bvh_nodes.len()); info!( - " Savings: {}", + " Savings: {} bytes", (bvh.bvh_nodes.capacity() - bvh.bvh_nodes.len()) * std::mem::size_of::() ); bvh.bvh_nodes.shrink_to_fit(); + //dbg!(&bvh); bvh } @@ -315,56 +316,72 @@ where } } - fn intersect_bvh(&self, r: Ray, node_idx: u32, t_min: f32, t_max: f32) -> Option { - // TODO(wathiede): visit nodes front to back and remove recursion. - let node = &self.bvh_nodes[node_idx as usize]; - if !node.aabb.hit(r, t_min, t_max) { - return None; - } - //dbg!(&self); + fn intersect_bvh(&self, r: Ray, t_min: f32, t_max: f32) -> Option { + let mut node = &self.bvh_nodes[0]; + let mut stack = Vec::with_capacity(2); + let mut nearest = None; + loop { + if node.is_leaf() { + let canditate = (node.left_first..(node.left_first + node.tri_count)) + // Map from idx to Triangle + .map(|idx| &self.triangles[self.triangle_index[idx as usize]]) + // Try to hit all triangles for this node, filtering out misses. + .filter_map(|tri| intersect_tri(r, &tri)) + // Find the nearest hit (if any). + .min_by(|a, b| a.t.partial_cmp(&b.t).unwrap()); - if node.is_leaf() { - return self - .triangles - .iter() - .map(|tri| { - if let Some(RayTriangleResult { t, p }) = intersect_tri(r, tri) { - // We don't support UV (yet?). - let uv = (0.5, 0.5); - let v0 = tri.verts[0]; - let v1 = tri.verts[1]; - let v2 = tri.verts[2]; - - let v0v1 = v1 - v0; - let v0v2 = v2 - v0; - let normal = cross(v0v1, v0v2).unit_vector(); - //println!("hit triangle {tri:?}"); - Some(HitRecord { - t, - uv, - p, - normal, - material: &self.material, - }) - } else { - None + // merge candidate with nearest. + nearest = match (&canditate, &nearest) { + (Some(_), None) => canditate, + (None, Some(_)) => nearest, + (Some(c), Some(n)) => { + //info!("merging {c:#?} and {n:#?}"); + if c.t < n.t { + canditate + } else { + nearest + } } - }) - .filter_map(|hr| hr) - .min_by(|a, b| a.t.partial_cmp(&b.t).unwrap()); - } else { - let r1 = self.intersect_bvh(r, node.left_first, t_min, t_max); - let r2 = self.intersect_bvh(r, node.left_first + 1, t_min, t_max); - // Merge results, if both hit, take the one closest to the ray origin (smallest t - // value). - match (&r1, &r2) { - (Some(_), None) => return r1, - (None, Some(_)) => return r2, - (None, None) => (), - (Some(rp1), Some(rp2)) => return if rp1.t < rp2.t { r1 } else { r2 }, + (None, None) => None, + }; + if stack.is_empty() { + break; + } + node = stack.pop().unwrap(); + continue; + } + + let child1 = &self.bvh_nodes[node.left_first as usize]; + let child2 = &self.bvh_nodes[node.left_first as usize + 1]; + let dist1 = child1.aabb.hit_distance(r, t_min, t_max); + let dist2 = child2.aabb.hit_distance(r, t_min, t_max); + // Swap c1/c2 & d1/d2 based on d1/d2. + let (child1, child2, dist1, dist2) = match (dist1, dist2) { + (Some(d1), Some(d2)) if d1 > d2 => (child2, child1, dist2, dist1), + (None, Some(_)) => (child2, child1, dist2, dist1), + _ => (child1, child2, dist1, dist2), + }; + + // dist1/child1 should now be the nearest hit. + + // If we missed dist1/child1, then we implicitly missed dist2/child2, so pop a child + // from the stack or exit the function. + if dist1.is_none() { + if stack.is_empty() { + break; + } + node = stack.pop().unwrap(); + } else { + // We hit child1, so process it next. + node = child1; + // If we also hit child2 save it on the stack so we can process it later. + if dist2.is_some() { + stack.push(child2); + } } } - None + nearest + .and_then(|rtr: RayTriangleResult| Some(rtr.hit_record_with_material(&self.material))) } } @@ -426,6 +443,7 @@ fn intersect_tri(r: Ray, tri: &Triangle) -> Option { return Some(RayTriangleResult { t, p: r.point_at_parameter(t), + tri: tri.clone(), }); } None @@ -436,7 +454,7 @@ where M: Material, { fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option { - self.intersect_bvh(r, 0, t_min, t_max) + self.intersect_bvh(r, t_min, t_max) } fn bounding_box(&self, _t_min: f32, _t_max: f32) -> Option { @@ -444,9 +462,33 @@ where } } +#[derive(Debug)] struct RayTriangleResult { t: f32, p: Vec3, + tri: Triangle, +} + +impl RayTriangleResult { + fn hit_record_with_material<'m>(self, material: &'m dyn Material) -> HitRecord<'m> { + // We don't support UV (yet?). + let uv = (0.5, 0.5); + let v0 = self.tri.verts[0]; + let v1 = self.tri.verts[1]; + let v2 = self.tri.verts[2]; + + let v0v1 = v1 - v0; + let v0v2 = v2 - v0; + let normal = cross(v0v1, v0v2).unit_vector(); + //println!("hit triangle {tri:?}"); + HitRecord { + t: self.t, + uv, + p: self.p, + normal, + material, + } + } } #[cfg(test)] @@ -461,67 +503,13 @@ mod tests { texture::ConstantTexture, }; use pretty_assertions::assert_eq; + use proptest::prelude::*; use std::{ io::{BufReader, Cursor}, sync::Arc, }; use stl::STL; - /* - #[test] - fn build_bvh() { - let stl_triangles: Vec<_> = (0..4) - .flat_map(|y| { - (0..2).map(move |x| { - let x = x as f32; - let y = y as f32; - stl::Triangle { - normal: [1., 0., 0.].into(), - verts: [ - [2. * x + 0., 2. * y + 0., 0.].into(), - [2. * x + 1., 2. * y + 0., 0.].into(), - [2. * x + 1., 2. * y + 1., 0.].into(), - ], - attr: 0, - } - }) - }) - .collect(); - let stl = STL { - header: [0; 80], - triangles: stl_triangles, - }; - /* - let mut bvh_triangles: Vec<_> = stl_triangles - .iter() - .map(|tri| { - let div3 = 1. / 3.; - let v0 = tri.verts[0]; - let v1 = tri.verts[1]; - let v2 = tri.verts[2]; - let centroid = (v0 + v1 + v2) * div3; - Triangle { - centroid, - verts: tri.verts, - } - }) - .collect(); - bvh_triangles.sort_by(|a, b| a.centroid.y.partial_cmp(&b.centroid.y).unwrap()); - let material = Lambertian::new(ConstantTexture::new([0., 0., 0.])); - let bvh_nodes = Default::default(); - let want = BVHTriangles { - triangles: bvh_triangles, - bvh_nodes, - material, - }; - */ - let material = Lambertian::new(ConstantTexture::new([0., 0., 0.])); - let bvh = BVHTriangles::new(&stl, material); - dbg!(&bvh); - assert_eq!(bvh.bvh_nodes.len(), 2 * bvh.triangles.len() - 2); - } - */ - #[test] fn compare_cuboid() { let c = Cuboid::new( @@ -579,7 +567,6 @@ mod tests { }) }) .collect(); - // These currently differ between STL and cuboid. if false { // Outward in at an angle. let sqrt2 = 2f32.sqrt(); @@ -591,8 +578,6 @@ mod tests { ]); } - // TODO(wathiede): proptest this, it's still not perfectly equal when rendering. - for r in rays.into_iter() { let c_hit = c .hit(r, 0., f32::MAX) @@ -616,4 +601,62 @@ mod tests { ); } } + + proptest! { + // TODO(wathiede): make this work. + //#[test] + fn compare_cuboid_proptest( + ox in -20.0f32..40.0, + oy in -20.0f32..40.0, + oz in -20.0f32..40.0, + dx in -1.0f32..1.0, + dy in -1.0f32..1.0, + dz in -1.0f32..1.0, + ) { + let r = Ray::new([ox,oy,oz].into(), Vec3::new(dx, dy, dz).unit_vector(), 0.5); + let c = Cuboid::new( + [0., 0., 0.].into(), + [20., 20., 20.].into(), + Arc::new(Dielectric::new(1.5)), + ); + let stl_cube = STL::parse( + BufReader::new(Cursor::new(include_bytes!("../stls/cube.stl"))), + false, + ) + .expect("failed to parse cube"); + + let s = BVHTriangles::new( + &stl_cube, + Dielectric::new(1.5), + //Metal::new(Vec3::new(0.8, 0.8, 0.8), 0.2), + //Lambertian::new(ConstantTexture::new(Vec3::new(1.0, 0.2, 0.2))), + ); + + let c_hit = c .hit(r, 0., f32::MAX); + let s_hit = s .hit(r, 0., f32::MAX); + + match (c_hit, s_hit) { + (Some(_), None)=>assert!(false, "hit cuboid but not STL"), + (None, Some(_))=>assert!(false, "hit STL but not cuboid"), + (Some(c_hit), Some(s_hit))=> { + assert!( + (c_hit.t - s_hit.t).abs() < 0.00001, + "{r:?} [t] c_hit: {c_hit:#?}, s_hit: {s_hit:#?}" + ); + // uv isn't valid for BVHTriangles. + // assert_eq!( c_hit.uv, s_hit.uv, "{i}: [uv] c_hit: {c_hit:?}, s_hit: {s_hit:?}"); + assert_eq!( + c_hit.p, s_hit.p, + "{r:?}: [p] c_hit: {c_hit:#?}, s_hit: {s_hit:#?}" + ); + assert_eq!( + c_hit.normal, s_hit.normal, + "{r:?}: [normal] c_hit: {c_hit:?}, s_hit: {s_hit:?}" + ); + }, + // It's okay if they both miss. + (None,None)=>(), + } + } + } }