rtiow: BVHTriangles faster BVH traversal.
This commit is contained in:
parent
f51d3396f4
commit
7ec30d8557
@ -29,7 +29,7 @@ impl BVHNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct Triangle {
|
pub struct Triangle {
|
||||||
centroid: Vec3,
|
centroid: Vec3,
|
||||||
verts: [Vec3; 3],
|
verts: [Vec3; 3],
|
||||||
@ -170,10 +170,11 @@ where
|
|||||||
info!(" Predict: {}", bvh.bvh_nodes.capacity());
|
info!(" Predict: {}", bvh.bvh_nodes.capacity());
|
||||||
info!(" Actual: {}", bvh.bvh_nodes.len());
|
info!(" Actual: {}", bvh.bvh_nodes.len());
|
||||||
info!(
|
info!(
|
||||||
" Savings: {}",
|
" Savings: {} bytes",
|
||||||
(bvh.bvh_nodes.capacity() - bvh.bvh_nodes.len()) * std::mem::size_of::<BVHNode>()
|
(bvh.bvh_nodes.capacity() - bvh.bvh_nodes.len()) * std::mem::size_of::<BVHNode>()
|
||||||
);
|
);
|
||||||
bvh.bvh_nodes.shrink_to_fit();
|
bvh.bvh_nodes.shrink_to_fit();
|
||||||
|
//dbg!(&bvh);
|
||||||
bvh
|
bvh
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,56 +316,72 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn intersect_bvh(&self, r: Ray, node_idx: u32, t_min: f32, t_max: f32) -> Option<HitRecord> {
|
fn intersect_bvh(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
|
||||||
// TODO(wathiede): visit nodes front to back and remove recursion.
|
let mut node = &self.bvh_nodes[0];
|
||||||
let node = &self.bvh_nodes[node_idx as usize];
|
let mut stack = Vec::with_capacity(2);
|
||||||
if !node.aabb.hit(r, t_min, t_max) {
|
let mut nearest = None;
|
||||||
return None;
|
loop {
|
||||||
}
|
if node.is_leaf() {
|
||||||
//dbg!(&self);
|
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() {
|
// merge candidate with nearest.
|
||||||
return self
|
nearest = match (&canditate, &nearest) {
|
||||||
.triangles
|
(Some(_), None) => canditate,
|
||||||
.iter()
|
(None, Some(_)) => nearest,
|
||||||
.map(|tri| {
|
(Some(c), Some(n)) => {
|
||||||
if let Some(RayTriangleResult { t, p }) = intersect_tri(r, tri) {
|
//info!("merging {c:#?} and {n:#?}");
|
||||||
// We don't support UV (yet?).
|
if c.t < n.t {
|
||||||
let uv = (0.5, 0.5);
|
canditate
|
||||||
let v0 = tri.verts[0];
|
} else {
|
||||||
let v1 = tri.verts[1];
|
nearest
|
||||||
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
|
|
||||||
}
|
}
|
||||||
})
|
(None, None) => None,
|
||||||
.filter_map(|hr| hr)
|
};
|
||||||
.min_by(|a, b| a.t.partial_cmp(&b.t).unwrap());
|
if stack.is_empty() {
|
||||||
} else {
|
break;
|
||||||
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);
|
node = stack.pop().unwrap();
|
||||||
// Merge results, if both hit, take the one closest to the ray origin (smallest t
|
continue;
|
||||||
// value).
|
}
|
||||||
match (&r1, &r2) {
|
|
||||||
(Some(_), None) => return r1,
|
let child1 = &self.bvh_nodes[node.left_first as usize];
|
||||||
(None, Some(_)) => return r2,
|
let child2 = &self.bvh_nodes[node.left_first as usize + 1];
|
||||||
(None, None) => (),
|
let dist1 = child1.aabb.hit_distance(r, t_min, t_max);
|
||||||
(Some(rp1), Some(rp2)) => return if rp1.t < rp2.t { r1 } else { r2 },
|
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<RayTriangleResult> {
|
|||||||
return Some(RayTriangleResult {
|
return Some(RayTriangleResult {
|
||||||
t,
|
t,
|
||||||
p: r.point_at_parameter(t),
|
p: r.point_at_parameter(t),
|
||||||
|
tri: tri.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@ -436,7 +454,7 @@ where
|
|||||||
M: Material,
|
M: Material,
|
||||||
{
|
{
|
||||||
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
|
fn hit(&self, r: Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
|
||||||
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<AABB> {
|
fn bounding_box(&self, _t_min: f32, _t_max: f32) -> Option<AABB> {
|
||||||
@ -444,9 +462,33 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct RayTriangleResult {
|
struct RayTriangleResult {
|
||||||
t: f32,
|
t: f32,
|
||||||
p: Vec3,
|
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)]
|
#[cfg(test)]
|
||||||
@ -461,67 +503,13 @@ mod tests {
|
|||||||
texture::ConstantTexture,
|
texture::ConstantTexture,
|
||||||
};
|
};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
use proptest::prelude::*;
|
||||||
use std::{
|
use std::{
|
||||||
io::{BufReader, Cursor},
|
io::{BufReader, Cursor},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use stl::STL;
|
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]
|
#[test]
|
||||||
fn compare_cuboid() {
|
fn compare_cuboid() {
|
||||||
let c = Cuboid::new(
|
let c = Cuboid::new(
|
||||||
@ -579,7 +567,6 @@ mod tests {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
// These currently differ between STL and cuboid.
|
|
||||||
if false {
|
if false {
|
||||||
// Outward in at an angle.
|
// Outward in at an angle.
|
||||||
let sqrt2 = 2f32.sqrt();
|
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() {
|
for r in rays.into_iter() {
|
||||||
let c_hit = c
|
let c_hit = c
|
||||||
.hit(r, 0., f32::MAX)
|
.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)=>(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user