When and how does openGL calculate F_depth(depth value)

Some sources treat it as part of the viewport transformation, others consider the viewport (x,y) and depth range (z) separately. NDC x and y are transformed to window coordinates according to the glViewport parameters, NDC z is transformed to depth according to the glDepthRange parameters.

Most programs use glViewport to set the viewport to the window size (this is done implicitly the first time a context is bound to a window or other display surface, but an explicit call is needed if a window is resized), but use of glDepthRange is less common; many programs just leave the depth range at the initial state of [0,1].

So, the formula for F_depth is not something exactly used in openGL? And, it’s not something derived by considering operations related to openGL?

So, this formula is not necessarily used for viewport transformation?

Do you mean this one:

?
If you put z=-zeye and rearrange, you’ll get the formula you have.

The F_depth formula is derived from a number of conventions, but isn’t inherent to OpenGL, for reasons including:

  1. The projection matrix can be anything. It doesn’t have to have the form used by e.g. glFrustum. It could be orthographic, it could be a perspective projection with an infinite far plane (e.g. glm::infinitePerspective). In modern OpenGL, there doesn’t even have to be a projection matrix; the vertex shader just emits gl_Position in clip space, how that position is derived is up to the programmer.
  2. weye isn’t forced to be 1. It usually is, but it’s not required (although fixed-function lighting is likely to be rather … odd if you use non-uniform W).
  3. The various stages are performed separately. E.g. clipping is performed in clip space (hence the name).
  4. The depth range isn’t forced to be [0,1], although the F_depth formula assumes that.

That is the formula used for transforming NDC to window coordinates (x,y) and depth (z). Note that farVal and nearVal are the glDepthRange parameters, not the near/far distances used to generate the projection matrix.

Note that the calculation for zNDC->zw is equivalent to this one:

just factored differently.

I just got everything how the F_depth and how [-1,1] to [0,1] mapping occurs. I very much thank you and Alfonse for that.

But, one thing, during window transformation z value seems to map to [0,1], meaning (x’,y’,z’), where x’,y’,z’ are the window transformed coordinates. meaning (x’,y’,z’) don’t span a plane, they span a space, so while drawing, the z’ coords are ignored?

If you don’t have a depth buffer, the z’ coordinates are redundant. The x’,y’ coordinates determine which pixels are covered by the primitive (point, line, triangle).

2 Likes

Then, the thing that would make this equation linear again would be un-dividing by wclip=-zye and then do the glDepthRange(n,f) transformation?

i.e.
z_linear = z_NDC * W_clip = -(f+n)/(f-n)*z_eye + ( 2fn/(f-n) )

and, with depth range transformation:
z_[0,1] = ( z_linear + 1 ) /2
= ( (f+n)*z_eye - 2fn + f - n )/ ( 2(f-n) )

but, in the linked site for depth testing(learnopenGL .com)this is done:

First we transform the depth value to NDC which is not too difficult:

float ndc = depth * 2.0 - 1.0; 

We then take the resulting ndc value and apply the inverse transformation to retrieve its linear depth value:

float linearDepth = (2.0 * near * far) / (far + near - ndc * (far - near));	

I don’t really get how this equation was calculated, could you please give me more insight? and how does calculate from non-linear depth to linear depth ?

That would be linear, but the range is no longer [-1,1]. An orthographic projection has a linear relationship between zeye and zNDC, which is
zNDC = -2*zeye/(f-n) + (f+n)/(f-n)
This gives zNDC=-1 when zeye=-n and zNDC=1 when zeye=n.

But note that if you want a linear depth buffer with a perspective projection, you have to do this by having the fragment shader write to gl_FragDepth, which disables the early depth test optimisation.

Note that this is again assuming glDepthRange(0,1). In general, it’s

float ndc = depth * 2 / (F-N) - (F+N)/(F-N); 

where F, N are the parameters to glDepthRange(0,1)

This is calculating zeye from a depth value calculated using the perspective projection. It’s the inverse of this:

zNDC = (f+n)/(f-n) + 2*f*n/(f-n) / zeye
=> zNDC*(f-n) = (f+n) + 2*f*n / zeye
=> zNDC*(f-n) - (f+n) = 2*f*n / zeye
=> zeye = 2*f*n / (zNDC*(f-n) - (f+n))
Except the learnopenGL version is negated, i.e. you’re getting the distance in front of the viewpoint (the projection matrices generated by glFrustum etc have the positive eye-space Z axis pointing out of the screen).

