Almost every graphics technique is a trade. You spend more memory or compute or complexity and get something faster or more accurate or better looking in return. Reverse-Z is one of the rare exceptions. Flip the near and far planes and depth precision goes from a known problem to a non-issue.
I wanted to implement Reverse-Z in VGLX early on. It's always better to do it sooner rather than later. However, taking advantage of it was challenging because the engine still targets OpenGL 4.1 for macOS compatibility so I shelved the branch for now (more on that later).
Hitting that roadblock made me want to understand the math. Reverse-Z requires looking at how depth values and floating point numbers are distributed across the same range and what happens when those two distributions interact.
There are resources that implement Reverse-Z and show it works empirically 1, but I haven't seen one that reframes two compounding problems as opposing forces. Putting the pieces together was satisfying so I decided to share what I learned.
Depth Precision Problem
Perspective divide is a step in the pipeline that takes clip-space coordinates and divides each one by the homogeneous coordinate
Dividing the
The solution is encoding two constants,
There's one glaring problem with this approach. The depth output is a function of
The easiest way to understand the impact of this nonlinearity is to visualize it. The graph below plots depth values against world-space distance
Looking at this graph it's easy to see how the depth output function
A common mistake is assuming that more precision will automatically mitigate this problem. You might try to switch to a 32-bit floating-point buffer. An extra 8 bits of precision must be a good thing, right? It's actually worse, but understanding why also explains why Reverse-Z works so beautifully.
Floating Point Distribution
32-bit floating-point depth buffers follow the IEEE 754 standard where numbers are represented by three components: a 1-bit sign (
This formula doesn't make the distribution obvious but it's easy to see if we break it down. The sign component is simple: a number can be positive or negative and we have a single bit to represent that. The exponent defines the power-of-two range the number belongs to, and the mantissa is an approximation of where the number sits within that range.
If you found the last sentence confusing an example should clear it up3. Let's take the familiar number
The size of the mantissa is
The key takeaway is that the size of the mantissa is fixed which means that precision is lost as the exponent covers a wider range. At our example range of
Standard depth precision and floating-point numbers have the same key property: most of the precision is clustered near zero. Combining them doesn't fix the problem, it doubles down on it.
The two distributions reinforce each other. The
We cannot change how a float distributes its bits across the
Reverse-Z
Reverse-Z flips the depth mapping so the near plane maps to
Because floating-point numbers have an exponential abundance of precision values right next to zero, the format expands where the projection compresses. The two biases cancel out perfectly.
Implementing this requires adjusting three things: changing the depth test from less to greater, clearing the depth buffer to
In standard perspective projection we find our matrix constants by mapping the near plane to 0 and the far plane to 1. To reverse the system, we swap those targets so that the near plane maps to 1 and the far plane maps to 0.
Seen side-by-side, the standard Vulkan projection matrix and its Reverse-Z counterpart highlight how isolated the change is. The projection matrix changes only in the third row while everything else remains untouched.
The graph below applies Reverse-Z to a simulated float buffer. The values that crowded the near plane before now land at the far plane, exactly where the projection was starving for precision.
The reversed constants have an elegant property. Push the far plane to infinity and the constants simplify rather than break down:
With standard projection an infinite far plane would be a disaster, shoving every value into the region where precision is already scarce. Reverse-Z inverts that. The far plane now lives at
And that's it. Near-perfect precision across the entire range. Well, almost. Even the perfect hack comes with some terms and conditions.
The Fine Print
Any shader that does manual depth comparison, reconstructs world position from depth, or implements depth-based effects must account for the flipped range. This becomes a major headache if you switch conventions mid-project 4.
Another factor is the stencil buffer. A standard 32-bit floating-point format provides increased depth precision but leaves no room for a stencil channel. That is fine if you don't use one but most non-trivial applications do.
The classic 24/8 format cleanly packed depth and stencil into a single 32-bit container. The floating-point equivalent is a format like D32F_S8X24 which yields 32 bits of depth and 8 bits of stencil but balloons to 64 bits per pixel because of the 24-bit padding. That means double the memory footprint and double the bandwidth. On constrained hardware or at high resolutions that cost is real, which explains why 24/8 remained the default for so long.
Finally, there is the clip space restriction. Reverse-Z requires a depth range of
Modern APIs handle this natively and OpenGL 4.5 added glClipControl to remap the depth range to
Reverse-Z is standard in most modern engines, but what makes it interesting isn't that it works. It's how it works.
Most precision problems in graphics are solved by throwing more resources at them. Reverse-Z solves one by rearranging what is already there. It turns two compounding problems into two opposing forces that perfectly balance out.
It is the perfect hack because it does not change the math in the pipeline. It just flips which end of the range maps to which. The precision improvement is enormous. No extra memory. No extra compute. Just a different mapping.
Footnotes
-
The canonical reference for this topic is Nathan Reed's Depth Precision Visualized. ↩
-
There are many resources on how to derive projection matrices. Brendan Galea has a nice video about this topic called The math behind (most) 3D games - perspective projection. ↩
-
This example is borrowed from Fabien Sanglard Floating Point Visually Explained. ↩
-
Godot learned this the hard way Introducing Reverse Z (I'm sorry for breaking your shader). ↩