Yandersen

08-19-2014, 08:16 PM

Just want to share some thoughts about the famous depth buffer' resolution issues when drawing the large scenes.

It all comes from the uniform division of all coordinate components by w, which is, I think, undesirable in some cases.

As I am rendering a terrain of a size as large as 100x100 km (!), I had to win in the battle of Z-fighting. I had to linearize the Z-buffer in order to do that.

I do understand that the default non-linear Z-buffer allows eye-to-world transformations done by a single inverse matrix multiplication, while linear depth will require some magic to be done separately for perspective-divided xy and linear z values to achieve the same transformation. But IMO this disadvantage is not as big as the Z-fighting issues.

So let's estimate the precision aspect of the problem - what level of tolerance do we need from a Z-buffer? I think, in most scenes the surfaces hardly get closer than 1 mm to each other. So having the tolerance of +-0.5mm for Z-buffer to distinguish is enough. In case the z-values linearly related to the distance, what would be the maximum drawable distance with a depth buffer of 32-bit precision? Surprisingly, it can handle over 2000 km (!), distinguishing the surfaces which are 1mm apart from each other. At any distance! In case of 24-bit depth buffer we get 8 km only, but it is still not bad, isn't it?

Try to do it with conventional Z-buffer, huh? The zNear should be pushed so far back, that the new camera will need to be introduced to cover the gap, and then there will be a mess with two depth buffers, or a fragmented one... Headache.

So how do we make a Z-buffer linear instead of the default perspective-divided?

In some places I've seen the advice to premultiply the vertex shader output's z-component by w, so after the unavoidable division by w we will get the original z back. I came to this naive idea too, and THIS WAY IS WRONG!!! I got pop-through artifacts for the primitives intersecting the clipping plane. The source of that artifact is a clipping, which is performed BEFORE the division by w. Once the primitive gets clipped, the new vertices are inserted, and their x,y,z,w are the product of linear interpolation, which results in incorrect value assigned to z-coordinate - it makes it bigger than it actually should be, letting the primitives just behind the rendering ones to bleed-through.

So the correct way to make the Z-buffer linear is to write the desired depth value right into the gl_FragDepth inside the fragment shader. So we take z from the result of model-view transformation, negate it, divide by zFar and get a depth value which is 0.0 at the camera's position and 1.0 at the most distant point in the scene. The resulted value calculated inside the vertex shader we store in the additional output component to interpolate across the primitive and simply write it directly to gl_FragDepth inside the fragment shader.

And here is the drawback: modifying the gl_FragDepth switches off early Z-cull, which is a very desirable feature, especially if the fragment shader's code is heavy.

And it seem to be to silly that there is no extension exists yet, which turns on/off the division of z-component by w. Or better yet, let the vertex shader to specify the clipping and division vectors explicitly instead of the fixed-functionality-generated {w,w,w,w}|{-w,-w,-w,w} pair for clipping, and {w,w,w,w*w} for division.

What do you people think?

It all comes from the uniform division of all coordinate components by w, which is, I think, undesirable in some cases.

As I am rendering a terrain of a size as large as 100x100 km (!), I had to win in the battle of Z-fighting. I had to linearize the Z-buffer in order to do that.

I do understand that the default non-linear Z-buffer allows eye-to-world transformations done by a single inverse matrix multiplication, while linear depth will require some magic to be done separately for perspective-divided xy and linear z values to achieve the same transformation. But IMO this disadvantage is not as big as the Z-fighting issues.

So let's estimate the precision aspect of the problem - what level of tolerance do we need from a Z-buffer? I think, in most scenes the surfaces hardly get closer than 1 mm to each other. So having the tolerance of +-0.5mm for Z-buffer to distinguish is enough. In case the z-values linearly related to the distance, what would be the maximum drawable distance with a depth buffer of 32-bit precision? Surprisingly, it can handle over 2000 km (!), distinguishing the surfaces which are 1mm apart from each other. At any distance! In case of 24-bit depth buffer we get 8 km only, but it is still not bad, isn't it?

Try to do it with conventional Z-buffer, huh? The zNear should be pushed so far back, that the new camera will need to be introduced to cover the gap, and then there will be a mess with two depth buffers, or a fragmented one... Headache.

So how do we make a Z-buffer linear instead of the default perspective-divided?

In some places I've seen the advice to premultiply the vertex shader output's z-component by w, so after the unavoidable division by w we will get the original z back. I came to this naive idea too, and THIS WAY IS WRONG!!! I got pop-through artifacts for the primitives intersecting the clipping plane. The source of that artifact is a clipping, which is performed BEFORE the division by w. Once the primitive gets clipped, the new vertices are inserted, and their x,y,z,w are the product of linear interpolation, which results in incorrect value assigned to z-coordinate - it makes it bigger than it actually should be, letting the primitives just behind the rendering ones to bleed-through.

So the correct way to make the Z-buffer linear is to write the desired depth value right into the gl_FragDepth inside the fragment shader. So we take z from the result of model-view transformation, negate it, divide by zFar and get a depth value which is 0.0 at the camera's position and 1.0 at the most distant point in the scene. The resulted value calculated inside the vertex shader we store in the additional output component to interpolate across the primitive and simply write it directly to gl_FragDepth inside the fragment shader.

And here is the drawback: modifying the gl_FragDepth switches off early Z-cull, which is a very desirable feature, especially if the fragment shader's code is heavy.

And it seem to be to silly that there is no extension exists yet, which turns on/off the division of z-component by w. Or better yet, let the vertex shader to specify the clipping and division vectors explicitly instead of the fixed-functionality-generated {w,w,w,w}|{-w,-w,-w,w} pair for clipping, and {w,w,w,w*w} for division.

What do you people think?