Search Unity

Expanding an outline to procedurally generated Texture2D shapes

Discussion in 'Shaders' started by razzraziel, Aug 12, 2019.

  1. razzraziel

    razzraziel

    Joined:
    Sep 13, 2018
    Posts:
    111
    Hi all,

    I have fragment shader, Texture2Ds (R8 format) and runtime generated random shapes on them (obviously red channel).

    Here is an example:


    I want an outline style expanding on these random shapes like this (Lighter color to show).


    I did this with testing the pixel red value and with if statement on frag. If it is red then sampling neighbour pixels and expanding on them if neighbour's not red. But this is a lot of work. Approx. 8+ sampling per pixel for same texture with if statements.

    Do you know is there any easier way to do this? Thank you for reading!
     
    Last edited: Aug 12, 2019
  2. Namey5

    Namey5

    Joined:
    Jul 5, 2013
    Posts:
    149
    You're most of the way there with this method. Rather than using if statements, you can use other functions to combine the samples and extract a usable outline. To start off with, sample the neighbourhood like you are already doing. Then take the maximum of all of those samples and store it in a variable. After doing so, you can then subtract the original colour from this variable and you have a separate outline, i.e;

    Code (CSharp):
    1. //Where c is the centre sample, and c0-c7 are the neighbouring samples
    2. float outline = max (c0, max (c1, max (c2, max (c3, max (c4, max (c5, max (c6, c7)))))));
    3. outline = outline - c;
    In theory you could abuse bilinear filtering in the same way a tent filter would - instead of taking 8 samples for the neighbourhood, you could just take 4 samples at the corners of the centre pixel. It would be less accurate and you would need to saturate the output (and potentially boost the intensity of the corner samples).
     
    razzraziel likes this.
  3. razzraziel

    razzraziel

    Joined:
    Sep 13, 2018
    Posts:
    111
    Yeah i tried a few variants with more texel distances and fewer neighbours etc. But results werent satisfying enough.

    The most annoying thing is to sample 8 times within 2K texture where most of pixels are unnecessary. But there is a room for optimization within this method. These all pixels dont need to happen in same frame. Player moves on this texture and i know exactly which part is that (its divided like 16 equal squares with virtual tiles). So i guess it could be better to divide and sample 512px instead of 2K. But this time i need some cpu & gpu code for logic and i dont know if it would be faster or not. Also it probably cause some glitches on tile edges.

    I wonder if there is any other way to expand these shapes without 8 samplings.
     
  4. Namey5

    Namey5

    Joined:
    Jul 5, 2013
    Posts:
    149
    To reduce some of the overhead, you could use a higher mipmap bias when sampling for the outline. Also, separating the samples into multiple passes is a general workaround for multi-tap filters that (should) be applicable here, depending on how you organise the samples (i.e, first pass is the 4 vertical/horizontal neighbours, then send the output of that into the second pass which takes the 4 diagonal neighbours, thus making the outline thicker for a lower cost than running the filter twice). It is an interesting problem, though - I'm not sure how you would go about filtering an image like this with fewer neighbours for similar results (aside from, say, temporally accumulating the samples over multiple frames).
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    6,994
    This is the traditional "scatter as gather" problem that GPU based image processing has always encountered. From a CPU code mentality you would think to iterate over the pixels until you find the colored pixels, then write to those pixels within range that they should be "an edge". That's a scatter, and that's great for single threaded processing, but bad for multi-threaded processing, which is what GPUs need to do to be efficient. A gather is what you're doing now.

    @Namey5 has already gone over the common solutions: use fewer samples, make use of hardware filtering to get more texels per sample, use multiple passes. You can use a mix of these options together to speed things up. The other option is to use a compute shader so you can do actual "scatter", at least for groups of pixels in parallel.

    Some random thoughts though:
    The max() function is implemented on the GPU as an if. These three code examples compile to identical shader assembly:
    Code (csharp):
    1. float val = 0;
    2. if (foo > bar)
    3.     val = foo;
    4. else
    5.     val = bar;
    Code (csharp):
    1. float val = foo > bar ? foo : bar;
    Code (csharp):
    1. float val = max(foo, bar);
    Those all become a single asm lt (less than) command and swap.


    This is a good option, but a potentially better solution would be to use a Gather4() function, if you're aiming for PC / Console. Instead of using sampler2D _MyTexture; to define your texture and tex2D(uv) or tex2Dlod(float4(uv,0,0)) to sample it, use Texture2D _MyTexture; SamplerState sampler_MyTexture; and _MyTexture.GatherRed(sampler_MyTexture, uv); to get a float4 value where each component of the returned vector is the value from 4 separate pixels. Only really works when you only care about a single channel, which appears to be the case here.
    https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/sm5-object-texture2d-gatherred

    Then absolutely use that to your advantage. If you know the region of the texture the player is going to be in, you can clip the rendering to only happen in that area by adjusting the viewport area before doing your Blit.
    https://docs.unity3d.com/ScriptReference/Rendering.CommandBuffer.SetViewport.html
    https://docs.unity3d.com/ScriptReference/GL.Viewport.html

    Just be mindful that your blit doesn't know that it's being confined to a smaller part of the screen, so you'd want to scale & offset the UVs so the texture your reading from is in the same alignment as where you're rendering to. The blit() functions have scale & offset values for that.