Get IOLITE PROChangelogDocumentationDevBlogSubscriber Area

Back to all Blog Posts

Reverse Z Cheatsheet

By Benjamin Wrensch

Summary: Cheatsheet for getting Reverse Z integrated into your codebase quickly. Reverse Z can be used to better utilize the available precision of the depth buffer and thus to aid larger rendering distances without running into Z-fighting issues quickly.


Just recently I've started looking into ways to optimize the depth buffer precision for large draw distances and one specific approach caught my eye again. A technique that is now commonly referred to as Reverse Z. While it comes with just a few minor changes, the results can be quite considerable.

That considerable everyone should just go ahead and use it.

Introduction

The general idea itself is actually very simple: instead of mapping the interval between the near and far plane [zn,zf][z_n, z_f] to [0,1][0, 1], a special projection matrix is constructed in a way that it is being mapped to [1,0][1, 0] instead.

Why this increases the depth buffer precision is not directly obvious, but I will also not go into detail here. I've added some references to articles on this topic at the end of this post.

Simply put: this approach works very well in combination with floating-point depth formats (16- or ideally 32-bit) and it utilizes the high precision in the numerical area around zero for distant objects. Using it in conjunction with normalized integer formats will not yield the expected precision improvements.

Sadly the overall knowledge about this approach is still a bit scattered through the Net and I finally thought it's a good idea to put the most useful information in a single place. Thus I am going to focus on the following things:

  • Setting up the projection matrices
  • Adjusting your codebase
  • Efficiently linearizing the native depth buffer values (without a matrix multiplication)

... and here we go!

Projection Matrices

Let's start out with the well known perspective projection matrices for left- and right-handed coordinate systems:

PRH=(sx0000sy0000zfznzfzfznznzf0010)P_{RH} = \left( \begin{array}{cccc} s_x&0&0&0\newline 0&s_y&0&0\newline 0&0&\frac{z_f}{z_n-z_f}&\frac{z_f z_n}{z_n-z_f}\newline 0&0&-1&0\newline \end{array} \right)
PLH=(sx0000sy0000zfzfznzfznzfzn0010)P_{LH} = \left( \begin{array}{cccc} s_x&0&0&0\newline 0&s_y&0&0\newline 0&0&\frac{z_f}{z_f-z_n}&-\frac{z_f z_n}{z_f-z_n}\newline 0&0&1&0\newline \end{array} \right)

We can remap the depth range from [0,1][0, 1] to [1,0][1, 0] by applying a simple transformation matrix

MI=(1000010000110001)M_I = \left( \begin{array}{cccc} 1&0&0&0\newline 0&1&0&0\newline 0&0&-1&1\newline 0&0&0&1\newline \end{array} \right)

to both projection matrices respectively, yielding the final Reverse Z projection matrices:

MIPRH=(sx0000sy0000znzfznzfznzfzn0010)M_I P_{RH} = \left( \begin{array}{cccc} s_x&0&0&0\newline 0&s_y&0&0\newline 0&0&\frac{z_n}{z_f-z_n}&\frac{z_f z_n}{z_f-z_n}\newline 0&0&-1&0\newline \end{array} \right)
MIPLH=(sx0000sy0000znzfznzfznzfzn0010)M_I P_{LH} = \left( \begin{array}{cccc} s_x&0&0&0\newline 0&s_y&0&0\newline 0&0&-\frac{z_n}{z_f-z_n}&\frac{z_f z_n}{z_f-z_n}\newline 0&0&1&0\newline \end{array} \right)

Using the described projection matrices, while setting the near plane distance to zn=15z_n=15 and the far plane distance to zf=1000z_f=1000, we are able to plot the following results:

Visualization of the Z Values After Applying the Different Projection Matrices
Visualization of the Z Values After Applying the Different Projection Matrices

It's also possible to completely remove the need for a fictional far plane and reduce potential rounding and truncation errors during projection and matrix concatenations to almost zero. To achieve this, we can simply assume that the far plane is infinitely far away, yielding:

