Some depth buffer precision experiments

Hey, I made this little program for testing the depth precision of different depth buffer formats and techniques.

It’s a small program that checks the depth buffer precision by drawing 256 quads closely stacked together. The program then reads back the resulting color texture and can figure out the strength of the depth bleeding by how “deep” into this stack of quads the depth fighting is occurring.

Controls:

  • Mouse to rotate camera
  • Mouse wheel to zoom in and out (quad size is scaled with zoom, but not spacing, so zooming out requires better depth precision)
  • Keys 1-9 to switch modes.

The program specifically tests the 16-bit uint, 24-bit uint and 32-bit float depth buffer formats, in combination with two techniques: swapping the near and far planes and using a 0.0 to 1.0 mapping instead of the default OpenGL -1.0 to +1.0 mapping using ARB_clip_control.

Visually inspecting the tests gave some very interesting results. Here are the things I found out from this program:

  • A 16-bit uint depth buffer has so low precision that no matter what mapping you use you will be able to utilize all possible values.

  • The normal OpenGL mapping of -1.0 to +1.0 needs to be converted to 0.0 to 1.0, ruining precision. A 24-bit uint depth buffer has more precision than the calculated depth value so the entire range is not utilized, and the depth is not temporally stable.

  • Since the limiting factor of a 24-bit uint depth buffer is the precision of the depth value being written, using a 32-bit float depth buffer produces identical results.

  • The standard DirectX mapping of 0.0 to 1.0 actually has slightly LESS precision than OpenGL’s mapping. This is marginal, but still curious.

  • Reversing the depth on standard OpenGL should not have a significant impact as swapping (-1.0 to +1.0) to (+1.0 to -1.0) shouldn’t affect float precision, but due to the scaling and biasing to (0.0 to 1.0) (or (1.0 to 0.0)) later, reversing the depth actually does slightly improve precision here, although very marginally.

  • The by far best mapping, having 20x higher precision than the runner-up, was reverse depth (1.0 to 0.0) mapping with a 32-bit float depth buffer. As shown here: http://outerra.blogspot.se/2012/11/maximizing-depth-buffer-range-and.html, this gives several times better precision than a 24-bit uint depth buffer thanks to the logarithmic precision distribution of floats being matched to the precision requirements of the depth values.

  • Even using reverse depth (1.0 to 0.0) mapping with a 24-bit uint depth buffer doubles precision as the depth values now have more than enough precision to utilize the entire 24-bit value space of the depth buffer. In addition, the depth bleeding becomes much more regular instead of the flickering, noisy mess.

To get a 0.0 to 1.0 (or the optimal 1.0 to 0.0) mapping of depth buffers, you’ll need either NV_depth_buffer_float (supported by Nvidia and AMD cards) or ARB_clip_control (supported by Nvidia and some newer Intel GPUs). You’ll also need a projection matrix that has that outputs depth values in a 0.0 to 1.0. JOML now supports creating such perspective/ortho matrices. My conclusion is that reverse depth always has equal or better quality, so even if the 1.0 to 0.0 depth range isn’t supported it at least doesn’t harm precision (it slightly improves it) to still use reverse depth, so there’s no point in not using it everywhere, switching to the 1.0 to 0.0 depth range whenever it’s supported for that sweet extra depth precision.

Using reverse depth means:

  • Clearing the depth buffer to 0.0 instead of 1.0.
  • Inverting all depth funcs (GL_LESS —> GL_GREATER)
  • Swapping the near and far planes in the depth buffer.
  • Fixing problems with frustum culling since the near and far planes are swapped now.
  • (If the 1.0 to 0.0 depth range is supported): Make sure all matrices are generated for that depth range

Also, Vulkan defaults to the much more sane DX style mapping of 0.0 to 1.0, meaning that everything that supports Vulkan also supports the reverse 32-bit float mapping for super precision.