Tip of the day: logarithmic zbuffer artifacts fix

Published August 20, 2009
Advertisement
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 ?
0 likes 13 comments

Comments

cameni
--
August 20, 2009 01:43 PM
cameni
--
August 20, 2009 01:47 PM
cameni
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!
August 20, 2009 01:48 PM
Ysaneya
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.
August 20, 2009 03:35 PM
cameni
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].
August 21, 2009 05:11 AM
Ysaneya
Yeah, I realized that too (and I tested to make sure, the [0-1] range in the pixel shader is indeed correct :)).
August 21, 2009 10:52 AM
Matias Goldberg
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
August 21, 2009 09:40 PM
Ysaneya
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.
August 22, 2009 06:31 AM
Martin
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
August 24, 2009 05:09 AM
Ysaneya
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.
August 24, 2009 08:03 AM
cameni
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
August 24, 2009 08:59 AM
johny5
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).

September 01, 2009 11:03 PM
cameni
* 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 ...
September 02, 2009 03:18 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement