Depth buffer values read in FS extremely inaccurate (in just a few units distance)

First of all: I’m well aware of Z-fighting issues but what I’m observing here looks like an error in my code as it even occurs in relatively close distances. The main issue is that I’m using a lot of undocumented functions and it’s very hard to find anything useful on Google.

The shader I’m testing with just reads the value from the depth buffer, compares it to the fragment depth and outputs the difference. The test scene is just one cube and one plane, the cube writes to the depth buffer and the plane cuts the cube diagonally and has the following shader attached to it:

Shader "Custom/DepthTest"
{
    SubShader
    {
        Tags
        {
            "Queue"="Geometry"
            "RenderType"="Opaque"
        }

        Fog
        {
            Mode Off
        }

        Pass
        {
            Cull Back
            ZWrite Off
            ZTest Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            uniform sampler2D _CameraDepthTexture;

            inline float ZBufferRead(float4 projpos)
            {
                return tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(projpos)).r;
            }

            struct v2f {
                float4 pos     : POSITION;
                float4 projpos : TEXCOORD0;
            };

            v2f vert(appdata_base v) {
                v2f o;

                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                o.projpos = ComputeScreenPos(o.pos);

                return o;
            }

            fixed4 frag(v2f i) : COLOR {

                float zvalue = ZBufferRead(i.projpos); // Z-Buffer value
                float zdepth = LinearEyeDepth(zvalue); // Z-Buffer depth
                float fdepth = i.projpos.z;            // Fragment depth

                float depth_delta = fdepth - zdepth;

                return float4(depth_delta, depth_delta, depth_delta, 1);
            }

            ENDCG
        }
    }

    Fallback "None"
}

In case you want to test it on your machine, remember to enable the depth texture of your scene and editor cameras, for example by attaching this script to the scene camera:

using UnityEngine;

[ExecuteInEditMode]
public class EnableDepth : MonoBehaviour
{
    void OnPreCull()
    {
        if (camera != null) camera.depthTextureMode = DepthTextureMode.Depth;
    }

    void Update()
    {
        if (Camera.current != null) Camera.current.depthTextureMode = DepthTextureMode.Depth;
    }
}

The result looks as follows:


(right click → open in new tab to see all details)

I got the idea to play around with this from this blog post and the shaders he posted seem to suffer from the same issue. The threads I found on Google did pretty much the same thing (tex2dproj + linearization) but I couldn’t find anything describing my issue.

The second day of testing is coming to an end and I found some interesting facts.

I wrote this shader:

float zvalue = ZBufferRead(i.projpos); // Z-Buffer value
float zdepth = LinearEyeDepth(zvalue); // Z-Buffer depth
float fdepth = i.projpos.z;            // Fragment depth
float fvalue = (1/fdepth - 1/_ProjectionParams.y)
             / (1/_ProjectionParams.z - 1/_ProjectionParams.y);

Where the “Xvalue” variables represent the numbers that have been or will be written to the depth buffer and the “Xdepth” variables are the linearized distances to the camera.

I rendered a test cube twice, once with the default diffuse shader and once with the test shader, with the depth buffer differences as the color:

float f = abs(fdepth - zdepth);
return float4(f, f, f, 1);

The result looks like this:

I did a second run where I wrote “fvalue - zvalue”, which at first rendered a solid black cube no matter how far I zoomed out. However, I realized that this might be because the differences in the nonlinear space are just too low for the screen to display, so I multiplied the output with 10000 and the artifacts appeared again, meaning that the error is independent of the linearlization.

To verify this further I went ahead and converted the “Xvalue” variables to the “Xdepth” variables and back again. I did this in a for loop to see if some sort of error accumulation would happen. That was not the case.

float zdepthbackup = zdepth;
float zvaluebackup = zvalue;
for (int i = 0; i < 100; i++)
{
    zdepth = 1
           / (zvalue * (1/_ProjectionParams.z - 1/_ProjectionParams.y) + 1/_ProjectionParams.y);

    zvalue = (1/zdepth - 1/_ProjectionParams.y)
           / (1/_ProjectionParams.z - 1/_ProjectionParams.y);
}

I think I eliminiated most possible error sources. What’s left is the writing to the depth buffer. This appeared strange to me as Unity uses a 24 bit depth buffer if the hardware supports it. I have a GTX 460 in this machine so I’d say that shouldn’t be the limiting factor.

The question remaining is: Does a 24 bit depth buffer really introduce such a massive error? If so, I guess I could just introduce that error to my “fdepth” variable as this should at least get rid of the gradients. However, to do this I’d have to somehow find out the depth buffer resolution of the hardware, reduce the float to that bit number and work with the resulting value. I’ll go get something to eat and dive into this later tonight, but if in the meantime somebody reads this, feel free to post your thoughts or any ideas on how to force the loss in precision in the “fdepth” variable to trade the gradients for some (hopefully less disturbing) banding.

EDIT: I just changed my my rendering path from forward to deferred and the artifacts are gone. Now I’m really speechless - is the precision of the depth buffer in deferred rendering automatically higher (e.g. 32 bit) because of the enhanced dependency on the depth buffer? If so, can this additional accuracy be achieved in forward rendering, too?

Depending on the graphics API / platform / render pipeline, Unity may store depth values encoded in 32 bit colour texture. To ensure you decode it correctly you need to use the UNITY_SAMPLE_DEPTH macro.

float ZBufferRead(float4 projpos)
{
  return UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(projpos)));
}

Thanks for the reply. It’s good to know some more details about how Unity handles the depth buffer, but I actually already had this in my code and just removed it for testing purposes. It didn’t fix the forward rendering artifacts for me, so far the only solution I know of is running in deferred lighting.