I E

Sunstone dev log 16: depth in depth

I fixed the remaining z-fighting using logarithmic depth. It’s a very small change, so I spent a day and a half understanding how it actually works. The next most obvious visual artifact is moiré patterns on the textures at a distance, which I think I can fix with anti-aliasing.

Depth buffer resolution analysis

After perspective transformation PP, a vertex (x,y,z,1)(x, y, z, 1) looks something like this (assuming view space’s +Z axis points into the screen):

[x′y′zff−n−nff−nz] \begin{bmatrix} x' \\ y' \\ z\frac{f}{f-n} - \frac{nf}{f-n} \\ z \end{bmatrix}

where nn is the distance to the near plane, and ff is the distance to the far plane.

Perspective division brings the final Z coordinate to

depth(z)=ff−n−1znff−n \text{depth}(z) = \frac{f}{f-n} - \frac{1}{z}\frac{nf}{f-n}

So depth(z)\text{depth}(z) is the value stored in a depth buffer for a perspective-transformed vertex with view space depth zz, assuming n≤Z≤fn \le Z \le f (otherwise the vertex is clipped).

Let’s plot this relationship for n=0.001,f=1000n = 0.001, f = 1000 for 0≤z≤10000 \le z \le 1000. This nn and ff are the settings for the camera in the Bistro scene I loaded in my last Sunstone dev log.

Er, that’s just a horizontal line? Let’s zoom in to 0≤z≤10 \le z \le 1:

Okay, it has some shape now. Let’s enhance:

You can finally see the x-intercept, where depth(n)=0\text{depth}(n) = 0.

On the far right of the graph, we can see that depth(0.005)≈0.8\text{depth}(0.005) \approx 0.8. VK_FORMAT_D16_UNORM uses 16 bits for storage, meaning there are 216=655362^16 = 65536 depth values, where minimum depth is represented by 0 and maximum depth is represented by 65535. That means 0.8⋅216≈524290.8 \cdot 2^16 \approx 52429 are used to represent the depth for 0.001≤z≤0.0050.001 \le z \le 0.005, with the remaining 0.2⋅216≈131070.2 \cdot 2^16 \approx 13107 values representing depths for 0.005<z≤10000.005 \lt z \le 1000.

Invert depth\text{depth} to get

depth−1(d)=−1(f−nnf)(d−ff−n) \text{depth}^{-1}(d) = \frac {-1} {(\frac{f-n}{nf})(d - \frac{f}{f-n})}

and we can find the zz after which there are only 100 16-bit depth values remaining:

depth−1(1−100216)≈0.655 \text{depth}^{-1}(1 - \frac{100}{2^16}) \approx 0.655

or 10 values remaining:

depth−1(1−10216)≈6.5 \text{depth}^{-1}(1 - \frac{10}{2^16}) \approx 6.5

If we’re measuring distances in metres, this means there are only 100 depth “buckets” for points between 65cm and 1km away, and 90 of them are used to bucket points from 65cm to 6.5m away. By the same logic, points from 6.5m away to 7.2m (depth−1(1−9216)≈7.2\text{depth}^{-1}(1 - \frac{9}{2^16}) \approx 7.2) all share the same depth value, as do points from 7.2m to 8.1m (depth−1(1−8216)≈8.1\text{depth}^{-1}(1 - \frac{8}{2^16}) \approx 8.1). Finally, points further than 61.5m (depth−1(1−1216)≈61.5\text{depth}^{-1}(1 - \frac{1}{2^16}) \approx 61.5) are all considered to have maximum depth.

Logarithmic depth is the classic solution. It’s super simple to implement, but before I do I want to spend some time understanding exactly what’s going on.

I want to start with the idea of “resolution”. The article linked above gives a table that maps distance from the camera (in metres) to “resolution” (also in metres). My guess is that it’s answering the question of, at a given distance, how much change in distance Δz\Delta z is required to observe a change in depth Δd=depth(z+Δz)−depth(z)\Delta d = \text{depth}(z + \Delta z) - \text{depth}(z). The particular Δd\Delta d that’s we’re interested in depends on the number of bits in a depth value. A 16-bit depth buffer has 2162^16 possible depth values, which means we can only store outputs of depth\text{depth} in multiples of 1216−1\frac{1}{2^16 - 1}:

I want to know, at a certain distance zz, the Δz\Delta z that would cause the depth buffer value to increase by 1. In other words, which Δz\Delta z gives Δd=depth(z+Δz)−depth(z)=1216−1\Delta d = \text{depth}(z + \Delta z) - \text{depth}(z) = \frac{1}{2^16 - 1}?

All these differences are making me think of derivatives. We know that the derivative of depth\text{depth} is roughly the ratio of Δd\Delta d to Δz\Delta z:

depth′(z)≈ΔdΔz \text{depth}'(z) \approx \frac{\Delta d}{\Delta z}

