Search code examples
openglglslfragment-shadervertex-shaderdepth-testing

OpenGL depth test with unclamped range


I'm using a OpenGL pipeline with a vertex and fragment shader, which is such that early fragment tests need to be enabled.

So the depth test is always done with the vertex shader output gl_Position.z which (after perspective divide) need to be between -1 and +1. So the vertex shader needs to normalize the Z values of the vertices from their full possible range to [-1, +1]. But in this case the full range of the vertices is not known beforehand.

Is there a way to make OpenGL accept any values for depth testing, without clamping to [-1, +1]? The depth texture/renderbuffer has GL_FLOAT format, so it should be able to handle any float value.

I've tried glEnable(GL_DEPTH_CLAMP), but this only allows to forward values outside this range to the fragment shader (transformed to window-space), but still does not do the depth test on such values.

There seems to be an extension "GL_NV_depth_buffer_float" with a new depth buffer format DEPTH_COMPONENT32F_NV that would disable this clamping, but I've not yet gotten this to work, and it is NVidia-only.

I've also tried replacing the depth test by a custom implementation in the fragment shader that does imageAtomicMax() on an image (with the float depth values transformed into uint values such that they have the same ordering), but it seems impossible to avoid race conditions. The fragment shader need to write output into multiple framebuffer color attachments, depending on the depth test result.

Another way seems to be to use a function such as atan() in the vertex shader (to map [-inf, +inf] to [-1, +1], but this could slow it down, and the actual Z values might end up so close to each other that floating-point precision can no longer distinguish them.


Solution

  • What you want can be done with the "Reverse-Z" technique.

    The near-plane clipping is necessary due to the singularity at the camera plane. The geometry needs to be clipped so that we don't see what's behind the camera.

    The far-plane clipping can actually be side-stepped by setting it to infinity. Normally that cannot be done with fixed point depth buffers due to precision loss. However, with a floating point depth buffer it is achievable as follows.

    You set the clipping planes to 0 and 1 (instead of -1 and 1) using the command:

    glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE);
    

    Reverse the depth test:

    glDepthFunc(GL_GREATER); // default is GL_LESS
    glClearDepth(0); // default is 1
    

    And tweak your projection matrix so that the far plane at infinity is projected to 0, and the near plane is projected to 1:

    A  0  0  0
    0  B  0  0
    0  0  0  n
    0  0  -1 0
    

    Here A and B determine your field-of-view, and n is the location of the near plane. This technique gives you uniform relative precision all across the entire half-infinite range. You can set the near-plane arbitrarily close at the expense of the far-end of the exponent.

    Sources: