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

Standard Surface vs. Unlit shaders - backface culling and transparency

Discussion in 'Shaders' started by Cazil, Aug 14, 2021.

  1. Cazil

    Cazil

    Joined:
    Apr 18, 2014
    Posts:
    6
    Hi all!

    Just to preface, I'm new to writing shaders so sorry if anything is a little confusing. I tried to look this question up but couldn't find an answer online.

    I'm confused as to how transparency and culling work in Shaderlab. I'm writing a holographic-type shader in both a Standard Surface shader and an Unlit shader (just for learning purposes). They both have transparency enabled and need to do backface culling so the holographic effect doesn't show on internal geometry.

    In an Unlit shader, all I have to do to do backface culling is use:
    Code (CSharp):
    1. Cull Back
    to not render the back of the mesh.

    However, in a Standard Surface shader, I have to add an entire pass before the holographic pass:
    Code (CSharp):
    1. Pass
    2. {
    3.        Zwrite On
    4.        ColorMask 0
    5. }
    And if I put Cull Back instead of the second pass in the Standard Surface shader, it doesn't actually change anything! (at least visually).

    Why do I have to have a new pass for a Standard Surface shader, and why do Cull Back/Off/Front have different effects depending on what type of shader you're using?
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    I think you're misunderstanding what
    Cull
    does.

    Each triangle has a facing, which is determined by the winding order as it appears on screen. Basically if the vertices of a triangle are in a clockwise order when seen by the camera, that's the "front" of the triangle, and if it's counter-clockwise it's the "back" of the triangle.
    Cull
    determines if either side is skipped if it's facing the camera, where
    Cull Back
    hides the back of the triangle,
    Cull Front
    hides the front, and
    Cull Off
    shows both sides.

    That's it.

    I suspect what you're describing is not backface culling. I suspect what you're describing is overlapping front faces are visible and you want the ones in "the back" to not be. That's depth rejection.

    See this image from Unity's old documentation on culling and depth testing.

    Both of these meshes are using shaders with back face culling.

    The difference is the one on the right is using that extra
    Pass
    you posted above that you're using with your Surface Shader.

    That extra
    Pass
    is writing to the depth buffer (aka Z buffer) which is normally used for opaque depth sorting, but can also be used like this. The depth buffer's main reason to exist is when a triangle is rendered, it calculates its depth, and for each pixel of the triangle that's on screen it checks if it's further away than the value in the depth buffer. If it is, it skips rendering that pixel of the triangle. If it's closer, it renders that pixel and replaces the depth value in the depth buffer with the new closer value. You can control that behavior with
    ZTest
    and
    ZWrite
    if you want to change it, but the above is the default behavior if you don't specify.

    And that explains why your "unlit" vertex fragment shader is working like you want it. You probably set a
    Blend
    mode to make it transparent, but aren't changing the
    ZWrite
    behavior. However you're also getting lucky that all (most?) of the triangles closer to the camera in the mesh you're using are the ones rendering first. I suspect if you rotate around your model using that "unlit" shader you'll see seemingly random faces "in the back" show up still, just because the order of the triangles in your mesh won't and can't be perfectly sorted for all camera angles (for all but some very specific concentric convex shape setups). If you add
    ZWrite Off
    , as is usual for most transparent shaders, it'll look more like the Surface Shader did w/o the extra pass.

    So I said
    ZWrite On
    is the default behavior if there's no
    ZWrite
    line in the shader. That's kind of a lie, but also isn't. Surface Shaders are vertex fragment shader generators (as is Shader Graph), and setting a shader to be transparent by adding
    alpha
    to the
    #pragma surface
    line adds
    ZWrite Off
    to the generated shader passes. You could put
    ZWrite On
    in the shader at the start of the
    SubShader
    , but that won't do anything since the setting in the generated passes overrides whatever settings exist in the
    SubShader
    . However adding an additional pass yourself gets around that because it's not one being generated by the Surface Shader itself.

    It's also important that it's the first Pass in the shader because otherwise it'll be writing to the depth buffer after the rest of the passes have rendered. It's also the "correct" way to solve this problem because it ensures only the closest triangle surfaces for that one mesh are ever rendered regardless of the order they're in. So really you want to do this even for your "unlit" vertex fragment shader.
     
  3. Cazil

    Cazil

    Joined:
    Apr 18, 2014
    Posts:
    6
    It's like a lightbulb went off in my head! You're right, I totally got culling and depth mixed up - I thought backface culling would just remove all the stuff behind the front of an object, which doesn't make sense in retrospect. The generated ZWrite Off also clears up my confusion. Thank you so much for taking the time to help!

    I've read through a lot of your forum responses during my shader learning journey, lol. It's sort of an honor to have bgolus reply to a question of mine!