Search Unity

Rendering depths into render textures in both OpenGL and Direct3D

Discussion in 'Shaders' started by svizcay, Sep 4, 2017.

  1. svizcay

    svizcay

    Joined:
    Oct 26, 2015
    Posts:
    28
    Hi, I have to write some shaders that works well in both OpenGL and Direct3D.

    In one of the shaders, I render the depth of the scene into a RenderTexture (writing to its color buffer).
    In that shader, I set:
    Code (CSharp):
    1. ZWrite On
    2. ZTest Less
    And I clear the depth buffer to 0.

    I know that in OpenGL the depth buffer goes from 0 (near plane) to 1 (far plane) and that in Direct3d goes from 1 (near plane) to 0 (far plane). I was expecting Unity to hide this difference but I think it doesn't. I'm using i.vertex.z in the fragment shader as depth value (being i.vertex the SV_POSITION after multiplying a vertex with its MVP matrix in the vertex shader). By the way, I'm sending Model, View and Projection matrices to the shader manually by calling material.SetMatrix(name, matrix) (I needed to be like that).

    My fragment shader is as following:
    Code (CSharp):
    1. struct fragOutput {
    2.     float4 color : SV_TARGET;
    3.     float depth : SV_DEPTH;
    4. };
    5.  
    6. fragOutput frag(v2f i)
    7. {
    8.     fragOutput o;
    9.     float depth = i.vertex.z;
    10.     o.color = float4(depth, depth, depth, 1);
    11.     o.depth = depth;
    12.     return o;
    13. }
    14.  
    15.  
    In OpenGL the previous code works like this:
    * depth test works well (geometries close to the near plane cover geometries behind them)
    * geometries close to the near plane has a dark color and geometries farther away are whiter.

    Running the same shader in Direct3D results like this:
    * depth doesn't work. Geometries at the back are being drawn over geometries at the front.
    * geometries at the front are darker and geometries at the back are whiter

    So according to that (by inspecting the output color), i.vertex.z goes from 0 (near) to 1 (far plane) in both, OpenGL and Direct3D. The depth written to the depth buffer in Direct3D is wrong though.

    To fix that, I added the following code.
    Code (CSharp):
    1.  
    2. #if SHADER_API_D3D11 || SHADER_API_D3D9
    3.     o.depth = 1 - i.vertex.z;
    4. #else
    5.     o.depth = i.vertex.z;
    6. #endif
    And with that the colors "seems" to be the same (they are slightly different in terms of intensity) and depth test works.

    Inverting the value written into the depth buffer seems to make it work but I don't understand why. If i.vertex.z is 0 for vertices in the near plane, the value that I write to the depth buffer (direct3D convention) then it should be 1 and by using ZTest Less, they should be discarded. I'm right?

    There are 2 problems with this approach, first, the intensity of the pixels are slightly different, what makes me get a higher accumulate error when doing additional calculations and second, there is a drop of FPS when running using Direct3D. I guess it is because sometimes I write a value higher than 1 (I don't know why this happens) to the depth buffer and that makes the gpu run some extra code.

    Please let me know if what I'm doing is the right way and if my guess about i.vertex.z is correct.

    Maybe I should use the helper macros commented here https://docs.unity3d.com/Manual/SL-DepthTextures.html but I don't understand how they work, when I should call them (vertex or fragment shader), what's the input and what's the output.

    I would like if someone explain better how those macros are supposed to be used by given a full example. According to the doc:

    What is "i"? Is it a float3? is it in clip coordinates? where do I get the output back? in the fragment shader? by assigning the return value to a variable in the vertex shader?

    When I searched for that this is the only example that I found but that doesn't match the documentation.
    https://forum.unity3d.com/threads/direct3d-opengl-camera-depth-difference.488780/#post-3188384

    Many thanks for your help!
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,342
    It's a little more complicated than that.

    Clip space Z for OpenGL is -1 to 1, and DirectX is 0 to 1. Due to the peculiarities of floating point values, inverting 0.0 to be far and 1.0 to be near gives more useful precision across the depth range. So for the platforms that support it (ie: DX11), Unity does this reverse, as well as flips the ZTest values to their inverse (ie: Less becomes Greater, etc.). It's not done for OpenGL because flipping the -1 and 1 range doesn't give any benefits.

    However the depth texture doesn't reflect this -1 to 1 z range in OpenGL because ...

    This value is not the value that was set in the vertex shader, but instead it is the value post rasterization. If you look at the x and y values for instance these will both be the pixel coordinates in the fragment shader (values 0 to resolution width & height - 1) where as they were -w to w values in the vertex shader. Similarly the z has been modified from value set in the vertex shader to a 0 to 1 depth range. You also may not want to rely on using SV_POSITION in the fragment shader. Some platforms don't support reading this in the fragment shader, like mobile. If you're just doing this for desktop and not mobile then it should be fine.

    Because the documentation is subtly wrong. The "i" in COMPUTE_EYEDEPTH(i) should be "o". It's the variable the macro assigns the output eyedepth value to.
    Here's the function as it appears in UnityCG.cginc.
    Code (CSharp):
    1. #define COMPUTE_EYEDEPTH(o) o = -UnityObjectToViewPos( v.vertex ).z
    So it should be used like this:
    Code (CSharp):
    1. float eyeDepth;
    2. COMPUTE_EYEDEPTH(eyeDepth);
    Note, this is eye depth, aka linear view space depth, not the post rastorization projection space depth buffer depth. In the shaders that use COMPUTE_EYEDEPTH() you'll see they use the LinearEyeDepth() function to convert from the depth texture (which is in depth buffer depth) to linear depth.

    You should use #if UNITY_REVERSED_Z if you're testing for this case. Unity does not use the reversed z for D3D9!

    I highly suggest you download the built in shader source files and look through them yourself if you're getting into the gritty details of stuff like this.
     
  3. svizcay

    svizcay

    Joined:
    Oct 26, 2015
    Posts:
    28
    Hi, thank you very much for your answer but I still have a couple of questions.

    So i.vertex.z (in the fragment shader) goes from 0 for vertices at the near plane and 1 for vertices at the far plane right? for both platforms? Is [0, 1] the real limit or they can go beyond those values?
    I was using this post as reference. Those values work independently of platform, right?

    Actually, I also have to make sure that runs on mobile platforms (and using oculus as well). Should I replace SV_POSITION by POSITION? By changing that, Do I read different values in the fragment shader or are those still the same?

    I just played around with that macro but I don't understand the visual result that I get. I declared and extra float4 variable in the v2f struct (using a texcoordN semantic) and set one of its components to the value set by that macro by calling COMPUTE_EYEDEPTH(o.eyeDepth.z) (all of this in the vertex shader). Then after that, in the fragment shader I use the interpolated value i.eyeDepth.z as my new depth. The output doesn't make any sense, sometimes a see objects far away, sometimes I don't (same with near objects...and I'm making sure objects are within the camera frustum). I see objects with the same white intensity even though they are at different distances from the camera.

    Is the eye depth the distance along the z axis from the camera to the object in camera space, right? The second one that you mentioned is the depth in "projection"/"screen? space? Should I call LinearEyeDepth() in the fragment shader by doing something like this?
    Code (CSharp):
    1. float depth = LinearEyeDepth(i.eyeDepth.z);
    2. o.color = float4(depth, depth, depth, 1);
    3. o.depth = depth;
    If I do that, I get a super weird result, like gray spheres with white borders and a solid gray cube that doesn't change its color as often as the sphere when I move the camera.

    By the way, I'm not reading values from depth textures. I use framebuffers (rendertextures) with color and depth buffers. I render the depth to the color buffer, use the framebuffer's depth buffer for depth comparison and then I pass the color buffer as an input texture for the next step in my shader pipeline.

    Thanks! I'll change that if statement and download the built in shaders but is still a little bit hard to understand having all these different spaces and depth values.
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,342
    The range should be clamped to 0 to 1, but no it's not going to be the same for OpenGL and DirectX 11. If Unity is using reversed z depth then 1 will be near and 0 will be far, like you've been seeing. As far as the APIs are concerned 1 is still "far", but Unity is rendering with ZTest GEqual instead of the "default" LEqual. Unity's functions for converting from depth buffer to linear depth are handling the inversion for you.

    No, SV_POSITION is what DX11 uses, but it gets automatically translated to POSITION for DX9 and OpenGL (actually gl_Position for OpenGL) for the vertex shader. And I was wrong here, I think only DX9 has some potential issues with using POSITION in the fragment shader. For all OpenGL variants POSITION is translated to gl_FragCoord in the fragment shader. In DX9 this is the equivalent of VPOS. For DX11 SV_POSITION is accessible in the fragment shader, but it has the same data that VPOS has.

    In all cases, gl_FragCoord, VPOS, or SV_POSITION, the z is the post rastorization depth buffer value.

    Eye depth is not the value that shows up in the depth buffer! It's linear depth.

    Yes, EyeDepth is the distance from the camera. So something that is 1 unity away will have a value of 1 in EyeDepth. The depth buffer value for something that's 1 meter away depends on the fov, near plane, and far plane, and is non-linear when using perspective (ie: not orthogonal). In other words if your near plane is 0.1 and far plane is 1.9, a distance of 1 meter from the camera is not a depth of 1 (that's what 1.9 is), or 0 (that's 0.1), or 0.5 (that depends on the fov)!

    Eye depth is already the linear eye depth value, so using this function on it is going to get you all sorts of weird results. The LinearEyeDepth() function is assuming a value with a range of 0.0 to 1.0 (with near at 0 or 1 depending on what Unity is doing) and converting it to match the same values as eye depth.

    Yep, it's complicated, and Unity's macro naming schemes don't help this.

    Lets go over this quickly.
    COMPUTE_EYEDEPTH()
    Get the linear depth of v.vertex (the local space vertex position) away from the camera along the z axis. 1 unit in world space == 1.0.

    COMPUTE_DEPTH_01()
    Get the linear depth of v.vertex away from the camera along the z axis, scaled to a range of 0.0 to 1.0. 0.0 is always at the camera, 1.0 is always the far plane. A value of 0.5 is always half way between the camera and the far plane.

    DECODE_EYEDEPTH() or LinearEyeDepth()
    Get the linear depth from the non-linear depth buffer. This is converting the non-linear 0.0 to 1.0 range (again, 0.0 may be the near plane or far plane, but the extremes are always the near and far planes) to linear eye depth. LinearEyeDepth("SV_POSITION.z") == COMPUTE_EYEDEPTH()

    Linear01Depth()
    Like LinearEyeDepth(), but matches the output of COMPUTE_DEPTH_01().

    Both LinearEyeDepth and Linear01Depth are expecting either the value from the _CameraDepthTexture or from the SV_POSITION variable's z component.

    "SV_POSITION.z"
    This is the post rastorization projection space Z buffer depth. Unity doesn't ship with any functions to convert from linear depth to non-linear Z buffer depth. However LinearEyeDepth() is:
    Code (CSharp):
    1. // Z buffer to linear depth
    2. inline float LinearEyeDepth( float z )
    3. {
    4.     return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
    5. }
    So if you solve the function for "z" it should give you the inverse.
    Code (CSharp):
    1. // linear to Z buffer depth
    2. inline float ZBufferDepth( float linear )
    3. {
    4.     return (1.0 - linear * _ZBufferParams.w) / (linear * _ZBufferParams.x);
    5. }
    I think that function should be able to take the output of COMPUTE_EYEDEPTH and convert it into the same value that shows up in the depth buffer and camera depth texture.

    If this is still confusing (and I suspect it is, because it's still confusing to me) try looking up some videos that explain z buffers and projection space depth.
     
    Last edited: Sep 5, 2017
    st-VALVe likes this.