Ray-box Intersection

How to 3D

Chapter 8: Interaction

Ray-box Intersection

The bounding geometry we use in collision detection should fit snugly around a model. If a model is tall or wide, a sphere will produce many false positives in the unoccupied space around the model. Such models are better approximated by a bounding box. Let's examine how to find the intersection between a ray and a box. We consider only boxes that are aligned perfectly with the x-, y-, and z-axes. Such axis-aligned bounding boxes (AABB) are a reasonable place to start because they are simpler to intersect than oriented bounding boxes (OBB).

The intersection algorithm depends on the following information:

The sphere had a nice equation out of which we built a solvable system. The box has no such equation. However, if we tackle the intersection one dimension at a time, we can produce solvable equations. Let's start with the x-dimension.

If we consider only an x-value, then the box's side is an infinite plane, rather than a finite quadrilateral. Any point \(\mathbf{p}\) on the plane containing the left side of the box will satisfy this equation:

$$p_x = a_x$$

We want to find where the ray hits this plane. We combine the ray and plane equations and solve for \(t_{x0}\):

$$\begin{align} o_x + t_{x0} \times d_x &= a_x \\ t_{x0} &= \frac{a_x - o_x}{d_x} \end{align}$$

Next we intersect with the plane containing the right side and solve for \(t_{x1}\):

$$\begin{align} t_{x1} &= \frac{b_x - o_x}{d_x} \end{align}$$

The values \(t_{x0}\) and \(t_{x1}\) are the distances that we must travel along the ray's direction before we hit the two planes. If the ray starts on the right side of the box, then \(t_{x1}\) will be less than \(t_{x0}\). If that's the case, we swap the values so that \(t_{x0}\) is always the smaller of the two.

Develop an intuition for casting a ray into a box with this renderer:

Move the ray by dragging the sphere at its start. Aim the ray by dragging the cylinder. The view is locked to two dimensions because three is confusing. When you understand the ray mechanics, enable the x-intersections and manipulate the ray. Observe how the intersection points are locked to the left and right planes.

After considering only the x-dimension, we have two possible \(t_*\) values at which the ray might intersect the box. To determine if either actually is an intersection, we must consider more dimensions. Let's consider the y-dimension. The ray intersects with the bottom and top planes at these values of \(t_{y0}\) and \(t_{y1}\):

$$\begin{align} t_{y0} &= \frac{a_y - o_y}{d_y} \\ t_{y1} &= \frac{b_y - o_y}{d_y} \end{align}$$

Again we swap the values if necessary so they are sorted from least to greatest. Enable the y-intersections in the renderer. Move and aim the ray so that you can see four possible intersection points for these four possible values of \(t_*\).

This same principle applies when we consider the z-dimension, which is not shown in the renderer above. From the three \(t_{*0}\) values, we select out the maximum. From the three \(t_{*1}\) values, we select out the minimum.

Now manipulate the ray so it doesn't intersect the box.

If a ray passes through both planes of a dimension before entering the first plane of another dimension, no collision is possible.

This logic, extended to include the z-dimension, has been assembled in this fully 3D renderer:

Click to cast a ray through the mouse and rotate to see how it intersects the box. This algorithm isn't as robust as it should be. In particular, it fails when \(\mathbf{d}\) contains a 0. This is the function that captures the algorithm described above:

function intersectRayBox(rayStart: Vector3, rayDirection: Vector3, boxMin: Vector3, boxMax: Vector3) {
  // Intersect the ray with the left and right planes.
  let t0 = (boxMin.x - rayStart.x) / rayDirection.x;
  let t1 = (boxMax.x - rayStart.x) / rayDirection.x;

  // Swap to keep t0 the smaller of the two.
  if (t0 > t1) {
    const tmp = t0;
    t0 = t1;
    t1 = tmp;
  }

  // Intersect the ray with the bottom and top planes.
  let ty0 = (boxMin.y - rayStart.y) / rayDirection.y;
  let ty1 = (boxMax.y - rayStart.y) / rayDirection.y;

  if (ty0 > ty1) {
    const tmp = ty0;
    ty0 = ty1;
    ty1 = tmp;
  }

  // If we've exited one dimension before starting another, bail.
  if (t0 > ty1 || ty0 > t1) return [];

  // Keep greater t*0 and smaller t*1.
  if (ty0 > t0) t0 = ty0;
  if (ty1 < t1) t1 = ty1;

  // Intersect the ray with the near and far planes.
  let tz0 = (boxMin.z - rayStart.z) / rayDirection.z;
  let tz1 = (boxMax.z - rayStart.z) / rayDirection.z;

  if (tz0 > tz1) {
    const tmp = tz0;
    tz0 = tz1;
    tz1 = tmp;
  }

  // If we've exited one dimension before starting another, bail.
  if (t0 > tz1 || tz0 > t1) return [];

  // Keep greater t*0 and smaller t*1.
  if (tz0 > t0) t0 = tz0;
  if (tz1 < t1) t1 = tz1;

  // Locate two points on ray.
  return [
    rayStart.add(rayDirection.multiplyScalar(t0)),
    rayStart.add(rayDirection.multiplyScalar(t1)),
  ];
}
function intersectRayBox(rayStart: Vector3, rayDirection: Vector3, boxMin: Vector3, boxMax: Vector3) {
  // Intersect the ray with the left and right planes.
  let t0 = (boxMin.x - rayStart.x) / rayDirection.x;
  let t1 = (boxMax.x - rayStart.x) / rayDirection.x;

  // Swap to keep t0 the smaller of the two.
  if (t0 > t1) {
    const tmp = t0;
    t0 = t1;
    t1 = tmp;
  }

  // Intersect the ray with the bottom and top planes.
  let ty0 = (boxMin.y - rayStart.y) / rayDirection.y;
  let ty1 = (boxMax.y - rayStart.y) / rayDirection.y;

  if (ty0 > ty1) {
    const tmp = ty0;
    ty0 = ty1;
    ty1 = tmp;
  }

  // If we've exited one dimension before starting another, bail.
  if (t0 > ty1 || ty0 > t1) return [];

  // Keep greater t*0 and smaller t*1.
  if (ty0 > t0) t0 = ty0;
  if (ty1 < t1) t1 = ty1;

  // Intersect the ray with the near and far planes.
  let tz0 = (boxMin.z - rayStart.z) / rayDirection.z;
  let tz1 = (boxMax.z - rayStart.z) / rayDirection.z;

  if (tz0 > tz1) {
    const tmp = tz0;
    tz0 = tz1;
    tz1 = tmp;
  }

  // If we've exited one dimension before starting another, bail.
  if (t0 > tz1 || tz0 > t1) return [];

  // Keep greater t*0 and smaller t*1.
  if (tz0 > t0) t0 = tz0;
  if (tz1 < t1) t1 = tz1;

  // Locate two points on ray.
  return [
    rayStart.add(rayDirection.multiplyScalar(t0)),
    rayStart.add(rayDirection.multiplyScalar(t1)),
  ];
}

If neither a box nor a sphere snugly fit around a model, then we can assemble multiple smaller boxes and spheres together. Or we can employ more complex bounding shapes.

← Ray-Sphere IntersectionPicking by Color →