use crate::mesh::{AABB, Triangle, TriangleMesh, Vec3}; /// BVH over a triangle mesh for fast nearest-point queries. pub struct BVH { nodes: Vec, tri_indices: Vec, } enum BVHNode { Leaf { bounds: AABB, first: usize, count: usize, }, Internal { bounds: AABB, left: usize, right: usize, }, } impl BVH { pub fn build(mesh: &TriangleMesh) -> Self { let n = mesh.triangles.len(); let mut tri_indices: Vec = (0..n).collect(); let mut centroids: Vec = mesh.triangles.iter().map(|t| t.centroid()).collect(); let mut nodes = Vec::with_capacity(2 * n); build_recursive( &mesh.triangles, &mut tri_indices, &mut centroids, &mut nodes, 0, n, ); Self { nodes, tri_indices } } /// Find the signed distance from point p to the mesh. /// /// Sign is determined by angle-weighted pseudonormal voting across all /// triangles within a tolerance of the nearest distance. This handles /// edge and vertex proximity correctly, where a single triangle's face /// normal would give ambiguous or wrong sign. pub fn signed_distance(&self, mesh: &TriangleMesh, p: Vec3) -> f64 { let mut best_dist_sq = f64::INFINITY; self.query_nearest_dist(mesh, p, 0, &mut best_dist_sq); if best_dist_sq == f64::INFINITY { return f64::INFINITY; } // Collect all triangles within a relative tolerance of the best distance. // This catches co-nearest triangles sharing an edge or vertex. let eps_factor = 1.0 + 1e-4; let threshold_sq = best_dist_sq * eps_factor * eps_factor + 1e-20; let mut neighbors: Vec<(usize, Vec3)> = Vec::new(); // (tri_idx, closest_point) self.collect_near(mesh, p, 0, threshold_sq, &mut neighbors); let sign = if neighbors.len() <= 1 { // Single closest triangle: use its face normal directly if let Some(&(tri_idx, closest)) = neighbors.first() { let to_point = p - closest; let normal = mesh.triangles[tri_idx].normal(); if to_point.dot(normal) >= 0.0 { 1.0 } else { -1.0 } } else { 1.0 } } else { // Multiple co-nearest triangles: angle-weighted normal vote. // Weight each triangle's contribution by the solid angle it subtends // at p, approximated by the triangle area divided by distance squared. let mut weighted_dot = 0.0f64; for &(tri_idx, closest) in &neighbors { let tri = &mesh.triangles[tri_idx]; let to_point = p - closest; let normal = tri.normal(); let area = tri.area(); let d_sq = to_point.dot(to_point); // Weight: area / (distance^2 + epsilon) — larger, closer faces dominate let weight = area / (d_sq + 1e-20); weighted_dot += to_point.dot(normal) * weight; } if weighted_dot >= 0.0 { 1.0 } else { -1.0 } }; best_dist_sq.sqrt() * sign } /// Find the nearest triangle index and unsigned distance. pub fn nearest_triangle(&self, mesh: &TriangleMesh, p: Vec3) -> (usize, f64) { let mut best_dist_sq = f64::INFINITY; let mut best_idx = 0; self.query_nearest_idx(mesh, p, 0, &mut best_dist_sq, &mut best_idx); (best_idx, best_dist_sq.sqrt()) } /// Find the minimum squared distance from p to any triangle. fn query_nearest_dist( &self, mesh: &TriangleMesh, p: Vec3, node_idx: usize, best_dist_sq: &mut f64, ) { match &self.nodes[node_idx] { BVHNode::Leaf { bounds, first, count } => { if bounds.distance_to_point(p).powi(2) > *best_dist_sq { return; } for i in *first..(*first + *count) { let tri_idx = self.tri_indices[i]; let tri = &mesh.triangles[tri_idx]; let (_, dist) = tri.closest_point(p); let dist_sq = dist * dist; if dist_sq < *best_dist_sq { *best_dist_sq = dist_sq; } } } BVHNode::Internal { bounds, left, right } => { if bounds.distance_to_point(p).powi(2) > *best_dist_sq { return; } let left_bounds = node_bounds(&self.nodes[*left]); let right_bounds = node_bounds(&self.nodes[*right]); let dl = left_bounds.distance_to_point(p); let dr = right_bounds.distance_to_point(p); if dl < dr { self.query_nearest_dist(mesh, p, *left, best_dist_sq); self.query_nearest_dist(mesh, p, *right, best_dist_sq); } else { self.query_nearest_dist(mesh, p, *right, best_dist_sq); self.query_nearest_dist(mesh, p, *left, best_dist_sq); } } } } /// Collect all (triangle_index, closest_point) pairs within threshold_sq of p. fn collect_near( &self, mesh: &TriangleMesh, p: Vec3, node_idx: usize, threshold_sq: f64, out: &mut Vec<(usize, Vec3)>, ) { match &self.nodes[node_idx] { BVHNode::Leaf { bounds, first, count } => { if bounds.distance_to_point(p).powi(2) > threshold_sq { return; } for i in *first..(*first + *count) { let tri_idx = self.tri_indices[i]; let tri = &mesh.triangles[tri_idx]; let (closest, dist) = tri.closest_point(p); if dist * dist <= threshold_sq { out.push((tri_idx, closest)); } } } BVHNode::Internal { bounds, left, right } => { if bounds.distance_to_point(p).powi(2) > threshold_sq { return; } self.collect_near(mesh, p, *left, threshold_sq, out); self.collect_near(mesh, p, *right, threshold_sq, out); } } } fn query_nearest_idx( &self, mesh: &TriangleMesh, p: Vec3, node_idx: usize, best_dist_sq: &mut f64, best_idx: &mut usize, ) { match &self.nodes[node_idx] { BVHNode::Leaf { bounds, first, count } => { if bounds.distance_to_point(p).powi(2) > *best_dist_sq { return; } for i in *first..(*first + *count) { let tri_idx = self.tri_indices[i]; let tri = &mesh.triangles[tri_idx]; let (_, dist) = tri.closest_point(p); let dist_sq = dist * dist; if dist_sq < *best_dist_sq { *best_dist_sq = dist_sq; *best_idx = tri_idx; } } } BVHNode::Internal { bounds, left, right } => { if bounds.distance_to_point(p).powi(2) > *best_dist_sq { return; } let left_bounds = node_bounds(&self.nodes[*left]); let right_bounds = node_bounds(&self.nodes[*right]); let dl = left_bounds.distance_to_point(p); let dr = right_bounds.distance_to_point(p); if dl < dr { self.query_nearest_idx(mesh, p, *left, best_dist_sq, best_idx); self.query_nearest_idx(mesh, p, *right, best_dist_sq, best_idx); } else { self.query_nearest_idx(mesh, p, *right, best_dist_sq, best_idx); self.query_nearest_idx(mesh, p, *left, best_dist_sq, best_idx); } } } } /// Count triangles whose centroid falls within a given AABB. pub fn count_in_region(&self, mesh: &TriangleMesh, region: &AABB) -> usize { self.count_recursive(mesh, region, 0) } fn count_recursive(&self, mesh: &TriangleMesh, region: &AABB, node_idx: usize) -> usize { match &self.nodes[node_idx] { BVHNode::Leaf { bounds, first, count } => { if !aabb_overlaps(bounds, region) { return 0; } let mut n = 0; for i in *first..(*first + *count) { let c = mesh.triangles[self.tri_indices[i]].centroid(); if point_in_aabb(c, region) { n += 1; } } n } BVHNode::Internal { bounds, left, right } => { if !aabb_overlaps(bounds, region) { return 0; } self.count_recursive(mesh, region, *left) + self.count_recursive(mesh, region, *right) } } } } fn node_bounds(node: &BVHNode) -> &AABB { match node { BVHNode::Leaf { bounds, .. } | BVHNode::Internal { bounds, .. } => bounds, } } fn aabb_overlaps(a: &AABB, b: &AABB) -> bool { a.min.x <= b.max.x && a.max.x >= b.min.x && a.min.y <= b.max.y && a.max.y >= b.min.y && a.min.z <= b.max.z && a.max.z >= b.min.z } fn point_in_aabb(p: Vec3, b: &AABB) -> bool { p.x >= b.min.x && p.x <= b.max.x && p.y >= b.min.y && p.y <= b.max.y && p.z >= b.min.z && p.z <= b.max.z } const MAX_LEAF_SIZE: usize = 8; const SAH_NUM_BINS: usize = 16; const SAH_TRAVERSAL_COST: f64 = 1.0; const SAH_INTERSECT_COST: f64 = 1.5; /// Surface area of an AABB (used as probability proxy in SAH). fn aabb_surface_area(b: &AABB) -> f64 { let e = b.extent(); 2.0 * (e.x * e.y + e.y * e.z + e.z * e.x) } /// SAH bin for accumulating triangle bounds during split evaluation. struct SAHBin { bounds: AABB, count: usize, } impl SAHBin { fn empty() -> Self { Self { bounds: AABB::empty(), count: 0 } } } /// Evaluate SAH cost for splitting at each bin boundary along the given axis. /// Returns (best_split_bin, best_cost). Split puts bins [0..split] left, [split..N] right. fn sah_best_split( triangles: &[Triangle], indices: &[usize], centroids: &[Vec3], start: usize, end: usize, parent_bounds: &AABB, axis: usize, ) -> (usize, f64) { let get_axis = |v: Vec3| match axis { 0 => v.x, 1 => v.y, _ => v.z, }; // Centroid bounds (tighter than triangle bounds) for bin mapping let mut centroid_min = f64::INFINITY; let mut centroid_max = f64::NEG_INFINITY; for &idx in &indices[start..end] { let c = get_axis(centroids[idx]); centroid_min = centroid_min.min(c); centroid_max = centroid_max.max(c); } let span = centroid_max - centroid_min; if span < 1e-12 { // All centroids coincide on this axis; can't split here return (SAH_NUM_BINS / 2, f64::INFINITY); } let inv_span = SAH_NUM_BINS as f64 / span; // Bin triangles let mut bins: Vec = (0..SAH_NUM_BINS).map(|_| SAHBin::empty()).collect(); for &idx in &indices[start..end] { let c = get_axis(centroids[idx]); let b = ((c - centroid_min) * inv_span) as usize; let b = b.min(SAH_NUM_BINS - 1); bins[b].bounds = bins[b].bounds.union(&AABB::from_triangle(&triangles[idx])); bins[b].count += 1; } // Sweep from left: accumulate bounds and counts for splits [0..i+1] | [i+1..N] let mut left_area = vec![0.0f64; SAH_NUM_BINS - 1]; let mut left_count = vec![0usize; SAH_NUM_BINS - 1]; let mut running_bounds = AABB::empty(); let mut running_count = 0usize; for i in 0..(SAH_NUM_BINS - 1) { running_bounds = running_bounds.union(&bins[i].bounds); running_count += bins[i].count; left_area[i] = aabb_surface_area(&running_bounds); left_count[i] = running_count; } // Sweep from right let mut right_area = vec![0.0f64; SAH_NUM_BINS - 1]; let mut right_count = vec![0usize; SAH_NUM_BINS - 1]; running_bounds = AABB::empty(); running_count = 0; for i in (0..(SAH_NUM_BINS - 1)).rev() { running_bounds = running_bounds.union(&bins[i + 1].bounds); running_count += bins[i + 1].count; right_area[i] = aabb_surface_area(&running_bounds); right_count[i] = running_count; } let parent_area = aabb_surface_area(parent_bounds); let inv_parent = if parent_area > 1e-12 { 1.0 / parent_area } else { 1.0 }; let mut best_cost = f64::INFINITY; let mut best_bin = SAH_NUM_BINS / 2; for i in 0..(SAH_NUM_BINS - 1) { if left_count[i] == 0 || right_count[i] == 0 { continue; } let cost = SAH_TRAVERSAL_COST + SAH_INTERSECT_COST * inv_parent * (left_area[i] * left_count[i] as f64 + right_area[i] * right_count[i] as f64); if cost < best_cost { best_cost = cost; best_bin = i; } } (best_bin, best_cost) } fn build_recursive( triangles: &[Triangle], indices: &mut [usize], centroids: &mut [Vec3], nodes: &mut Vec, start: usize, end: usize, ) -> usize { let count = end - start; let mut bounds = AABB::empty(); for &idx in &indices[start..end] { bounds = bounds.union(&AABB::from_triangle(&triangles[idx])); } if count <= MAX_LEAF_SIZE { let node_idx = nodes.len(); nodes.push(BVHNode::Leaf { bounds, first: start, count }); return node_idx; } // SAH split: evaluate all three axes, pick lowest cost let leaf_cost = SAH_INTERSECT_COST * count as f64; let mut best_axis = 0usize; let mut best_bin = SAH_NUM_BINS / 2; let mut best_cost = f64::INFINITY; for axis in 0..3 { let (bin, cost) = sah_best_split( triangles, indices, centroids, start, end, &bounds, axis, ); if cost < best_cost { best_cost = cost; best_bin = bin; best_axis = axis; } } // If SAH says a leaf is cheaper, or no valid split found, fall back to median let use_median = best_cost >= leaf_cost || best_cost == f64::INFINITY; let mid = if use_median { // Median split along longest axis let axis = bounds.longest_axis(); let get_axis = |v: Vec3| match axis { 0 => v.x, 1 => v.y, _ => v.z, }; indices[start..end].sort_unstable_by(|&a, &b| { get_axis(centroids[a]) .partial_cmp(&get_axis(centroids[b])) .unwrap_or(std::cmp::Ordering::Equal) }); start + count / 2 } else { // Partition around the SAH split bin let get_axis = |v: Vec3| match best_axis { 0 => v.x, 1 => v.y, _ => v.z, }; let mut centroid_min = f64::INFINITY; let mut centroid_max = f64::NEG_INFINITY; for &idx in &indices[start..end] { let c = get_axis(centroids[idx]); centroid_min = centroid_min.min(c); centroid_max = centroid_max.max(c); } let span = centroid_max - centroid_min; let inv_span = SAH_NUM_BINS as f64 / span; // Partition: triangles in bins [0..=best_bin] go left let slice = &mut indices[start..end]; let mut lo = 0usize; let mut hi = slice.len(); while lo < hi { let c = get_axis(centroids[slice[lo]]); let b = ((c - centroid_min) * inv_span) as usize; let b = b.min(SAH_NUM_BINS - 1); if b <= best_bin { lo += 1; } else { hi -= 1; slice.swap(lo, hi); } } let mid = start + lo; // Guard: if partition degenerates, fall back to midpoint if mid == start || mid == end { start + count / 2 } else { mid } }; let node_idx = nodes.len(); nodes.push(BVHNode::Leaf { bounds: AABB::empty(), first: 0, count: 0 }); // placeholder let left = build_recursive(triangles, indices, centroids, nodes, start, mid); let right = build_recursive(triangles, indices, centroids, nodes, mid, end); nodes[node_idx] = BVHNode::Internal { bounds, left, right }; node_idx } #[cfg(test)] mod tests { use super::*; fn make_unit_cube_mesh() -> TriangleMesh { // 12 triangles forming a unit cube centered at origin let h = 0.5; let corners = [ Vec3::new(-h, -h, -h), Vec3::new( h, -h, -h), Vec3::new( h, h, -h), Vec3::new(-h, h, -h), Vec3::new(-h, -h, h), Vec3::new( h, -h, h), Vec3::new( h, h, h), Vec3::new(-h, h, h), ]; // CCW winding viewed from outside each face let faces: [(usize, usize, usize); 12] = [ (0,2,1), (0,3,2), // -Z face: normal points -Z (4,5,6), (4,6,7), // +Z face: normal points +Z (0,1,5), (0,5,4), // -Y face: normal points -Y (2,3,7), (2,7,6), // +Y face: normal points +Y (0,4,7), (0,7,3), // -X face: normal points -X (1,2,6), (1,6,5), // +X face: normal points +X ]; let mut triangles = Vec::new(); let mut bounds = AABB::empty(); for &(a, b, c) in &faces { let tri = Triangle { v: [corners[a], corners[b], corners[c]] }; for v in &tri.v { bounds.min = bounds.min.component_min(*v); bounds.max = bounds.max.component_max(*v); } triangles.push(tri); } TriangleMesh { triangles, bounds } } #[test] fn bvh_builds_for_cube() { let mesh = make_unit_cube_mesh(); let bvh = BVH::build(&mesh); assert!(!bvh.nodes.is_empty()); } #[test] fn signed_distance_inside_cube() { let mesh = make_unit_cube_mesh(); let bvh = BVH::build(&mesh); let d = bvh.signed_distance(&mesh, Vec3::new(0.0, 0.0, 0.0)); assert!(d < 0.0, "origin should be inside cube, got {d}"); } #[test] fn signed_distance_outside_cube() { let mesh = make_unit_cube_mesh(); let bvh = BVH::build(&mesh); let d = bvh.signed_distance(&mesh, Vec3::new(2.0, 0.0, 0.0)); assert!(d > 0.0, "point far from cube should be outside, got {d}"); } #[test] fn signed_distance_on_edge_correct_sign() { // Point near a cube edge: the multi-triangle voting should give // consistent sign even though two face normals are involved. let mesh = make_unit_cube_mesh(); let bvh = BVH::build(&mesh); // Slightly outside the cube near the +X/+Y edge let d = bvh.signed_distance(&mesh, Vec3::new(0.6, 0.6, 0.0)); assert!(d > 0.0, "point outside near edge should be positive, got {d}"); // Slightly inside near the same edge let d2 = bvh.signed_distance(&mesh, Vec3::new(0.4, 0.4, 0.0)); assert!(d2 < 0.0, "point inside near edge should be negative, got {d2}"); } #[test] fn signed_distance_on_vertex_correct_sign() { // Point near a cube vertex: three face normals are involved. let mesh = make_unit_cube_mesh(); let bvh = BVH::build(&mesh); let d = bvh.signed_distance(&mesh, Vec3::new(0.7, 0.7, 0.7)); assert!(d > 0.0, "point outside near vertex should be positive, got {d}"); } #[test] fn nearest_triangle_finds_closest() { let mesh = make_unit_cube_mesh(); let bvh = BVH::build(&mesh); let (_, dist) = bvh.nearest_triangle(&mesh, Vec3::new(1.5, 0.0, 0.0)); assert!((dist - 1.0).abs() < 0.01, "distance to +X face should be ~1.0, got {dist}"); } }