2011-02-04
Logarithmic Depth Buffer
========================
Background
----------
There are a good couple of blog postings by Brano Kemen about
z-buffers for planetary-scale rendering. He explains how to use a
log() function to map view z into depth buffer values, that
distributes resolution more evenly than the commonly available z- and
w-buffers.
http://outerra.blogspot.com/search/label/depth%20buffer
Slightly earlier versions appears on gamasutra as well, links:
http://www.gamasutra.com/blogs/BranoKemen/20090812/2725/Logarithmic_Depth_Buffer.php
http://www.gamasutra.com/blogs/BranoKemen/20091231/3972/Floating_Point_Depth_Buffers.php
He also explains that you can get the benefit of a logarithmic z
buffer by using a floating-point z-buffer, and running it backwards.
I.e. map the near values to larger depth-buffer values, and use
glDepthFunction(GL_DEPTH_LEQUAL) instead of GL_DEPTH_GEQUAL (or DX
equivalent).
Humus also has an earlier article that advocates running the
floating-point depth buffer backwards:
http://www.humus.name/index.php?ID=255
Kemen's formula for the z buffer is:
f(z) = log(C*z + 1) / log(C*Far + 1) * K
Where K = maximum value in depth buffer and C is a tuning parameter.
My Slight Variation
-------------------
I studied the same problem and came to similar conclusions, but
without the tuning parameter. Here's the function I derived:
assume an integer depth buffer
k is the number of bits in the depth buffer
K = 2^k - 1
z is the view-space z value for the pixel in question
zn is the position of the near-clip plane
zf is the position of the far-clip plane
f(z) = integer value to write into depth buffer
f(z) = K * log(z / zn) / log(zf / zn)
This function gives constant relative precision. I.e.
g(i) = the view z value that maps to depth-buffer value i
= zn * exp((i / K) * log(zf / zn))
delta(z) = how far (in view space) to the next discrete depth value
= g(f(z) + 1) - z
rel(z) = relative precision
= delta(z) / z
= (n * exp((K * log(z/n) / log(f/n) + 1) / K * log(f/n)) - z)/z
= (zf / zn) ^ (1 / K) - 1
Implications
------------
The function f(z) is designed so that rel(z) is constant! In some
sense this is the optimal way to distribute z-buffer precision. And
in practice, it is DRASTICALLY better than an ordinary z or w buffer.
The z-buffer piles up nearly all the resolution right up against the
near plane, at the expense of everything else, and the w buffer
distributes things linearly in view space, which is very bad for
close/mid objects.
For example, using the log formula we can use a 16-bit z-buffer to
draw planetary-scale scenes! Let's make the world coords in meters,
set zn to 1 meter and zf to 100M meters (~16 Earth radii):
let zn = 1
let zf = 100e6
f(zn) == 0
f(zf) == 65535
g(0) = 1
g(1) = 1.000281
g(2) = 1.000562
...
g(65534) = 99971895.79
g(65535) = 100000000.0
Relative precision is ~ 0.000281, meaning at a depth of z, the depth
buffer can resolve a difference in depth of (z * 0.000281).
So for some values of z:
z precision
--------- ------------
1 meter 0.2 mm
100 meters 2.8 cm
100 km 28 m
100,000 km 28 km
That's sort of on the edge of what is easy to deal with, although
compared to a regular 16-bit z-buffer it's a miracle.
If that's not good enough, go to 24 bits. Same zf and zn, at 24 bits,
gives a relative precision of 0.00000014 . At zn = 1 meter, precision
is in microns. At zf == 100M meters, precision is 14 meters. That
should be enough for any imaginable system.
Caveats
-------
The main problem is that this function is not currently built into
hardware. You can simulate it in a pixel shader using gl_FragDepth or
DX equivalent, although that is not available in OpenGL ES 2.0
(i.e. mobile chips and WebGL). Also, even where it is available, it
will disable hardware speedups like early z-rejection and hierarchical
z test.
You can also sort of simulate it in a vertex shader, but honestly I
tried it and it sucks, because interpolated values in the middle of
triangles are way off, and you end up with crazy depth-sorting errors.
The best thing would be to get the function added to hardware.
Compared to floating-point Z
----------------------------
As Kemen and Humus observe, running a floating point z buffer
backwards has a similar effect. The advantage is, current high-end
hardware does support this, yay! The disadvantage is, current low-end
hardware (i.e. mobile) does not. Also, floats need more bandwidth for
a similar result, so if we could fix the hardware, we'd go for the
integer depth buffer using the log function.
Compared to conventional Z or W buffering
-----------------------------------------
No contest; those both totally suck.
Visualizer
----------
This page lets you interactively graph relative imprecision of several
depth functions with various parameters. The code is embedded in the
page as Javascript so you can inspect it or change it.
http://tulrich.com/geekstuff/log_depth_buffer_vis.html
Some insights from the visualizer:
* There's sort of a zero-sum situation -- getting more precision in on
part of the view depth means you will lose precision somewhere else.
No free lunch.
* Yet some functions are better than others. I happen to think that
worst-case relative imprecision is the ideal metric to minimize, and
the functions behave differently with respect to this metric.
* Adding bits helps all of the functions. With enough bits, even the
bad ones can be fine in practice.
* Conventional Z and conventional W are both quite bad. Z is really
precise at the near plane and bad everywhere else, while W is the
reverse.
* Conventional floating-point Z is really extra bad. 32-bit float z
is (in the worst-case) no better than 24-bit fixed-point Z!
* Floating-point W and reverse floating point Z are both pretty good,
and exact mirror images of each other. I'm not sure if
half-precision (16-bit) floating-point Z is common in GPUs, but if
so, doing reverse half-precision Z could be a good option for many
apps.
* The log(z/zn)/log(zf/zn) function beats everything else in all
conditions (it's optimal for the metric).
References
----------
Brano Kemen 2009-08-12 posting on gamedev.net, looks like slightly
earlier version of gamasutra posting. A good graph of his log
function near origin.
http://www.gamedev.net/blog/715/entry-2001520-logarithmic-depth-buffer/
Steve Baker's "Learning to Love your Z-buffer" mentions "Floating
point or Logrithmic Z (these are pretty similar concepts
actually). This also tends to even out the distribution of bits over
the Z range - but without completely eliminating the improved
resolution close to the eye. SGI Infinite Reality machines do this -
and claim to get the equivelent *useful* Z precision using 15 bits as
a conventional machine gets with 24bits." No date or further details
but the Infinite Reality thing sounds similar and IR came out in 1996.
http://en.wikipedia.org/wiki/InfiniteReality (as of 2011-02-04)
doesn't say anything about this.