4
\$\begingroup\$

I'm really new to graphics programming, so I'm learning about depth buffers with this article.

I got that the conversion from View Space to NDC is non-linear, but I think we can avoid the problem by doing the following. I suppose I could make it linear by doing the following, but why don't people do this?

Main code:

float near = 0.1f, far = 100.0f;
projection = glm::perspective(glm::radians(45.0f), WIDTH/HEIGHT, near, far);
projection[2][2] = 2.0f/(near-far);          // like ortho only about z
projection[3][2] = (near+far)/(near-far);    // like ortho only about z

Vertex shader:

vec4 outvec = projection * view * model * vex4(VertexCoord, 1.0);
outvec.z *= outvec.w;   // I prevent dividing by w
gl_Position = outvec;
\$\endgroup\$
2
  • \$\begingroup\$ What results do you get when you run with this code? Are they adequate for your needs? \$\endgroup\$
    – DMGregory
    Commented Nov 18, 2021 at 18:31
  • \$\begingroup\$ Why do you think being non linear is a bad thing? Usually you want to spend your precision points closer to the player \$\endgroup\$ Commented Jan 25 at 12:57

2 Answers 2

4
\$\begingroup\$

The main reason the conversion from View Space to Normalised Device Coordinates is non-linear is floating-point precision, amongst others. Using a multiplicative inverse (thus, non-linear) relation over a linear one is due to a trade-off between visual quality and numerical stability at different distances.

The linear transformation

From a merely numerical standpoint, having either a linear or non-linear conversion makes no difference. Converting values among different reference frames is trivial if the conversion formulae are known. In the end, Z-values of any (visible) object are re-mapped to the \$[0, 1]\$ interval nonetheless. However, we must account for floating-point precision because computer numbers are discrete (more on that later).

The following linear function transforms the z-value to a depth value between 0.0 and 1.0:

$$F_{depth} = \frac{z - near}{far - near}$$

Here, \$near\$ and \$far\$ are the distances of the near and the far plane. The result is a linear mapping: you are 'scaling' and applying an offset to some values so that they fit a different scale range.

One property of values transformed with a linear function is, they keep their relative proportions. For example, if point \$A\$ is distant twice than \$B\$ from the camera (i.e. \$A_z = 2 B_z ⇒ \frac{A_z}{B_z} = 2\$), then the ratio is still the same when comparing their depth values.

This property holds because we are using the same precision for all values in the \$[0, 1]\$ interval:

Relation between the z-value and its corresponding depth value

Discriminating close and distant objects

Using a linear transformation means using the same precision for near and distant values.

However, because of perspective projection, closer objects appear larger and farther ones smaller. Also, due to the parallax effect, closer objects appear to move faster when the camera moves around. Such an object's position on the screen will noticeably change even for small camera movements. On the other hand, when rendered, objects at a distance will barely move.

Sometimes, we can't represent such small changes due to floating-point precision. Some values are so close to each other a machine can't evaluate them, and an approximation is required. In 3D rendering, this happens because of depth buffer precision and causes Z-fighting.

I think we can avoid the problem by doing the following.

Its very nature makes Z-fighting unavoidable. You can only mitigate its effects by using a different solution. And, these glitches mustn't show up near the camera.

Since closer objects are more susceptible to camera movement during rendering, we prioritise precision in the depth buffer for them. Then, we need to define a new mapping function that allocates more values for closer objects while using the remaining values for farther ones. Such a solution is a non-linear transformation.

The multiplicative inverse transformation

Because of the above projection properties, we use a non-linear depth equation proportional to \$1/z\$. We cannot compare ratios via depth buffer values anymore: they were linear only before applying the projection matrix.

The following is the new formula:

$$F_{depth} = \frac{\frac{1}{z} - \frac{1}{near}}{\frac{1}{far} - \frac{1}{near}}$$

It distributes value density in the \$[0, 1]\$ range differently from a linear equation:

Non-linear relation between the z-value and the resulting depth buffer's value

I suppose I could make it linear by doing the following, but why don't people do this?

The above function gives us enormous precision for very small z-values in comparison to a linear function: values between 1.0 and 2.0 would result in depth values between 1.0 and 0.5, which is half of the \$[0,1]\$ range. Greater z-values will be stored using less precision over and over since they represent distant objects. This is acceptable: we can hide Z-fighting in the distance via fog effects, design choices (e.g. indoor environments), or even (almost) avoided by limiting the far plane distance and never overlapping surfaces and objects.

Alternate rendering techniques are also possible, such as rendering very far objects separately from the playable scene and displaying them in the background (this solution is implemented as pre-rendered cube maps in some space simulators such as SpaceEngine).


Reference: 'Depth testing' at Learn OpenGL, by Joey de Vries.

\$\endgroup\$
0
\$\begingroup\$

Because your formula breaks the depth interpolation.

Vertex shaders compute out the (x, y, z) values and w values. Then in an unprogrammable step, (x, y, z) is divided by w, and interpolated. In fragment shaders, you will only get interpolated z values. This interpolated z value is crucial for the depth test to get a correct render image.

Let's say we have two line segments, one is (-1, 0, -1)-(1, 0, -3) with red color, and the other one is (1, 0, -2)-(-1, 0, -2) with green color, and your camera look towards -z.

These two segments should intersect at (0, 0, -2). That is to say, you see red on the left(where x<0), and green on the right(where x>0).

How does your GPU know they intersect at x=0? It first has to do the perspective projection to the 4 vertices, and then interpolate the 4 depth values in window space. The x, y, z mapping from camera space to window space is carefully designed to make sure that the interpolated z value of the two line segments equals when x=0.

That's why z-mapping cannot be made linear.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .