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:
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:
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.