Search Unity

How does this silhouette outline shader work?

Discussion in 'Shaders' started by Jonathan_L, Jun 7, 2020.

  1. Jonathan_L

    Jonathan_L

    Joined:
    Jan 26, 2016
    Posts:
    43
    Hi, I was wondering if anybody can explain how the below shader works? It's by Valve for their SteamVR API made before the new render pipeline was released I think. I am trying to understand this as much as possible to migrate it to URP using shader graph. I know shadergraph doesn't support geometry shaders so I was hoping I might be able to create the new geometry on the CPU before rendering on an update call or something.

    https://github.com/ValveSoftware/st...eractionSystem/Core/Shaders/Silhouette.shader
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    The basic idea is it uses the geometry shader to extrude fins out of every triangle edge, but has them push out in a flattened, screen space vector based on the vertex normal. It then uses multiple passes with stencils to prevent the geometry shader pass from rendering where the object is.


    Honestly I really dislike this shader as it ends up being almost identical to Unity’s built in toon shader in quality, but about 10 times more expensive.
     
  3. Jonathan_L

    Jonathan_L

    Joined:
    Jan 26, 2016
    Posts:
    43
    Yeah thanks for the heads up. I figure the method would be even more expensive if I was editing the vertices of the mesh on every update call.

    I don't know how the built in Unity toon shader works, but I found the easiest for me was to blur a mask and use the edges of the blur as the outline. Made the textures and did the blurring using Render Features.

    http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/

    upload_2020-6-12_0-38-16.png
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    I recently went down this path. Tried brute force sampling and blurring to do wide outlines. Brute force wasn't as slow as I expected, especially after a lot of work. Blurring was faster, but not as much as I had thought it would be because I had to use very wide blurs to get the same outline widths as brute force. For smaller outlines brute force was still faster. (And yes, I was using bilinear sampling to halve the Gaussian samples.) But I think after around 4-5 pixels wide the blur based approach was faster.
    https://twitter.com/bgolus/status/1261104581515161600

    And then I tried jump flooding, as it should be faster for very wide outlines. But I wasn't sure how wide the outline needed to be to be faster than brute force and blur based outlines. So I decided to do some benchmarks to find out.
    https://twitter.com/bgolus/status/1263368014608494592

    Spoiler Alert: 2 pixels wide outlines were faster with jump flooding than either brute force or blur. Extremely hand optimized 1 pixel brute force outlines were faster. and that's about it.
    https://twitter.com/i/status/1264288567506812928

    While I didn't use this implementation, one of the replies to my original tweet has a great example project showing how to do outlines with the jump flood algorithm.
    https://twitter.com/JohnSelstad/status/1261459259922894849
     
    colin299 and jamespaterson like this.
  5. Jonathan_L

    Jonathan_L

    Joined:
    Jan 26, 2016
    Posts:
    43
    Wow thanks for the info. Cool to see your research here. I looked into how outlines were done and I never seen that method of implementing outlines. I'll probably look into this a bit more and take a look at that example github project to try to understand it (unfortunately I couldn't find any blogpost/video tutorials using this approach for outlines). This JF/SDF stuff seems like it can be useful for some other effects as well. Question, do you have any tips on how you did your benchmarking?
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    The main tweet thread was mainly looking at overall frame rate in the editor. It's not a good metric for real cost as the editor mainly shows CPU times (even the render time is CPU time), but the main thing I tend to worry about is how something affects overall frame rate. As long as overall frame rate isn't adversely affected it's often "good enough". Note, you want to be mindful of very inconsistent frame rates in the editor during play mode as that's a sign of the GPU struggling to keep up.

    For more accurate timings to compare against JFA when I was looking for the inflection point, I was making standalone builds and running them through Nvidia Nsight multiple times to get real GPU times. I have a stupidly fast GPU (Nvidia RTX 2080 Super), and I wasn't locking the GPU & Memory clocks, so there was still some minor fluctuation between runs, but not a huge amount.
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Yeah. I plan on eventually doing a write up of what I did. But I want it to be part of a larger article, or series of articles, on doing outlines.

    The short version is this:
    Render meshes I want to outline to a render texture. In my case an anti-aliased R8 render texture the same resolution as the current camera. Using an anti-aliased initial silhouette requires some additional secret sauce to work properly for the jump flooding, so you might want to skip that. I then do only as many jump flood passes as needed to cover the range of the outlines I want to produce. That number of jump flood passes is calculated like this:
    Code (csharp):
    1. int numJumps = Mathf.CeilToInt(Mathf.Log(OutlinePixelWidth + 0.5f, 2.0f));
    The
    + 0.5f
    to the pixel width is to handle the extra anti-aliasing I do in my implementation. Then I sample the resulting distance field, which in my implementation is already in pixel distance from closest edge, and subtract that from my wanted outline pixel width and clamp (actually smoothstep outline width +/- 0.5) and I have my outline outer edge. Inner edge in my implementation is handled by a stencil when composting back into the scene to ensure proper MSAA resolves, but it could also be done by using the sampled SDF distance and masking out values < 1.0.

    When using an anti-aliased silhouette to initialize the jump flood buffer you have to either sample it with a hard threshold, or in my case I approximate the subpixel edge position from the gradient. I might do another pass at it with a custom resolve of the multi-sampled texture, but that requires knowing the sub sample positions and I don't think that's available outside of DX12?
     
    Last edited: Jun 12, 2020