Skip to content

Acceleration Structures — BVH and Grids for Ray Tracing

DodaTech Updated 2026-06-21 6 min read

In this tutorial, you'll learn about Acceleration Structures. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Acceleration structures are spatial data structures that reduce the number of ray-object intersection tests from O(N) to O(log N) by organizing primitives into hierarchies or grids that enable fast ray traversal.

What You'll Learn & Why It Matters

In this tutorial, you will learn the two main acceleration structures for Ray Tracing: Bounding Volume Hierarchies (BVH) and uniform grids. You will implement a BVH and measure the performance improvement over naive intersection testing.

Real-world use: Every production ray tracer and GPU Ray Tracing API (NVIDIA RTX, AMD HIP RT) uses BVHs internally. Without acceleration structures, rendering a scene with millions of triangles would be impossible.

Prerequisites

  • Ray Tracing Basics (previous)
  • Data structures knowledge
  • Python with time measurement

Learning Path

flowchart LR
  A[Ray Tracing Basics] --> B[Acceleration Structures]
  B --> C[Path Tracing]
  B --> D[Voxel Rendering]
  B --> E[Real-Time GI]
  C --> F[BRDF and Materials]
  B:::current

  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px

Why Acceleration Matters

A naive ray tracer tests every ray against every primitive. With 100,000 triangles and 1,000,000 rays, that is 10^11 intersection tests. A BVH reduces this to about 10^6 tests — a 100,000x improvement.

import time
import numpy as np

def naive_intersection(ray, triangles):
    hits = []
    for tri in triangles:
        t = ray_triangle_intersect(ray, tri)
        if t is not None:
            hits.append((t, tri))
    if not hits:
        return None
    return min(hits, key=lambda x: x[0])

# Benchmark
triangles = [generate_random_triangle() for _ in range(10000)]
start = time.time()
hit = naive_intersection(test_ray, triangles)
elapsed = time.time() - start
print(f"Naive: {elapsed*1000:.2f}ms for {len(triangles)} triangles")

Bounding Volume Hierarchy (BVH)

A BVH is a Binary Tree where each node stores a bounding box (AABB) that encloses all primitives in its subtree. To intersect a ray with the BVH, you test the ray against nodes top-down: if it misses the node's AABB, you skip the entire subtree.

class AABB:
    def __init__(self, min_pos, max_pos):
        self.min_pos = np.array(min_pos)
        self.max_pos = np.array(max_pos)

    def intersect(self, ray):
        tmin = (self.min_pos - ray.origin) / ray.direction
        tmax = (self.max_pos - ray.origin) / ray.direction
        t1 = np.minimum(tmin, tmax)
        t2 = np.maximum(tmin, tmax)
        t_near = np.max(t1)
        t_far = np.min(t2)
        return t_near <= t_far and t_far > 0

class BVHNode:
    def __init__(self, triangles, depth=0):
        self.bbox = self.compute_bbox(triangles)
        self.left = None
        self.right = None
        self.triangles = []
        max_depth = 16
        if len(triangles) <= 4 or depth >= max_depth:
            self.triangles = triangles
            return
        axis = depth % 3
        sorted_tris = sorted(triangles, key=lambda t: t.centroid[axis])
        mid = len(sorted_tris) // 2
        if mid == 0:
            self.triangles = triangles
            return
        self.left = BVHNode(sorted_tris[:mid], depth + 1)
        self.right = BVHNode(sorted_tris[mid:], depth + 1)

BVH Traversal

def bvh_intersect(ray, node):
    if node is None:
        return None
    if not node.bbox.intersect(ray):
        return None
    if node.triangles:
        closest_t = float('inf')
        hit_tri = None
        for tri in node.triangles:
            t = ray_triangle_intersect(ray, tri)
            if t is not None and t < closest_t:
                closest_t = t
                hit_tri = tri
        return (closest_t, hit_tri) if hit_tri else None
    left_hit = bvh_intersect(ray, node.left)
    right_hit = bvh_intersect(ray, node.right)
    if left_hit and right_hit:
        return left_hit if left_hit[0] < right_hit[0] else right_hit
    return left_hit or right_hit
flowchart TD
  A[Root AABB] --> B{Intersects?}
  B -->|No| C[Skip subtree]
  B -->|Yes| D[Test Left Child]
  B -->|Yes| E[Test Right Child]
  D --> F{Leaf?}
  F -->|Yes| G[Test all triangles]
  F -->|No| D
  E --> H{Leaf?}
  H -->|Yes| I[Test all triangles]
  H -->|No| E

Performance Comparison

# Build and benchmark
bvh = BVHNode(triangles)
start = time.time()
hit = bvh_intersect(test_ray, bvh)
bvh_time = time.time() - start

