A Couple 3D AABB Tricks

published on Dec 23 2025

Axis-aligned bounding boxes are ubiquitous in 3D programming, typically used as stand-ins for some other shape (for example, for purposes of collision detection or determining visibility). The main characteristic of an AABB is that it is, well, axis-aligned: its sides line up with the coordinate axes. Over the years I've encountered a couple useful tricks for working with them that I want to share here. None of these are new.

AABB Representation

The first trick has to do with AABB representation. In some learning materials, I've seen AABB represented as a point (usually the centroid) plus the dimensions of the box:


struct aabb {
  float centroid[3];
  float width_height_depth[3];
};

Sometimes the width/height/depth are stored divided by 2, but you get the idea.

A better (in my opinion) representation is storing the min and max values for the x, y and z coordinates within the AABB:


struct aabb {
  float x_minmax[2];
  float y_minmax[2];
  float z_minmax[2];
};

Incidentally, this representation is used in Peter Shirley's raytracing series. A slighly different version of this is just storing min and max corners of the box. Those "minmax" regions are also referred to as "slabs", because the AABB can be thought of as the intersection between three "slabs" of 3D space (think of a slab as an infinite region bounded by two parallel 3D planes).

This representation occupies the same amount of memory as the point-plus-dimensions one, but I think it's nicer because many operations call for computing those boundary values anyway. For example, computing the bounding box that encloses any two given bounding boxes is extremely trivial with this representation, whereas the point-plus-dimensions one requires some extra computation:


aabb enclosing_aabb(const aabb& b0, const aabb& b1) {
  return aabb {
    {std::min(b0.x_minmax[0], b1.x_minmax[0]), std::max(b0.x_minmax[1], b1.x_minmax[1])},
    {std::min(b0.y_minmax[0], b1.y_minmax[0]), std::max(b0.y_minmax[1], b1.y_minmax[1])},
    {std::min(b0.z_minmax[0], b1.z_minmax[0]), std::max(b0.z_minmax[1], b1.z_minmax[1])},
  };
}

Finding AABB Vertex Coordinates

Some operations require us to know the coordinates of individual bounding box vertices. For example, to test whether a bounding box intersects an arbitrary plane we need to check whether all of the vertices lie on the same side of the plane or not.

At first glance it might seem like the point-plus-dimensions representation should be ideal for this, but it actually turns out that the slab one results in much simpler code.

Two key observations here are: 1) the AABB has only eight vertices; 2) a coordinate of an AABB vertex cannot have a value that is between the allowed min and max values. For example, for any AABB vertex, its X coordinate is always either X_min or X_max.

Putting these two observations together, we can conclude that the index of an AABB vertex (a number ranging from 0 to 7) fully encodes the coordinate values of that vertex. The rules for this encoding are pretty simple: 1) bit 0 corresponds to coordinate X, bit 1 to Y, bit 2 to Z (the rest of the bits are irrelevant since there are only 8 possible index values); 2) if the bit value is 0, its corresponding coordinate should have the minimum value, otherwise it should have the maximum value.

Using this rule, we can determine the coordinates of any vertex using some bit-twiddling and indexing with no floating point math whatsoever:


// assuming `i` is the vertex index.
// for each axis, isolate the corresponding bit and use it to look up 
// the coordinate in the axis' minmax array.
float vertex_i_xyz[] = {
  aabb.x_minmax[(i & 1) >> 0],
  aabb.y_minmax[(i & 2) >> 1],
  aabb.z_minmax[(i & 4) >> 2]
};

I haven't seen this particular trick explicitly mentioned anywhere, though I am sure that many people have figured it out on their own.

Ray-AABB Intersection Test

There's a very nice way to check whether a given ray intersects an axis-aligned bounding box. This test is pretty old with roots dating all the way back to Kajiya and Kay's 1986 paper. It's used extensively to this day.

Ray Tracing Gems 2 includes an explanation for this algorithm but to be honest I think it's a bit terse, so I'll try explaining it myself.

First, a ray is fully defined by its origin point o and its direction d. Any point along the ray corresponds to a value of a scalar parameter t and can be found by evaluating o + t*d.

We already mentioned that an AABB can be thought of as an intersection of three "slabs". The idea of this ray-AABB intersection test is as follows:

  • For each slab, find the values of t corresponding to points where the ray pierces the slab. The value of t for the entry point is necessarily smaller than that for the exit point. We'll call this pair of values a "t-range".
  • If the t-ranges for all three slabs overlap, the ray intersects the AABB.

An axis-aligned slab is defined by two values - min and max. It's pretty trivial to find the ray-slab intersection points using the ray equation. This example shows finding the t-value of the intersection point with the "min" wall of the X-slab:


// If there is a point where the ray pierces the "min" wall of the slab, the
// following must be true for that point's corresponding t-value:
o_x + d_x * t = x_min 

// therefore:

t = (x_min - o_x) / d_x

The intuition behind this is that, whatever that intersection point is, its x-coordinate has to be x_min because that's just how the X-slab is defined. Similar logic applies to Y and Z slabs, which gives us t-ranges for each slab.

Finding whether all t-ranges overlap is also easy. Each t-range has a start and end value. For all ranges to overlap, the largest start value has to be smaller than the smallest end value.

This condition is necessary: if the largest start value were larger than the smallest end value, then the range with the largest start value would not overlap the range with the smallest end value. This condition is also sufficient: every range starts before the range with the largest start value (by definition), and every range ends within or after the range with the largest start value (due to our condition), therefore they all overlap.

With all of that said, we can write some pseudo-code for the AABB-ray intersection test:


// returns true if the ray `r` intersects the aabb `box` AND the t-value
// for the intersection point is within the [t_min; t_max] range.
bool intersects(const aabb& box, const ray& r, float t_min, float t_max) {
  const float3 min_walls { box.x_minmax[0], box.y_minmax[0], box.z_minmax[0] };
  const float3 max_walls { box.x_minmax[1], box.y_minmax[1], box.z_minmax[1] };
  const float3 reciprocal_ray_dir = 1.0 / r.d; // assumes div by 0 will result in inf.
  const float3 t_minwalls = (min_walls - r.o) * reciprocal_ray_dir;
  const float3 t_maxwalls = (max_walls - r.o) * reciprocal_ray_dir;
  // we have the t-values for intersection points with every slab wall.
  // now determine which of these are the start points of the t-ranges,
  // and which are the end points.
  const float4 t_ranges_starts { min(t_minwalls, t_maxwalls), t_min };
  const float4 t_ranges_ends { max(t_minwalls, t_maxwalls), t_max };
  const float largest_start = max_component(t_ranges_starts);
  const float smallest_end = min_component(t_ranges_ends);
  return largest_start <= smallest_end;
}

By now you probably have noticed that this algorithm contains a potential division by zero. Unless your environment is set to trap on division by 0, this operation should produce an infinity which is a perfectly legal IEEE floating point value (actually, would be a negative infinity in case of division by -0.0, which is also a perfectly valid IEEE float). The ray tracing gems article provides some further pointers on why this works, and explains a case where this code might produce NaNs (due to multiplying 0 with infinity).


Like this post? Follow me on bluesky for more!