So it should be the case that

Δz≈Δddepth′(z) \Delta z \approx \frac{\Delta d}{\text{depth}'(z)}

And for Δd=1216−1\Delta d = \frac{1}{2^16 - 1}

Δz≈1(216−1)depth′(z) \Delta z \approx \frac{1}{(2^16 - 1)\text{depth}'(z)}

The derivative of depth(z)=ff−n−1znff−n\text{depth}(z) = \frac{f}{f-n} - \frac{1}{z}\frac{nf}{f-n} is

depth′(z)=1z2nff−n \text{depth}'(z) = \frac{1}{z^2}\frac{nf}{f-n}

So we have

Δz≈z2(f−n)(216−1)nf \Delta z \approx \frac{z^2(f - n)}{(2^16 - 1)nf}

For n=0.001n = 0.001 and f=1000f = 1000, this gives

z Δz
0.001 1.5259e-8
0.01 1.5259e-6
0.1 1.5259e-4
1 1.5259e-2
10 1.5259
100 152.59
1000 15259

So at 10m away, objects less than 1.5m apart will appear to have the same depth value. Here’s the full plot:

My interpretation of this curve is that the ability to discriminate between distances degrades much faster than the distance increases. We can tell the difference between z=0.010z = 0.010 and z=0.011z = 0.011 (because 0.011−0.010=0.001≥1.5259⋅10−60.011 - 0.010 = 0.001 \ge 1.5259 \cdot 10^{-6}, so they’re assigned different depth values), but we can’t tell the difference between z=100z = 100 and z=110z = 110 (because 110−100=10<152.59110 - 100 = 10 < 152.59, so they’re assigned the same depth value).

The logarithmic depth solution says that we should be able to discriminate proportional distances, no matter how far away they are. So if we can discriminate z=0.010z = 0.010 and z=0.011z = 0.011, then we should also be able to discriminate z=1z = 1 and z=1.1z = 1.1, z=1000z = 1000 and z=1100z = 1100, etc. In other words, Δz∝z\Delta z \propto z. We already have Δz∝1depth′(z)\Delta z \propto \frac{1}{\text{depth}'(z)}, therefore we need depth′(z)∝1z\text{depth}'(z) \propto \frac{1}{z}. This is where the logarithm comes in: the derivative of logc(z)\text{log}_c(z) is proportional to 1z\frac{1}{z}.

This gives us a system of equations we can solve:

depth′(z)=c1zdepth(n)=0depth(f)=1 \begin{aligned} \text{depth}'(z) & = c\frac{1}{z} \\ \text{depth}(n) & = 0 \\ \text{depth}(f) & = 1 \end{aligned}

Integrate depth′\text{depth}':

depth(z)=∫c1zdz=cln(z)+d \text{depth}(z) = \int c\frac{1}{z} dz = c\text{ln}(z) + d

Solve for cc:

cln(n)+d=0d=−cln(n) \begin{aligned} c\text{ln}(n) + d & = 0 \\ d & = -c\text{ln}(n) \end{aligned}

cln(f)+d=1cln(f)−cln(n)=1c(ln(f)−ln(n))=1c=1ln(f)−ln(n)c=1ln(f/n) \begin{aligned} c\text{ln}(f) + d & = 1 \\ c\text{ln}(f) - c\text{ln}(n) & = 1 \\ c(\text{ln}(f) - \text{ln}(n)) & = 1 \\ c & = \frac{1}{\text{ln}(f) - \text{ln}(n)} \\ c & = \frac{1}{\text{ln}(f/n)} \end{aligned}

Solve for dd:

d=−ln(n)ln(f/n)=−logf/n(n) \begin{aligned} d & = -\frac{\text{ln}(n)}{\text{ln}(f/n)} \\ & = -\text{log}_{f/n}(n) \end{aligned}

Substitute:

depth(z)=ln(z)ln(f/n)−logf/n(n)=logf/n(z)−logf/n(n)=logf/n(z/n) \begin{aligned} \text{depth}(z) & = \frac{\text{ln}(z)}{\text{ln}(f/n)} - \text{log}_{f/n}(n) \\ & = \text{log}_{f/n}(z) - \text{log}_{f/n}(n) \\ & = \text{log}_{f/n}(z/n) \end{aligned}

The formula for Δz\Delta z in terms of this depth\text{depth} is:

Δz=zln(f/n)216−1 \Delta z = \frac{z\text{ln}(f/n)}{2^16 - 1}

Giving us

z Δz
0.001 2.1081e-7
0.01 2.1081e-6
0.1 2.1081e-5
1 2.1081e-4
10 2.1081e-3
100 2.1081e-2
1000 0.21081

with plot

This gives us sub-millimetre depth discrimination at 1m away. Surely that’s more than enough?

Next steps

References

← Sunstone dev log 15: Bistro is here Newest