limzfPRevRH=(sx0000sy00000zn0010)\lim_{z_f\to\infty} P_{RevRH} = \left( \begin{array}{cccc} s_x&0&0&0\newline 0&s_y&0&0\newline 0&0&0&z_n\newline 0&0&-1&0\newline \end{array} \right)
limzfPRevLH=(sx0000sy00000zn0010)\lim_{z_f\to\infty} P_{RevLH} = \left( \begin{array}{cccc} s_x&0&0&0\newline 0&s_y&0&0\newline 0&0&0&z_n\newline 0&0&1&0\newline \end{array} \right)

Setting the near plane distance to zn=15z_n=15, plotting the results reveals the following:

Reverse Z with an Infinite Far Plane
Reverse Z with an Infinite Far Plane

Note how the function is not starting to touch the x-axis when reaching the previous far plane distance.

An infinite far plane might not be the perfect fit for everyone. It has to be correctly handled throughout the whole codebase, requiring a bit of extra care and potentially a few hacks in certain scenarios.

Beyond Projection Matrices

Swapping the projection matrix is the easy part and there is still a little bit of work left to do (depending on the codebase you are working with). You have to...

  • Switch to a floating-point depth buffer (if you are currently using normalized integer formats)
  • Clear depth to zero (instead of one)
  • Adjust your depth tests from defaulting to less (or equal) to greater (or equal)
  • Adjust all calculations which rely on the assumption that a normalized post-projection depth of zero lies in front of the viewer
  • Keep the infinite far plane in mind for all your calculations - if you went down that path. Frustum corners, for example, are often calculated by transforming the screen space coordinates back to view or world space. You will have to swap the depth values for the near and far plane, but you will also have to make sure that you won't end up with infinite values for your far plane corners. Slightly biasing the screen space depth (something like 0+ϵ0 + \epsilon) for the far plane can do the trick here
  • Keep in mind that OpenGL uses an NDC depth range in [1,1][-1, 1], which requires some extra care. Using the OpenGL function glClipControl is one candidate for the job

Inversing Reverse Z

Multiplying with the inverse of the current projection matrix is a common approach to move the normalized post-projection coordinates back to view space.

But: in a lot of scenarios, it is already sufficient to apply this transformation to the zz component only and to skip the complexity of a complete matrix multiplication. This approach is often called linearizing the native depth values. We can deduce the transformation of the zz component by applying the inverse of the projection matrix, dividing by the ww component and simplifying the term of the zz component only:

vss=(0,0,zss,1)v_{ss} = (0, 0, z_{ss}, 1)
zvs=(P1vss)z(P1vss)wz_{vs}=\frac{(P^{-1} v_{ss})_z}{(P^{-1} v_{ss})_w}

Using this approach it is possible to derive the depth linearization functions fitting to each of the projection matrices depicted above:

Standard projection matrix (RH)

zvs=zfznzss(znzf)+zfz_{\text{vs}} = -\frac{z_fz_n}{z_{ss}(z_n-z_f)+z_f}

Reverse Z projection matrix (RH)

zvs=zfznzss(zfzn)+znz_{\text{vs}} = -\frac{z_fz_n}{z_{ss}(z_f-z_n)+z_n}

Reverse Z projection matrix with an infinite far plane (RH)

zvs=znzssz_{\text{vs}} = -\frac{z_n}{z_{ss}}

The left-handed counterparts are simply the negation of the right-handed functions.

As a small side note, when using those functions in shaders: make sure to prepare as much as possible on the CPU side and to pass the prepared constants as uniform or constant buffer values. Taking the Reverse Z linearization function as an example:

c=(zfzn,zfzn,zn,0)c = (z_fz_n,z_f-z_n,z_n,0)
zvs=cxzsscy+czz_{\text{vs}} = -\frac{c_x}{z_{ss}c_y+c_z}

... and that's all there is to it. To sum it up, the following plot shows the linearization functions with the near plane distance set to zn=15z_n=15 and the far plane distance set to zf=1000z_f=1000:

Native Depth Linearization
Native Depth Linearization

Further Reading

Leave a Comment


  Join us on Discord


Discord

Join our brand-new public Discord community and become one of the first 50 members!

By joining our community, you'll have the opportunity to give early feedback, connect with other developers, and be the first to know about new features and enhancements.

Get Started

Download IOLITE
Get IOLITE PRO

Subscribers

Subscriber Area

Social Media

Public Relations

Press Kit
© 2023 Missing Deadlines. All rights reserved.