Depth Sorting

How to 3D

Chapter 5: Graphics Pipeline

Depth Sorting

Once a model has passed through the transformation and rasterization stages of the pipeline, its fragments arrive at the framebuffer. Their colors are not automatically written to it. Other primitives might project their fragments onto these same pixels. How do we decide which fragment should be written to the framebuffer?

If the primitives are opaque, we want the fragment nearest to the eye to be visible. One way to achieve this would to be sort the primitives based on their depth. Then we could render the primitives from farthest to nearest, letting nearer primitives overwrite farther ones. The problem with this approach is that every time the scene changes or the viewer moves, we would have to sort the primitives again. Sorting is too expensive to perform so frequently.

In 1974, graduate student Ed Catmull proposed a less expensive algorithm. It obviates sorting but does require additional storage. The algorithm adds a depth buffer as a companion to the framebuffer. The framebuffer is a raster of colors, while the depth buffer is a raster of depths.

At the beginning of each frame, the depth buffer is cleared to the maximum depth of 1. The models are then drawn in any order and rasterized into fragments. A fragment's depth is \(z_\mathrm{norm}\), which is computed by your projection matrix and the perspective divide. Before a fragment is written, its depth is compared to the value currently in the depth buffer. If the fragment's depth is smaller, both the color and depth are written to the framebuffer. If the fragment's depth is bigger, it is discarded. After all the primitives are drawn, only the fragments that have the least depths are present in the framebuffer.

In WebGL, you turn on Catmull's depth sorting algorithm with this statement:

gl.enable(gl.DEPTH_TEST);
gl.enable(gl.DEPTH_TEST);

To clear the depth buffer, you add a flag to gl.clear, which you've already been using to clear the previous frame's colors:

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

Compare how these two intersecting triangles are drawn with and without depth sorting:

A fragment's depth is available in the fragment shader as the z-component of the builtin variable gl_FragCoord. It will be 0 at the near face of the viewing volume and 1 at the far face. Consider this renderer that colors each fragment of a box according to its depth:

Rotate the box to see how depth changes across the surface. The color is assigned with this statement:

fragmentColor = vec4(vec3(gl_FragCoord.z), 1.0);
fragmentColor = vec4(vec3(gl_FragCoord.z), 1.0);

Catmull went on to co-found Pixar.

← Perspective ProjectionScissor Test →