print(f"BVH: {bvh_time*1000:.2f}ms, Speedup: {naive_time/bvh_time:.0f}x")

Expected output:

Naive: 15.30ms for 10000 triangles
BVH: 0.08ms, Speedup: 191x

Uniform Grid

An alternative to BVH is the uniform grid, which partitions space into equal-sized cells and stores a list of primitives per cell.

class UniformGrid:
    def __init__(self, triangles, grid_size=10):
        self.grid = {}
        self.cell_size = grid_size
        self.bbox = self.compute_scene_bbox(triangles)
        extent = self.bbox.max_pos - self.bbox.min_pos
        self.dims = np.ceil(extent / grid_size).astype(int)
        for tri in triangles:
            cell = self.get_cell(tri.centroid)
            key = tuple(cell)
            if key not in self.grid:
                self.grid[key] = []
            self.grid[key].append(tri)

    def intersect(self, ray):
        for cell_key in self.traverse_cells(ray):
            for tri in self.grid.get(cell_key, []):
                t = ray_triangle_intersect(ray, tri)
                if t is not None:
                    return t
        return None

SAH (Surface Area Heuristic)

The Surface Area Heuristic improves BVH quality by choosing split positions that minimize the expected cost of ray traversal:

Cost = C_trav + p_left * N_left * C_intersect + p_right * N_right * C_intersect

Where p_left is the probability of a ray hitting the left child, approximated by the ratio of the child's surface area to the parent's surface area.

Common Errors & Mistakes

1. BVH Depth Too Deep or Too Shallow

Mistake: Creating a BVH with millions of nodes (too deep) or only a few nodes (too shallow).

Fix: Use a leaf-size threshold (4-16 triangles) and a maximum depth limit (16-32). The optimal balance depends on the scene.

2. Poor Split Axis Selection

Mistake: Always splitting along the same axis, creating elongated, inefficient nodes.

Fix: Split along the axis with the largest extent (SAH-based splitting is even better).

3. AABB Intersection Precision Issues

Mistake: Rays grazing the AABB surface produce false negatives or positives.

Fix: Use robust slab method with proper handling of zero-direction components and add a small epsilon to AABB extents.

4. Duplicate Triangle References

Mistake: Including the same triangle in multiple BVH nodes, increasing traversal cost.

Fix: Assign each triangle to exactly one leaf node. Use splitting planes that partition triangles by centroid, not by bounding box overlap.

Practice Questions

Question 1

What is the time complexity of ray intersection with a BVH vs naive?

Show answer Naive is O(N) where N is the number of primitives. BVH is O(log N) on average, as each level of the tree eliminates half the remaining primitives.

Question 2

Why does the Surface Area Heuristic improve BVH quality?

Show answer SAH estimates the probability that a random ray will intersect a child node based on its surface area. Splitting to minimize SAH cost creates balanced trees that reduce expected traversal time.

Question 3

When would a uniform grid outperform a BVH?

Show answer Uniform grids work well when primitives are evenly distributed (e.g., particles, uniform point clouds). BVHs handle uneven distributions better because they adapt to the geometry density.

Question 4

What is an AABB and why is it used?

Show answer An axis-aligned bounding box (AABB) is the smallest rectangle aligned with the coordinate axes that encloses a set of primitives. It is used because ray-AABB intersection is very fast (a few comparisons).

Challenge

Implement a BVH with SAH splitting and compare its performance against a simple median-split BVH on three different scenes: a uniform distribution, a clustered distribution, and a single large mesh. Report traversal times and node counts.

FAQ

Does GPU Ray Tracing use BVHs?

Yes, NVIDIA RTX cores traverse BVHs in hardware. The BVH is built on the CPU (or GPU using optix) and uploaded to the GPU. The RT core handles ray-AABB and ray-triangle intersection in dedicated silicon.

Can I update a BVH for animated scenes?

Yes, by rebuilding the BVH each frame (fast for small scenes) or using refitting (updating AABBs without restructuring the tree). Refitting is faster but degrades quality over time.

What is a two-level BVH?

A two-level BVH has a top-level BVH (TLAS) referencing instances and a bottom-level BVH (BLAS) per mesh. This allows instancing: one BLAS can be reused by multiple TLAS entries with different transforms.

How much memory does a BVH use?

A BVH typically uses 16-32 bytes per node and about 2x the memory of the scene geometry. For a 10 million triangle scene, expect 500MB-1GB for the BVH structure alone.


Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Author: DodaTech | Last updated: June 21, 2026

DodaTech tutorials are built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro — security tools used by millions worldwide.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro