Computer Graphics · Rasterization

Reversed-Z: Using Floating-Point Depth Without Wasting It

Standard depth mapping spends almost all of a floating-point buffer near the near plane; reversing it to 1 at the near plane and 0 at the far plane redistributes precision where it is needed.

Z-fighting is normally blamed on the depth buffer’s bit depth, and the fix offered is a wider format. That diagnosis is often wrong. The buffer has enough bits; they are merely distributed badly. A projection that maps the near plane to zero and the far plane to one, combined with a floating-point representation whose density is highest near zero, spends the buffer’s precision on the region just in front of the camera and leaves the far field with almost none. Reversing the mapping costs nothing and puts the precision where the geometry is.

Where the precision goes

A floating-point value with $e$ exponent bits and $m$ mantissa bits represents numbers with relative precision $2^{-m}$ regardless of magnitude, so the absolute spacing between consecutive representable values scales with the value’s magnitude. For a 32-bit float with 24 significand bits, spacing near $1.0$ is $2^{-24}$ and spacing near $0.5$ is $2^{-25}$. Near zero the spacing shrinks until denormals take over. This is the opposite of a fixed-point buffer, where the spacing is uniform.

Depth from a standard perspective projection is a hyperbolic function of eye-space distance, as shown in the interpolation article: most of the $[0,1]$ range is consumed within the first fraction of the far distance, and the region beyond is compressed into an interval whose width shrinks roughly as $n/z$ for near plane $n$ and distance $z$. Combining that mapping with floating point is doubly bad, because the compressed far field lands in the interval just below $1.0$, where float spacing is coarse relative to the small differences that remain. A depth difference at distance that is a substantial distance in eye space becomes zero in the stored value.

Fixed-point depth buffers do not have this specific problem: their spacing is uniform, so the hyperbolic compression is the only source of loss, and a 24-bit fixed-point buffer at least degrades predictably. Floating-point depth adds a second, magnitude-dependent loss on top, and for the far field the two losses compound. This is why switching from a 16-bit fixed-point depth buffer to a 32-bit float buffer sometimes does not help, and can produce worse far-field behaviour once the ratio of far to near is large.

The reversal

Reversed-Z changes one thing: the projection maps the near plane to depth $1$ and the far plane to depth $0$. Geometrically nothing changes; the same surfaces are in the same places. Arithmetically everything changes, because the compressed far field now sits just above $0.0$, where float spacing is finest, and the sparse near region sits near $1.0$, where a coarse spacing is irrelevant because the surfaces near the camera are separated by large eye-space distances.

The mapping is usually achieved by a small change to the projection matrix rather than by any change to the rasterizer. The standard OpenGL-style matrix for a perspective projection writes two coefficients into the $z$ and $w$ rows that compute $z_{ndc} = (Az + B)/z_{eye}$, and swapping the sign of the $z$ row flips the direction. The clip-space depth bound is whatever the API expects; for the zero-to-one convention, a straightforward reversal is written as follows.

/* Reversed-Z perspective projection, zero-to-one depth convention.
   fov_y in radians, aspect = width/height, n..f the positive near/far distances.
   Depth maps near -> 1, far -> 0. Compare with GREATER, clear depth to 0. */
void perspective_reversed_z(float m[16], float fov_y, float aspect,
                            float n, float f)
{
    float t = 1.0f / tanf(fov_y * 0.5f);
    memset(m, 0, 16 * sizeof(float));
    m[0]  = t / aspect;          /* x scale */
    m[5]  = t;                   /* y scale */
    m[10] = n / (f - n);         /* z: sign flipped relative to standard */
    m[11] = -1.0f;               /* w = -z_eye */
    m[14] = (f * n) / (f - n);   /* z translation */
}

Three consequences follow for everything downstream. The comparison function must be reversed, so a fragment is nearer when its depth is larger, which means the depth test becomes greater-than rather than less-than. The clear value must be at the far end of the range, which is zero, so the buffer starts with the farthest possible depth. And any code that reconstructs eye-space position from depth must invert the reversed mapping, which is a sign flip in the unprojection and a common source of bugs when a project mixes conventions.

The change to the comparison function is not cosmetic. Every depth-related feature inherits the reversal: shadow map comparisons, the sign convention of depth bias, the ordering assumed by a depth prepass, and the reconstruction of world position in a shader. A pipeline that converts one part and not another renders with inverted occlusion, which is visually obvious but difficult to trace when only one pass is wrong.

Why small near distances dominate

The distribution of precision is governed by the ratio $f/n$ far more than by either value separately. Making the near plane ten times closer multiplies the compression of the far field by roughly the same factor, and the standard mapping loses correspondingly. This is why the common advice to set the near plane as far as the application can tolerate is effective, and why reversed-Z attacks the same problem from the other end: instead of reducing the ratio, it fixes the distribution so that a large ratio costs little.