1 Like

case #1
But, this is rather confusing, why are we calculating the z_coord to eyespace again, when we want a linear depth buffer? Shouldn’t we be transforming the coordinates from eye space to window coordinates and not vice versa?

case #2
or is it that it doesn’t matter what space it is in, we just want values to map to [0,1]? In this case, (let [n,f] for glDepthRange(n,f) = [0,1]) ; then z_NDC = 2*depth - 1, shouldn’t we had replaced this value for z_NDC in the equation:

otherwise how does this equation map to [0,1] if case #2 is the case.

That isn’t what the article is discussing. It’s describing how to get eye-space z from the depth buffer. This is sometimes required for e.g. post-processing or picking.

The equation given in the article doesn’t map depth to [0,1], it maps it to [zNear,zFar], where those are the near/far plane values.

If you actually want a linear depth buffer with a perspective projection, there isn’t any need for this math. You just have the vertex shader output linear z values (i.e. (-zeye-n)/(f-n)) and have the fragment shader write the interpolated value to gl_FragDepth. But that disables early depth tests. With the fixed-function depth calculation, the depth buffer will always be non-linear for a perspective projection.

1 Like

But, note the explanation from learnopenGL, especially in the bold:

We can however, transform the non-linear depth values of the fragment back to its linear sibling. To achieve this we basically need to reverse the process of projection for the depth values alone. This means we have to first re-transform the depth values from the range [0,1] to normalized device coordinates in the range [-1,1] . Then we want to reverse the non-linear equation (equation 2) as done in the projection matrix and apply this inversed equation to the resulting depth value. The result is then a linear depth value.

First we transform the depth value to NDC which is not too difficult:

float ndc = depth * 2.0 - 1.0; 

We then take the resulting ndc value and apply the inverse transformation to retrieve its linear depth value: # this should’d been the z_eye coord

float linearDepth = (2.0 * near * far) / (far + near - ndc * (far - near));	

This equation is derived from the projection matrix for non-linearizing the depth values, returning depth values between near and far.

But, I saw code below, this was being done, inside the main() function:

#version 330 core
out vec4 FragColor;

float near = 0.1; 
float far  = 100.0; 
  
float LinearizeDepth(float depth) 
{
    float z = depth * 2.0 - 1.0; // back to NDC 
    return (2.0 * near * far) / (far + near - z * (far - near));	
}

void main()
{             
    float depth = LinearizeDepth(gl_FragCoord.z) / far; // divide by far for demonstration
    FragColor = vec4(vec3(depth), 1.0);
}

The explanation made me stuck there confused and I hadn’t seen what came below:

float depth = LinearizeDepth(gl_FragCoord.z)/far;

You were right! Thank you very very much for you taking time to write this. One last confirmation, if you’d like to answer, what would happen if we chose to divide gl_FragCoord.z/x for some x > far ? Is far important?

It’s guaranteed that near <= -zeye <= far for any fragment which is rendered. Dividing by far means that the maximum depth value will map to 1.0. Because it’s using z/far rather than (z-near)/(far-near), the value will lie in the range [near/far,1] (= [0.001,1] with near=0.1, far=100) rather than [0,1], but that’s not going to be significant when converted to a 8-bit normalised value for display.

1 Like

Thanks again for the great answer. :slight_smile:

I just noticed the glDepthRange(f,n) function you’ve mentioned above , and I came across the idea, why not just use this function to map z to any range we’d want? Why does the pipeline requires us to clip>perspective_divide>NDC>[0,1] transform for the z coordinate? Wouldn’t it just bring some overhead? we could just do the perspective thing for x,y coordinate and directly to the glDepthRange(n,f) for the z-coordinate, couldn’t we? @GClements?

You can’t skip any of the steps. As I mentioned before, you can implement a linear depth buffer by simply applying an affine mapping to zeye to map it to [0,1] then have the fragment shader write the interpolated value to gl_FragDepth. But that results in depth being a non-affine function of screen x,y, which means you need to perform per-pixel projective division prior to the depth test. Making depth an affine function of zNDC allows the division to be deferred until after the depth test has passed (if it passes). That was probably a fairly significant optimisation on the hardware for which OpenGL 1.0 (and its predecessor IrisGL) was designed, somewhat less so now. Linear depth would also interfere with how glPolygonOffset works.