Jump to content

  • Log In with Google      Sign In   
  • Create Account






Tip of the day: logarithmic zbuffer artifacts fix

Posted by Ysaneya, 20 August 2009 · 5,446 views

Logarithmic zbuffer artifacts fix

In cameni's Journal of Lethargic Programmers, I've been very interested by his idea about using a logarithmic zbuffer.

Unfortunately, his idea comes with a couple of very annoying artifacts, due to the linear interpolation of the logarithm (non-linear) based formula. It particularly shows on thin or huge triangles where one or more vertices fall off the edges of the screen. As cameni explains himself in his journal, basically for negative Z values, the triangles tend to pop in/out randomly.

It was suggested to keep a high tesselation of the scene to avoid the problem, or to use geometry shaders to automatically tesselate the geometry.

I'm proposing a solution that is much more simple and that works on pixel shaders 2.0+: simply generate the correct Z value at the pixel shader level.

In the vertex shader, just use an interpolator to pass the vertex position in clip space (GLSL) (here I'm using tex coord interpolator #6):


void main()
{
vec4 vertexPosClip = gl_ModelViewProjectionMatrix * gl_Vertex;
gl_Position = vertexPosClip;
gl_TexCoord[6] = vertexPosClip;
}

Then you override the depth value in the pixel shader:


void main()
{
gl_FragColor = ...
const float C = 1.0;
const float far = 1000000000.0;
const float offset = 1.0;
gl_FragDepth = (log(C * gl_TexCoord[6].z + offset) / log(C * far + offset));
}

Note that as cameni indicated before, the 1/log(C*far+1.0) can be optimized as a constant. You're only really paying the price for a mad and a log.

Quality-wise, I've found that solution to work perfectly: no artifacts at all. In fact, I went so far as testing a city with centimeter to meter details seen from thousands of kilometers away using a very very small field-of-view to simulate zooming. I'm amazed by the quality I got. It's almost magical. ZBuffer precision problems will become a thing of the past, even when using large scales such as needed for a planetary engine.

There's a performance hit due to the fact that fast-Z is disabled, but to be honnest in my tests I haven't seen a difference in the framerate. Plus, tesselating the scene more or using geometry shaders would very likely cost even more performance than that.

I've also found that to control the znear clipping and reduce/remove it, you simply have to adjust the "offset" constant in the code above. Cameni used a value of 1.0, but with a value of 2.0 in my setup scene, it moved the znear clipping to a few centimeters.

Results

Settings of the test:
- znear = 1.0 inch
- zfar = 39370.0 * 100000.0 inches = 100K kilometers
- camera is at 205 kilometers from the scene and uses a field-of-view of 0.01°
- zbuffer = 24 bits

Normal zbuffer:

http://www.infinity-universe.com/Infinity/Media/Misc/zbufflogoff.jpg


Logarithmic zbuffer:
http://www.infinity-universe.com/Infinity/Media/Misc/zbufflogon.jpg

Future works

Could that trick be used to increase precision of shadow maps ?




--
--
Cool, I see I was unnecessarily afraid of the performance hit from disabled fast-z [rolleyes]

Btw. the log-z equation actually utilizes only half the z-buffer range because z/w will lie in [0,1] range and not [-1,1] as OpenGL would like. But like you've said - it's almost magical anyway [smile]
We are also thinking about using it for logarithmic shadow maps, see Logarithmic Perspective Shadow Maps

Thanks!
Quote:
Original post by cameni
Cool, I see I was unnecessarily afraid of the performance hit from disabled fast-z [rolleyes]


Yeah. But of course it depends on the application (if the bottleneck is somewhere else, for example bandwidth, it won't show up) and the hardware, so I hesitate to say that it's no problem in general.

I need to do some more serious testing in a "real" scene. Unfortunately my engine isn't in a stage to do this test right now (need to re-enable a lot of the pipeline), but I'm confident the benefits will outweight the costs (if any).

Quote:
Original post by cameni
Btw. the log-z equation actually utilizes only half the z-buffer range because z/w will lie in [0,1] range and not [-1,1] as OpenGL would like. But like you've said - it's almost magical anyway [smile]


True, I forgot that. Couldn't it be solved by doing z = z * 2 - 1 at the end ? The w shouldn't come into play as we're already in the pixel shader.

Quote:
Original post by cameni
We are also thinking about using it for logarithmic shadow maps, see Logarithmic Perspective Shadow Maps


Cool, I'll have a look, thanks.
Quote:
Original post by Ysaneya
Quote:
Original post by cameni
Btw. the log-z equation actually utilizes only half the z-buffer range because z/w will lie in [0,1] range and not [-1,1] as OpenGL would like.
True, I forgot that. Couldn't it be solved by doing z = z * 2 - 1 at the end ? The w shouldn't come into play as we're already in the pixel shader.
Actually you've got it right - the [-1,1] range has to be output from vertex shaders, but gl_FragDepth has range [0,1].
Yeah, I realized that too (and I tested to make sure, the [0-1] range in the pixel shader is indeed correct :)).
By writting to the Z buffer in the pixel shader you lose Hierarchical Z which means more bandwidth usage.

This can get a notorious on bigger screen resolutions and the more pixels are covered by non-overlapping triangles.

You also lose Early Z, which is a shame if you do a lot of overdraw and you were decreasing fillrate by rendering front to back. You lose that advantage.
Early Z is not automatic, so you won't see a difference if you weren't already doing it. Early-Z is great when you're GPU-bound, but makes you more CPU-bound.

So, if you don't have big screen resolutions, bandwidth-limited, don't consume lot of fillrate, and/or do a lot of overdraw, writting to the Z buffer won't hit performance.

Cheers
Dark Sylinc

More info here
Yep, I mentionned that in the journal. It's not necessarily a problem though, as it's quite application dependent, for example if your bottleneck isn't in bandwidth. In my tests I haven't been able to notice a drop in framerate using the trick, and that was on a scene with decent overdraw, half a million polys and a pretty intensive pixel shader. But that was on a Radeon HD 4890, so I haven't tested that behavior on other cards/scenes.
For applications with significant overdraw losing the hierarchical z can be very painful. (I'd have to say also typically I'd prefer to call log per vertex rather than per pixel)

Perhaps something like this could be done in the vertex shader?


signZ = sign(z);
z = signZ * (log(C * signZ * z + offset) / log(C * far + offset));


Essentially for negative z values inverting the graph given by positive Z values.
Cheers,
Martin
Quote:
Original post by Martin
Essentially for negative z values inverting the graph given by positive Z values.
Cheers, Martin


Nope, I already tried that, to symmetrize the function ( with success ) or to use alternative functions, but the problem still remains, you get various popping of triangles as soon as some vertices as behind the camera.
Quote:
Original post by Martin
Perhaps something like this could be done in the vertex shader?
signZ = sign(z);

z = signZ * (log(C * signZ * z + offset) / log(C * far + offset));
Essentially for negative z values inverting the graph given by positive Z values

The problem is not so much in the values of Z being negative - lower C linearizes the function well enough so that this would not be problem. Problem is that rasterizer interpolates Z/W and 1/W linearly and computes per-pixel z by dividing the two values. It does this to obtain perspective correct values. The value written to Z-buffer could be a different thing from this all, but actually it's taken from this.
Anyway, the problem is that we are pre-multiplying Z with values of W at the vertices, to get rid of inherent 1/W. Basically then, even when the logarithmic function is linear enough in +-200m range, we are also linearly interpolating between values of W. But since the rasterizer interpolates 1/W, the corresponding values of W are quite different there.

I'm currently using the pixel shader fix only for objects that are close enough to pass through the near plane
Hello..

* I don't really understand this log trick with Z: vertex shader has 32bit float precision calculation, z-buffer also has 32-bit precision. If, after matrix multiplication z value already lost necessary precision, doing log here just increases rounding error & nothing more. At the same time, if resulting z value still has enough precision then z-buffer perfectly will store the resulting value. So where is the win here?

* Lack of fast-z optimization appears because you're doing logarithm in pixel shader, right? It causes a confusion for me, because I didn't even imagine that logarithm is being done in pixel shader. What was a reason of using log in PS?

* About multiplying on 'w' - if you don't need w value (you're trying to compensate for the division) then just make it equal to '1', don't need to multiply on 'w' then (& you may also avoid w = 0 suspicious situations).

* We are talking about Z-buffer storing integers, not floats. For floating-point Z-buffer there would be no problem, since floats are basically integers with logarithmic part (exponent) [smile]

* Yes, fast-Z problem arises when you use the logarithm in pixel shader, what is done to suppress the artifacts appearing when polygons cross the near plane. You may use the logarithm in pixel shader only for objects crossing the plane, though.

* If you mess with W you will affect perspective correct texturing ...
PARTNERS