Search code examples
unity-game-engineshaderhlslvertex-shader

UnityObjectToClipPos returns incorrect values for Z depth when compared to SV_Position


I have been trying to obtain the Z position of a vertex in the clip plane, i.e. its location in the depth buffer, but I have been observing weird behaviour affecting the result of UnityObjectToClipPos.

I have written a surface shader that colors vertices based on the depth. Here is the relevant code:

Tags { "RenderType"="Opaque" }
LOD 200
Cull off

CGPROGRAM
#pragma target 3.0
#pragma surface surf StandardSpecular alphatest:_Cutoff addshadow vertex:vert
#pragma debug

struct Input
{
    float depth;
};

float posClipZ(float3 vertex)
{
    float4 clipPos = UnityObjectToClipPos(vertex);
    float depth = clipPos.z / clipPos.w;
#if !defined(UNITY_REVERSED_Z)
    depth = depth * 0.5 + 0.5;
#endif
    return depth;
}

void vert(inout appdata_full v, out Input o)
{
    UNITY_INITIALIZE_OUTPUT(Input, o);
    o.depth = posClipZ(v.vertex);
}

void surf(Input IN, inout SurfaceOutputStandardSpecular o)
{
    o.Albedo.x = clamp(IN.depth, 0, 1);
    o.Alpha = 1;
}
ENDCG

Based on my understanding, UnityObjectToClipPos should return the position of the vertex in the camera's clip coordinates, and the Z coordinate should be (when transformed from homogenous coordinates) between 0 and 1. However, this is not what I am observing at all:

using UnityObjectToClipPos

This shows the camera intersecting a sphere. Notice that vertices near or behind the camera near clip plane actually have negative depth (I've checked that with other conversions to albedo color). It also seems that clipPos.z is actually constant most of the time, and only clipPos.w is changing.

I've managed to hijack the generated fragment shader to add a SV_Position parameter, and this is what I actually expected to see in the first place:

using SV_Position

However, I don't want to use SV_Position, as I want to be able to calculate the depth in the vertex shader from other positions.

It seems like UnityObjectToClipPos is not suited for the task, as the depth obtained that way is not even monotonic.

So, how can I mimic the second image via depth calculated in the vertex shader? It should also be perfect regarding interpolation, so I suppose I will have to use UnityObjectToViewPos first in the vertex shader to get the linear depth, then scale it in the fragment shader accordingly.


Solution

  • I am not completely sure why UnityObjectToClipPos didn't return anything useful, but it wasn't the right tool for the task anyway. The reason is that the depth of the vertex is not linear in the depth buffer, and so first the actual distance from the camera has to be used for proper interpolation of the depth of all the pixels between the vertices:

    float posClipZ(float3 vertex)
    {
        float3 viewPos = UnityObjectToViewPos(vertex);
        return -viewPos.z;
    }
    

    Once the fragment/surface shader is executed, LinearEyeDepth seems to be the proper function to retrieve the expected depth value:

    void surf(Input IN, inout SurfaceOutputStandardSpecular o)
    {
        o.Albedo.x = clamp(LinearEyeDepth(IN.depth), 0, 1);
        o.Alpha = 1;
    }
    

    Once again it is important not to use LinearEyeDepth inside the vertex shader, since the values will be interpolated incorrectly.