Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

What does Unity exactly do when we modify z buffer value using SV_Depth?

Discussion in 'Shaders' started by alphaxiao, Apr 12, 2018.

  1. alphaxiao

    alphaxiao

    Joined:
    Aug 4, 2017
    Posts:
    8
    I am developing a shader which rely on modifying z buffer in fragment shader.
    And now I use keyword SV_DEPTH to modify the z buffer, but I meet some problems.
    I did some experiment and have some questions. The experiment result is quite confusing so I am asking for your help.

    The experiment shader looks like following. It has two passes.
    // Z pass
    Pass {
    ZWrite On
    ColorMask 0
    vert() {
    o.pos = UnityObjectToClipPos(v.vertex);
    .....
    }

    frag(o, out float outDepth : SV_Depth) {
    // I test several depth value
    // n is near plane, f is far plane, retrived from unity variable _ProjectionParams
    outDepth = o.pos.z; // (1) this works out fine, like the one without modifying depth
    outDepth = depth read from camera depth texture // (2) this one produce incorrect result,
    outDepth = o.pos.z / o.pos.w // (3) incorrect result
    outDepth = (o.pos.z / o.pos.w) * (f - n) / 2 + (f + n) / 2 // (4) incorrect result
    }
    }
    // base forward shading pass
    Pass {
    Zwrite Off
    ZTest LEqual
    // do the normal shading task
    }

    I test four different depth, and only (1) has the correct result (the result has no strange holes and z fighting and there is no noise in image).

    When we modify the depth using SV_Depth, the compiled shader should modify the opengl variable gl_FragDepth, right? And according to opengl wiki(https://www.khronos.org/opengl/wiki/Vertex_Post-Processing#Perspective_divide), the z value written into z buffer should in window-space coordinate, which is calculated by this formula (If we do not modify z buffer manually):

    upload_2018-4-12_14-51-19.png

    So can anyone explain this, or is there any logical error in my understanding?

    And I am using Unity 2017.3.1f1, the platform is MaxOS with OpenGlCore API setting
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    1 - Of course this works, the pos.z is literally the value that would be written to the depth if your shader did not have an SV_Depth output. That’s the “window space” depth value.

    2 - I’d be curious how you’re sampling the depth texture, as that texture should also be exactly the same as the pos.z value assuming you’re just using the sampled value straight and not using the functions to convert to linear distance.

    3 - The pos.z value has already been divided by pos.w as part of the conversion from the vertex shader’s clip space to the post rasterization window space that is passed into the fragment shader. Dividing by w again doesn’t make much sense.

    4 - That’s the formula for converting from OpenGL’s normalized device space to window space. The GPU already did that for you and gave you the results in the form of the pos.z available in the fragment shader.

    The thing I think you’re missing is that the pos.z value output from the vertex shader is not the same one that comes into the fragment shader. The vertex shader outputs its position in homogeneous clip space. That value is converted into normalized device coordinate (NDC) space and then into window space as part of rasterization. The resulting value is what comes into the fragment shader. In OpenGL terms the vertex shader is writing to gl_Position and the fragment shader is reading gl_FragCoord. DirectX 10 HLSL simply uses the SV_Position semantic for both.

    To modify the depth in the fragment shader you’ll likely want to convert the depth into a linear depth, make your changes, then convert back to the non-linear depth the SV_Depth (and gl_FragDepth) is expecting using the built in LinearEyeDepth function and a function that reverses that. LinearEyeDepth is defined like this:
    Code (CSharp):
    1. inline float LinearEyeDepth( float z )
    2. {
    3.     return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
    4. }
    That’ll get you the depth value in linear world space units. You can then use a similar function to convert back:
    Code (CSharp):
    1. inline float LinearEyeDepthToOutDepth(float z)
    2. {
    3.     return (1 - _ZBufferParams.w * z) / (_ZBufferParams.z * z);
    4. }
     
  3. alphaxiao

    alphaxiao

    Joined:
    Aug 4, 2017
    Posts:
    8
    First of all, thank your, bgolus! I've been messing up with this problem for several days. Your post is quite intuitive and insightful.

    I check my code used in case (2). You are right. I used the wrong uv to sample the depth texture and the value from depth texture is absolutely equal with o.pos.z .

    So, what I get in fragment shader about pos.z, is in window-space(mentioned in the opengl's wiki), right?

    Actually, I am doing a project which want to reproduce a rendering result from unreal engine 4. And in UE4's material, there is a node called pixel depth offset. I looked into UE4 source code, and they apply this node like following,

    Code (CSharp):
    1. // get custom pixel depth offset value
    2. float PixelDepthOffset = max(GetMaterialPixelDepthOffset(PixelMaterialInputs), 0);
    3. MaterialParameters.ScreenPosition.w += PixelDepthOffset;
    4. OutDepth = MaterialParameters.ScreenPosition.z / MaterialParameters.ScreenPosition.w;
    5.  
    So, what I should do, in fragment shader, is transforming the o.pos.z back into clipSpace,

    clipZ = (o.pos.z - (f + n) / 2 ) / ((f-n) / 2) * o.pos.w

    And apply offset to o.pos.w, then change clipZ into window-space

    newZ = clipZ / (o.pos.w + pixelDepthOffset) * (f - n) / 2 + (f + n) / 2

    Am I right about the transformation? Because the result is still not what I am expecting, maybe there are other problems related to the platform(engine), such as scale or handness of coordination.


    And I want to mention one more thing. It may help others. It is about modifying z buffer in shader.
    My original shader uses two passes, one with Z Write On, the other with Z write off. If I want to modify depth(z buffer), I should output depth in both passes. If I only change depth in the first pass(with Z write on), then when the next pass comes to Z Test, there may be some inconsistency and cause Z fighting or strange result (because it is using the original depth to check against the modified z buffer).

    Thank you again, bgolus. Your reply is really helpful.
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    It certainly looks like Unreal is doing some work in something like clip space depth. I have no idea what values are in the ScreenPosition variable to say for sure, or if anything else is done to OutDepth. Assuming OutDepth is directly output as the SV_Depth or gl_FragDepth value then it is not the clip space value, but rather some other value that's not quite clip space, and not quite NDC space, but something else. Doing the perspective divide (z/w) with the clip space depth gives you the NDC space depth, but that code appears to be directly getting window space depth after a similar z/w.

    I also have no idea what the expected values input into Pixel Depth Offset are. World space depth would be the easiest for users, and indeed the clip space w is in fact world space depth, so I would wonder if ScreenPosition.z is not simply window space z * clip space w and ScreenPosition.w == clip.w? I have no idea if that would produce usable results.
     
    alphaxiao likes this.
  5. alphaxiao

    alphaxiao

    Joined:
    Aug 4, 2017
    Posts:
    8
    Thank you again, bgolus!

    When I search the problem in google, I came up with something called z buffer precision. And my understanding of it is that it's about how near/far plane mapped into [0, 1] z value. And particular nonlinear mapping can help increase precision close to near plane, thus give more detail to object close to the camera.

    So, with clip space z, NDC z, window-space z. Where does this mapping happen? Is it happen between view space to clip space (with a scaling value store in w component). I read articles about how projection matrix be constructed(same with the one unity uses), and it seems they do not care very much about the precision. They use a linear mapping regards to 1/z, not the one with better performance(logarithmic depth http://outerra.blogspot.com/2009/08/logarithmic-z-buffer.html). Am I missing something here?

    And I want to check something,
    1. The value of z buffer is range from [0, 1], right?
    2. The NDC to window-space is linear and opengl does the transform for us?
    3. clip.z / clip.w is range from -1 to 1 ?
    4. The reason of why we use a non-unit value w in clip space, is that from vertex shader to fragment shader, we can interpolate each component(x,y,z) linearly?
    5. when we modify the z buffer in the fragment shader, we lose the feature called early z culling. But in UE, there is a keyword called [[depth(less)]] (which specifies the output depth variable, similar to SV_DEPTH in unity). I guess it is supported by opengl or hardware, and the comments in UE source codes say it preserves some degree of z culling. Can we do this in Unity?

    Thank you, bgolus! For your precious time and great answers.
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Yes.

    The GPU does both the clip to NDC and NDC to window space transforms. It is a linear transform between the NDC and window space, though the clip.z is likely non-linear, and thus neither are the NDC z nor the window space z.

    For OpenGL, yes. DirectX is 0 to 1, or rather 1 to 0, I'll get back to that.

    It's so we can interpolate each component linearly in screen space, yes. A great example of this is to do something like sample a texture using the clip space XY values (passed as an additional texcoord). If you do the perspective divide (XY/W) in the vertex shader, any geometry not perfectly parallel with the view plane will have obvious warping. Doing the perspective divide in the fragment shader produces the expected results.

    I believe so. Try:
    [earlydepthstencil]

    However I believe that only works on Windows or consoles. I don't believe MacOS's OpenGL actually implemented that feature.

    Coming back to this. Unity uses reversed Z on DirectX and consoles. This is why I said the depth is 1 to 0. If you look at the update to that link you posted they go into the fact that using a reversed Z is nearly on par with the manual logarithmic depth with no performance impact and zero shader modifications needed. Just flip the projection matrix, the culling, and ZTest settings and you're done.

    However to support reversed Z in OpenGL requires 4.2+ or some extensions, neither of which MacOS, iOS, or Android devices support, so it'd only be useful for OpenGL on Windows (or Linux). I believe Metal and Vulkan can also use a 0 to 1 z range, but I don't know if they use a reversed Z for those as well or not.

    To use a log based Z on the other hand will work on any device, and the benchmarks on that site do make it seem like it shouldn't be a big problem, but it is a minor performance hit along with requiring specific shader modifications. The implementation of which are likely far more impactful on mobile devices.

    It should also be noted that both of those articles found that the lack of early Z wasn't a big deal... that's something that likely would not hold true in today's world of significantly more complex fragment shaders and the jump in screen resolutions.
     
    Last edited: Apr 14, 2018
    alphaxiao likes this.
  7. Fragsteel

    Fragsteel

    Joined:
    Nov 8, 2016
    Posts:
    6
    Hope this isn't too off-topic, but wanted to thank bgolus for all the detailed replies here because it helped me solve a problem I've spent days trying to fix, and post what I did (related) in case someone else has a similar problem.

    I'm doing AR portals, and had trouble because if a portal is closer than your eye than the near plane, but you haven't transitioned through the portal yet, it would clip.

    My solution was to clamp the vertices' z values to the near/far planes in the portal's vertex shader:

    o.vertex.z = clamp(o.vertex.z, -1, 0);


    But this messed with the depth in such a way that it wasn't visible. If I manually added a depth offset I could make it visible but it was extremely inconsistent.

    So I figured I'd save the depth before performing the clamp, and then apply it in the frag shader. But this messed with the depth really badly, so that objects intersecting the portal would be occluded in odd ways when I wasn't facing the portal directly. It's tricky to explain, but if my head was at a 20° degree angle from the portal, then the intersecting object would clip as if the portal was at a 40°.

    That's where bgolus's explanation of the NDC conversion happening after the vertex shader was done saved my ass. I just divided the pre-clamp depth value I saved by the final W value of the vertex, and it worked like a charm:

    depth = i.preclampdepth.x / i.vertex.w;


    I attached the full portal shader in case it helps someone in the future. The idea is that _MainTex is a RenderTexture rendered by the same camera in Update() (before the portal itself is drawn) but with the culling mask set to different layers.
     

    Attached Files:

    asqewfcq2egf and bgolus like this.