The Octree
Octree Node:
1# Octree Node structure2class OctreeNode:3 def __init__(self, bounds, capacity=8):4 self.bounds = bounds # AABB boundary (min_pt, max_pt)5 self.capacity = capacity # Maximum points before splitting6 self.points = [] # Points in this node7 self.children = None # Array of 8 child nodes (if split)8 self.is_leaf = True
An octree is a tree data structure in which each internal node has exactly eight children. It is most commonly used to partition a three-dimensional space by recursively subdividing it into eight octants.
Sub-Octant Partitioning:
1# Divide bounding box into 8 sub-octants2def get_octant_bounds(min_pt, max_pt, index):3 mid = [(a + b) / 2 for a, b in zip(min_pt, max_pt)]45 # Binary representation of index determines sub-box bounds6 x_min = mid[0] if (index & 1) else min_pt[0]7 x_max = max_pt[0] if (index & 1) else mid[0]89 y_min = mid[1] if (index & 2) else min_pt[1]10 y_max = max_pt[1] if (index & 2) else mid[1]1112 z_min = mid[2] if (index & 4) else min_pt[2]13 z_max = max_pt[2] if (index & 4) else mid[2]1415 return (x_min, y_min, z_min), (x_max, y_max, z_max)
A parent node partitions its bounding box into eight child nodes by bisecting the volume along three orthogonal planes.
Subdivision Rules:
is_leaf flag to false.1# Point insertion and node subdivision2def insert_point(node, point):3 if not node.is_inside(point):4 return False56 if node.is_leaf:7 if len(node.points) < node.capacity:8 node.points.append(point)9 return True10 node.subdivide() # Split into 8 children1112 # Re-insert existing and new points to child octants13 for p in node.points + [point]:14 for child in node.children:15 if child.insert_point(p):16 break17 node.points = []18 node.is_leaf = False19 return True
Points are added to leaf nodes. When a node exceeds capacity limits, it subdivides and moves its points to child sub-octants.
Nearest Point Search:
1# Distance-based octant traversal2def query_closest_point(node, target_pt, best_pt=None, best_dist=float('inf')):3 if node is None:4 return best_pt, best_dist56 # Check distance to node bounding box AABB7 if distance_to_box(target_pt, node.bounds) >= best_dist:8 return best_pt, best_dist # Prune910 if node.is_leaf:11 for p in node.points:12 d = sq_distance(p, target_pt)13 if d < best_dist:14 best_dist = d15 best_pt = p16 return best_pt, best_dist1718 # Sort children based on proximity to target19 sorted_children = sorted(node.children, key=lambda c: distance_to_box(target_pt, c.bounds))20 for child in sorted_children:21 best_pt, best_dist = query_closest_point(child, target_pt, best_pt, best_dist)2223 return best_pt, best_dist
To perform spatial point queries, we traverse children sorted by distance to the target, pruning nodes whose bounding volumes are further than the current best distance.
Frustum Culling:
1# Select octree nodes intersecting frustum planes2def frustum_cull_octree(node, frustum, visible_nodes):3 # Check node AABB intersection with 6 frustum planes4 intersection = check_aabb_frustum(node.bounds, frustum)56 if intersection == OUTSIDE:7 return89 if intersection == INSIDE:10 # Fully inside, add all descendents without further testing11 visible_nodes.extend(node.get_all_leaves())12 return1314 # Overlaps boundary, recurse children15 if node.is_leaf:16 visible_nodes.append(node)17 else:18 for child in node.children:19 frustum_cull_octree(child, frustum, visible_nodes)
To optimize rendering pipelines, spatial nodes that fall outside the camera's view frustum are quickly discarded, focusing resources only on visible zones.
Ray Traversal:
1# Slab-method Ray-AABB intersection traversal2def ray_octree_intersect(node, ray_origin, ray_dir):3 t_min, t_max = intersect_slabs(node.bounds, ray_origin, ray_dir)45 if t_max < 0 or t_min > t_max:6 return [] # No intersection78 if node.is_leaf:9 return [node] # Intersects leaf node1011 # Traverse child nodes that overlap the ray path interval [t_min, t_max]12 intersections = []13 for child in node.children:14 intersections.extend(ray_octree_intersect(child, ray_origin, ray_dir))1516 return intersections
Ray tracing applications walk along the ray direction, testing parametric line intersections against node AABB boundary slabs to locate intersecting leaf cells.
Voxel Extraction:
1# Convert octree leaf nodes to voxel grid2def get_octree_voxels(node, resolution):3 voxels = []45 def traverse(n):6 if n.is_leaf:7 if n.points: # Only export occupied cells8 center = n.bounds.center()9 voxels.append((center, n.bounds.size()))10 return11 for child in n.children:12 traverse(child)1314 traverse(node)15 return voxels
An octree can act as a multi-resolution voxelizer. By rendering the bounding volumes of occupied leaf nodes, we construct a discrete volumetric representation of continuous shape models.
Clash Detection:
1# Intersect two Octrees recursively to detect overlaps2def intersect_octrees(nodeA, nodeB, overlaps):3 if not boxes_overlap(nodeA.bounds, nodeB.bounds):4 return56 if nodeA.is_leaf and nodeB.is_leaf:7 if len(nodeA.points) > 0 and len(nodeB.points) > 0:8 overlaps.append((nodeA.bounds, nodeB.bounds))9 return1011 # Traverse children of overlapping nodes12 for i in range(8):13 childA = nodeA.children[i] if not nodeA.is_leaf else nodeA14 childB = nodeB.children[i] if not nodeB.is_leaf else nodeB15 intersect_octrees(childA, childB, overlaps)
Clash detection processes intersect two octrees to locate overlapping regions. By testing bounding boxes hierarchically, we discard non-overlapping branches and isolate intersections efficiently.
LooseBounds(center, half_size, k):
1def get_loose_cell_bounds(center, half_size, k=2.0) -> tuple:2 # Expand half_size by looseness factor k (usually 2.0 or 1.5)3 loose_half = half_size * k4 min_pt = [c - loose_half for c in center]5 max_pt = [c + loose_half for c in center]6 return min_pt, max_pt
A Loose Octree expands the boundary of each octant cell by a looseness factor k (typically k=2.0). This prevents moving objects that cross boundaries from thrashing between adjacent cells, keeping them inside a single node's enlarged volume.