Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

ZWrite/ZTest for 2d shaders/sprites?

Discussion in 'Shaders' started by Martinx09, May 31, 2021.

  1. Martinx09

    Martinx09

    Joined:
    Sep 27, 2013
    Posts:
    22
    I was wondering if there is anything similar to the ZTest Always for shaders used on sprite renderers,
    I'm trying to have 1 pass be always visible and a second pass normal, so affected by layers/sorting.
    I want to be able to see my character when it is behind objects (that's what the first pass is supposed to do, just render the character grey, while the second pass renders what is supposed to be visible using the proper texture). Any ideas? :)
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,375
    To the specific question of
    ZTest Always
    , the only thing that's required to make a sprite render on top is to render it last. The layering of sprites is 100% determined by the order they render in.

    The "render an silhouette of a character behind walls" effect that's used in 3D games relies on those walls being opaque and writing to the depth buffer. Not for the silhouette itself, that's done by rendering that after everything else has rendered, just like the 2D case. But rather for rendering the visible part of the character model.

    The depth buffer ensures that opaque overlapping geometry always renders exactly the same regardless of the order they render in. This allows for things like intersecting geometry or complex / convex objects to render correctly without having to individually sort every triangle or pixel in the scene. But it only works for opaque objects because the depth buffer only holds a single value. Either the object being rendered is closer than the value in the depth buffer and renders & replaces the depth value, or its further and gets skipped.

    The short version of how the silhouette works is first you render all opaque objects in the scene so the depth buffer is filled. Then you render the silhouette pass with
    ZTest Always
    , which makes it ignore the depth buffer and render over everything. Then you render the character model normally as the very last thing with the default
    ZTest LEqual
    (which is what it is if you don't include that line at all), which allows the depth buffer to skip the pixels that are behind opaque walls.

    You can't normally do this with sprites as they usually aren't opaque, or they're all at the same z depth so the depth buffer wouldn't be able to sort them anyway.


    So there's kind of three ways to go about this:
    1. Use opaque sprites with depth.
    2. Use stencils.
    3. "Fake it" by doing the projection for real.*
    1. Opaque Sprites & Depth
    Basically you need to use alpha tested sprites that write to the depth buffer for anything you want to be able to show the silhouette on. And you need to actually adjust the z position of the sprites to ensure they're closer to the camera than the character sprite. Then you can use the same kind of setup as the 3D one. Just render the character last with the special shader and it "just works".

    2. Stencils
    Stencils require alpha tested sprites as well. Though there's more than one solution here.

    The first idea is you use a custom alpha tested shader that writes to the stencil buffer on sprites you want the outline to appear on. The character's sprite(s) then also use a custom shader that uses two passes. The silhouette pass only renders where the stencil has been written to, and the "regular" pass only renders where the stencil has not been written to. This also requires the character render after those objects you want the silhouette to render on, just like the depth based approach. The advantage is you don't have to worry about the z of your sprites.

    The second idea would be to have your character sprite use a shader with a pass that writes to the stencil using an alpha testing, but also uses
    ColorMask 0
    so it doesn't render anything else. The objects you want to show the silhouette then use two passes themselves, first one normal and a second one that only renders where the stencil has been written to. You'd use the alpha of the foreground sprite but replace the color with the silhouette color. Advantage here is you can keep the sorting exactly as you have now, and the foreground sprites don't have to be alpha tested. But the silhouette shape will be sharp.

    3. "Fake it"
    I'm using quotes here because the above techniques are debatably "faking it", and this option might be the "real" way to do it, but really it's all faking it.

    This option requires you render your character to a separate render texture before rendering the scene, which increases the complexity of the setup and requires some custom c# code. It may even require a duplicate of all of the character sprites. The high level view is you render your character's silhouette to a render texture, and then the sprites you want to have this silhouette show on uses a shader that reads from that render texture and replaces its color with it. The advantage of this technique is you can keep everything alpha blended and soft. You can also have much more control over the look of the silhouette as it's whatever you want to render to that render texture. But the big disadvantage is it's a lot more work.
     
    Takap_ likes this.
  3. ArikaHoshino

    ArikaHoshino

    Joined:
    Sep 14, 2020
    Posts:
    32
    I found an article (in Chinese) that implement this using Stencils
    zhuanlan.zhihu.com/p/424876474