The BVH
A Bounding Volume Hierarchy (BVH) organizes geometric primitives (like triangles) into a tree structure of bounding volumes. The root volume encloses the entire model, and children subdivide primitives into smaller subsets.
BVH Node:
1# Bounding Volume Hierarchy Node2class BVHNode:3 def __init__(self, bounding_box, primitives=None):4 self.bbox = bounding_box # AABB bounding volume5 self.primitives = primitives # List of geometric objects (if leaf)6 self.left = None # Left child7 self.right = None # Right child8 self.is_leaf = True
Building hierarchies requires enclosing sub-nodes. We merge boundary boxes by finding the minimum and maximum coordinate components of both volumes.
AABB Merging:
1# Merging two Axis-Aligned Bounding Boxes (AABB)2class AABB:3 def __init__(self, min_pt, max_pt):4 self.min = min_pt5 self.max = max_pt67 def merge(self, other):8 # Union box min is the component-wise minimum9 new_min = [min(a, b) for a, b in zip(self.min, other.min)]10 # Union box max is the component-wise maximum11 new_max = [max(a, b) for a, b in zip(self.max, other.max)]12 return AABB(new_min, new_max)
A BVH is built top-down by sorting geometric primitive centroids along the longest bounding box axis and splitting them into left and right subsets recursively.
Top-Down Construction:
1# Top-Down BVH Construction2def build_bvh(primitives, depth=0):3 bbox = AABB.from_primitives(primitives)4 node = BVHNode(bbox)56 if len(primitives) <= MAX_LEAF_PRIMITIVES:7 node.primitives = primitives8 return node910 # Split primitives along the longest axis11 axis = bbox.longest_axis()12 primitives.sort(key=lambda p: p.centroid()[axis])13 mid = len(primitives) // 21415 node.left = build_bvh(primitives[:mid], depth + 1)16 node.right = build_bvh(primitives[mid:], depth + 1)17 node.is_leaf = False18 return node
To minimize ray intersection steps, we estimate partition costs based on surface areas of the children bounding boxes and the number of primitives they enclose.
SAH Cost Logic:
1# Surface Area Heuristic (SAH) split evaluation2def evaluate_sah_cost(bounds, left_bounds, right_bounds, left_count, right_count):3 sa_parent = bounds.surface_area()4 sa_left = left_bounds.surface_area()5 sa_right = right_bounds.surface_area()67 # Cost = Traversal_Cost + (SA_Left/SA_Parent)*Count_Left*Intersect_Cost8 # + (SA_Right/SA_Parent)*Count_Right*Intersect_Cost9 cost = 1.0 + (sa_left / sa_parent) * left_count * 0.8 + (sa_right / sa_parent) * right_count * 0.810 return cost
The "Slab Method" tests intersections of a ray against three sets of parallel planes that define the bounding box limits, yielding entrance and exit parametric intervals.
Slab Intersection:
1# Slab-method Ray-AABB intersection check2def intersect_ray_aabb(ray_origin, ray_dir, bbox):3 t_min = float('-inf')4 t_max = float('inf')56 for i in range(3): # For X, Y, Z axes7 invD = 1.0 / ray_dir[i]8 t0 = (bbox.min[i] - ray_origin[i]) * invD9 t1 = (bbox.max[i] - ray_origin[i]) * invD1011 # Swap values if invD is negative12 if invD < 0.0:13 t0, t1 = t1, t01415 t_min = max(t0, t_min)16 t_max = min(t1, t_max)1718 if t_max <= t_min:19 return False, 0.0 # Miss2021 return True, t_min # Hit
The Möller-Trumbore algorithm performs fast ray-triangle intersections by projecting coordinates directly into barycentric space without computing planar equations.
Möller-Trumbore:
1# Möller-Trumbore Ray-Triangle intersection check2def intersect_ray_triangle(ray_origin, ray_dir, v0, v1, v2):3 edge1 = v1 - v04 edge2 = v2 - v05 h = cross(ray_dir, edge2)6 a = dot(edge1, h)78 if -1e-6 < a < 1e-6:9 return False, 0.0, 0.0, 0.0 # Parallel1011 f = 1.0 / a12 s = ray_origin - v013 u = f * dot(s, h)14 if u < 0.0 or u > 1.0:15 return False, 0.0, 0.0, 0.01617 q = cross(s, edge1)18 v = f * dot(ray_dir, q)19 if v < 0.0 or u + v > 1.0:20 return False, 0.0, 0.0, 0.02122 t = f * dot(edge2, q)23 return t > 1e-6, t, u, v
To find the closest triangle on a complex mesh, we traverse the BVH hierarchy, calculating distances to bounding volumes and pruning sub-nodes that cannot contain closer elements.
Closest Primitive Query:
1# Finding closest point on mesh using BVH2def closest_point_bvh(node, target_pt, best_dist=float('inf'), best_pt=None):3 if distance_to_box(target_pt, node.bbox) >= best_dist:4 return best_pt, best_dist # Prune branch56 if node.is_leaf:7 for triangle in node.primitives:8 pt, d = closest_point_on_triangle(target_pt, triangle)9 if d < best_dist:10 best_dist = d11 best_pt = pt12 return best_pt, best_dist1314 # Search nearest branch first15 # ...
To detect collision intersections between two meshes, we intersect their BVHs. We recursively traverse sub-nodes only when parent bounding boxes overlap, avoiding O(N²) triangle checks.
Mesh Collision:
1# BVH-to-BVH mesh intersection clash detection2def intersect_bvhs(nodeA, nodeB, overlaps):3 if not boxes_overlap(nodeA.bbox, nodeB.bbox):4 return # Prune56 if nodeA.is_leaf and nodeB.is_leaf:7 # Check actual triangle-triangle overlaps at leaves8 for triA in nodeA.primitives:9 for triB in nodeB.primitives:10 if triangles_intersect(triA, triB):11 overlaps.append((triA, triB))12 return1314 # Traverse child tree pairs recursively15 intersect_bvhs(nodeA.left, nodeB.left, overlaps)16 intersect_bvhs(nodeA.left, nodeB.right, overlaps)17 intersect_bvhs(nodeA.right, nodeB.left, overlaps)18 intersect_bvhs(nodeA.right, nodeB.right, overlaps)