Frustum calculation and culling, HOPEFULLY demystified.

The Short(ish) Version

This is valid for view space, where the camera is at the origin and pointed along the Z axis.

Given:

Input values
Symbol Meaning Typical value
\(fov_y\) Vertical field of view 60 – 120 degrees
\(aspect\) Aspect ratio around 1.8 – use frame buffer Width / Height
\(z_{near}\) distance from camera to near clip plane in Z +1
\(z_{far}\) distance from camera to far clip plane. MUST be greater than zNear. 1000

We can calculate some useful intermediate values:

Derived values
Symbol Meaning Formula
\(hh\) half of the height of viewport at Z=1 \(tan(\frac{fov_y}{2})*Z_{near}\)
\(hw\) half of the width of viewport at Z=1 \(hh * aspect\)
Viewport corners at Z = 1
\(\vec{nw}\) north-west (top left) corner \(
\begin{bmatrix}
-hw & hh & 1
\end{bmatrix}
\)
\(\vec{ne}\) north-east (top right) corner \(
\begin{bmatrix}
hw & hh & 1
\end{bmatrix}
\)
\(\vec{se}\) south-east (bottom right) corner \(
\begin{bmatrix}
hw & -hh & 1
\end{bmatrix}
\)
\(\vec{sw}\) south-west (bottom left) corner \(
\begin{bmatrix}
-hw & -hh & 1
\end{bmatrix}
\)
Frustum plane normals
Note: these values MUST BE NORMALIZED for point/plane distance calculation to work correctly
The “X” operator is a cross product, NOT multiplication!
Symbol Meaning Formula
\(\vec{top}\) normal of top plane \(\vec{nw}\times\vec{ne}\)
\(\vec{right}\) normal of right plane \(\vec{ne}\times\vec{se}\)
\(\vec{bottom}\) normal of bottom plane \(\vec{se}\times\vec{sw}\)
\(\vec{left}\) normal of left plane \(\vec{sw}\times\vec{nw}\)
\(\vec{near}\) normal of near clip plane \(\begin{bmatrix}0 & 0 & 1\end{bmatrix}\)
\(\vec{far}\) normal of far plane \(\begin{bmatrix}0 & 0 & -1\end{bmatrix}\)
Distance of a point to a plane
Symbol Meaning
p point to test (center of object)
\(\vec{n}\) plane normal
\(d\) distance from plane to origin along its normal
distance \(\vec{p} \cdot \vec{n} – d\)

Usage

There are a number of instances where frustum intersection / containment tests are useful.

Performance

Draw calls (glDraw*(…)) are computationally expensive.

We would like to avoid drawing things that will not be displayed on screen. While the rasterizer will clip (cull) fragments not in the viewport, we want to avoid calling vertex shaders, geometry shaders and tessellation (aka, hull)  shaders for objects that will not be on screen. Draw

Viewport wrapping

For a game like Asteroids, we need to determine when objects (ship, asteroids, UFOs) leave the frustum so that we can wrap them to the opposite side.

And lots of other things.

So, how do we calculate a frustum?

In View space, remember that all items have been transformed  (scaled, rotated and translated)  so that they are relative to the camera. In view space, the camera is located at the origin (0,0,0), and faces along  the Z axis.

When we build a projection matrix, we need:

  • Field of view
  • ZNear
  • ZFar
  • Aspect ratio of the viewport.

The frustum is defined by six planes – the top, bottom and sides of a 4-sided pyramid. The camera is located at the peak of the pyramid.

(diagram coming at some point)

One way to define a plane is with a plane “normal” (a vector that is perpendicular to the plane) and the shortest distance from the plane to the origin. (This is called point-normal form)

\(
A\times x + B\times y + C\times z -D = 0
\)

 

We know that, for the frustum, each of the four sides pass through the origin, so the distance to the origin is zero.

Now, we need two other points on the plane to calculate the normal via a cross product (also called the “outer product”). We can use the origin (since the frustum sides all pass through the camera (eye) position, which is at the origin in view space), and two corners of the viewport.

We know that our viewport (window) is a rectangle. If, for the top plane, we can find the upper-left (north-west) and upper-right (north-east) corners in view space, we can create two vectors – one from the origin to nw, and one from the origin to ne), and take the cross product to get the top plane normal. The process is identical for the right, bottom and left frustum sides.

The near and far planes are a little different. Their planes point along the Z axis in positive and negative directions, respectively, but they do not intersect the origin. Fortunately, finding the distance of the object to these planes is easy:

\(
D_{near} = Z_{near} – P_Z\\
D_{far} = P_Z – Z_{far}
\)

 

So if, for example \(P_Z = 10\) and \(Z_{near} = 1\),

\(
D_{near} = 1 – 10 = -9
\)

 

We see that the distance from \(\vec{P}\) is negative, so the point is in front of the plane.

Summary

  1. Calculate half-height and half-width of viewport at Z=1
  2. Calculate corners of the viewport in View space
  3. Use the cross product of two vectors – from the origin to each corner to find the normal for the plane
  4. Dot the position of your object with each plane normal. If all values are negative, the point is in front of  all sides
  5. Check the point’s Z value  against \(Z_{near} and Z_{far}\) to ensure  it is between the near and far clip planes.