The mechanism is easiest to see in terms of representable values. With reverse-Z, the number of distinct depths available in a given eye-space interval at distance $z$ stays roughly constant in $\log z$, because the mapping is approximately logarithmic once the far plane is distant. A standard mapping inverts that, giving constant precision in $1/z$ regions but with the float’s magnitude-dependent spacing fighting it. The practical statement is that reverse-Z makes the depth resolution approximately uniform relative to distance, which is what matters for a perspective view, where distant objects subtend small angles and need coarse absolute precision anyway.

There is a well-known consequence for coplanar surfaces. Two surfaces separated by a small gap at distance become distinguishable with reverse-Z at ranges where a standard mapping would merge them. The gain is not a fixed number of bits; it depends on the ratio and on where in the range the surfaces sit. Published analyses of the distribution give the intuition, and the design conclusion is robust even without an exact figure: the reversal never hurts, because it moves precision away from a region that had surplus.

What the format does to the trade

The improvement is largest when the depth buffer is floating point, because the reversal exploits float’s density near zero. With a fixed-point buffer the reversal does nothing, since the spacing is uniform and the sign of the mapping is immaterial to how many distinct values exist per unit of $1/z$. This is the essential pairing: reversed-Z is a technique for floating-point depth, and running it with a fixed-point buffer is neither harmful nor useful.

Using a 32-bit float with reverse-Z is common in engines that need a very large far plane. Using 16-bit float with reverse-Z is also viable and is the more interesting case, because a half-float has few enough bits that the distribution matters more. The half-float’s 10-bit significand is coarse near $1.0$ and only moderately fine near $0.0$, so the reversal’s benefit is real but smaller than for a full float. The usual engineering question, whether 16-bit reversed beats 24-bit fixed forward, depends on the ratio and the tolerance for z-fighting, and it has no universal answer.

Four-byte float depth also brings a bandwidth cost: a full-resolution float depth buffer per sample costs twice the storage of a 16-bit format, and under multisampling that cost is multiplied by the sample count. Hardware compression mitigates it when the data is coherent, but depth buffers are exactly the kind of data that compresses well in flat regions and badly at silhouettes, which are where the extra bits are needed anyway.

Interactions with the rest of the pipeline

Depth bias, used to offset polygon depth for shadow maps and decals, is expressed in units that were defined for the standard mapping. Its scale factor multiplies the depth slope, and its constant term is added in normalized depth units; reversing the sign of depth means both terms must invert, or the bias pushes surfaces the wrong way. Implementations that expose slope-scaled bias apply it inside the rasterizer using the current comparison direction, so the API-level coefficients may need no change, but code that computes an offset by hand does.

The hierarchical depth structure in early depth rejection is direction-agnostic, but its per-tile summary changes meaning: the conservative summary of a reversed buffer is the minimum stored depth rather than the maximum, because nearer means larger. A renderer that builds its own Hi-Z pyramid in software has to flip the reduction accordingly, and a mismatch between the summary and the test produces either no rejection or incorrect rejection, the latter being a correctness bug rather than a performance one.

Depth prepass and equal-depth testing are largely unaffected, since both passes use the same projection. The subtlety is that the reversed buffer’s near values are close to $1.0$ where float spacing is largest, so an equality test between a prepass value and a main-pass value at close range is comparing two numbers with fewer distinct neighbours than the same test at far range. Storing the prepass depth and re-interpolating identically gives exact equality regardless of spacing, so the issue only appears when the stored value is compressed or quantized between the passes.

Clipping against the depth planes has to follow the reversed range as well, which is a place where driver and API conventions matter more than the math. The clip-space bound on $z$ and the sign of the depth test are specified per API, and a projection written for one convention placed in a pipeline configured for another produces geometry that fails the depth range test everywhere, usually visible as an entirely blank frame with no error reported.

The limits

Reverse-Z does nothing for the precision loss that comes from storing depth in a buffer smaller than the range requires. If the far plane is absurdly far, even a good distribution runs out of representable values at the far end, and the answer is a logarithmic depth buffer, which encodes $\log z$ explicitly and spreads precision evenly across the whole range at the cost of a different projection and a shader-side reconstruction. Reverse-Z is the cheap fix that captures most of the benefit; logarithmic depth is the complete fix for ratios that reverse-Z cannot rescue.

It also does nothing for the other common cause of z-fighting, which is genuinely coplanar geometry. Two surfaces at the same depth fight regardless of representation. The fixes there are geometric: offsetting one surface, using a stencil-based decal technique, or avoiding the coplanarity. Depth precision determines how far apart two surfaces must be to be distinguishable, and reverse-Z lowers that threshold; it does not remove it.

References

  1. Depth Precision Visualized — Nathan Reed, NVIDIA developer blog
  2. IEEE 754-2019: Standard for Floating-Point Arithmetic — IEEE
  3. Vulkan Specification — Depth Comparison and Clip Space — Khronos Group