Search Unity

Need to improve 2D circle shader performance

Discussion in 'Shaders' started by Triumanity, Mar 3, 2017.

  1. Triumanity

    Triumanity

    Joined:
    Feb 28, 2017
    Posts:
    2
    Hi, I'm working on a mobile game. I need resizeable rings (not stroke size but radius). I've written a shader works great but shows poor performance on mobile phones.

    I set shader's float array(RadiusArray) from outside once in a while to add more circles. However whenever I add new circle, it gets slower and slower until I remove all circles from the array. Array has two information about a circle: Radius and Opacity.

    Is there any way that I don't have to use for-loop like matrix multiplication etc.? Or any tips for optimization?

    Thank you.


    Here is how it looks;

    Capture.JPG




    Code (CSharp):
    1.  
    2. Shader "Custom/TappedCircleShader" {
    3.     Properties{
    4.         _Color("Color", Color) = (1,1,0,0)
    5.         _Thickness("Thickness", Range(0.0,0.5)) = 0.05
    6.         _Radius("Radius", Range(0.0, 0.5)) = 0.4
    7.     }
    8.     SubShader{
    9.     //Tags{ "Queue" = "Transparent"  "RenderType" = "Transparent" "IgnoreProjector" = "True" }
    10.     Pass{
    11.     Blend SrcAlpha OneMinusSrcAlpha // Alpha blending
    12.  
    13.  
    14.     //Cull Off
    15.     //Lighting Off
    16.     //ZWrite Off //Make On for background
    17.     //Fog{ Mode Off }
    18.  
    19.     CGPROGRAM
    20.  
    21. #pragma vertex vert
    22. #pragma fragment frag
    23. #include "UnityCG.cginc"
    24.  
    25.  
    26.     fixed4 _Color; // low precision type is usually enough for colors
    27.     fixed _Thickness;
    28.     fixed _Radius;
    29.     fixed RadiusArray[80]; //Coming from outside
    30.  
    31.     struct appdata_t
    32.     {
    33.         float4 vertex   : POSITION;
    34.         float2 texcoord : TEXCOORD0;
    35.     };
    36.     struct fragmentInput {
    37.         fixed4 pos : POSITION;
    38.         fixed2 uv : TEXCOORD0;
    39.     };
    40.     fragmentInput vert(appdata_t v)
    41.     {
    42.         fragmentInput o;
    43.         o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    44.         o.uv = v.texcoord.xy - fixed2(0.5,0.5);
    45.         return o;
    46.     }
    47.  
    48.     // r = radius
    49.     // d = distance
    50.     // t = thickness
    51.    
    52.     fixed antialias(fixed d, fixed t) {
    53.         fixed innerRadius = 0;
    54.         fixed outerRadius = 0;
    55.         fixed totalPixelOpacity = 0;
    56.         fixed circleOpacity = 0.0;
    57.         fixed currentcircleOpacity = 0.0;
    58.  
    59.         fixed radius;
    60.         int circleCount = (int)RadiusArray[0];
    61.  
    62.         for (int ri = circleCount * 2; ri > 0; ri -= 2)
    63.         {
    64.             radius = RadiusArray[ri - 1];
    65.             if (radius < 0 || totalPixelOpacity >= 1) //Last item
    66.                 break;
    67.  
    68.             innerRadius = radius - 0.5*t;
    69.             outerRadius = radius + 0.5*t;
    70.             circleOpacity = RadiusArray[ri];
    71.  
    72.             currentcircleOpacity =
    73.                 1 - (1 - (smoothstep(innerRadius - 0.003,
    74.                     innerRadius,
    75.                     d))
    76.  
    77.                     + (smoothstep(outerRadius,
    78.                         outerRadius + 0.003,
    79.                         d)));
    80.  
    81.             currentcircleOpacity *= circleOpacity;
    82.             totalPixelOpacity += currentcircleOpacity;
    83.         }
    84.  
    85.         return totalPixelOpacity > 1 ? 1 : totalPixelOpacity;
    86.  
    87.     }
    88.  
    89.  
    90.     fixed4 frag(fragmentInput i) : SV_Target{
    91.  
    92.  
    93.     fixed distance = 0;
    94.         distance = length(fixed2(i.uv.x, i.uv.y));
    95.         return fixed4(_Color.r, _Color.g, _Color.b, _Color.a*antialias(distance, _Thickness));
    96.     }
    97.  
    98.  
    99.         ENDCG
    100.     }
    101.     }
    102. }
    103.  
     
  2. SubPixelPerfect

    SubPixelPerfect

    Joined:
    Oct 14, 2015
    Posts:
    224
    to draw cheap circles you can use signed distance field function
    for circle it is as simple as this:
    Code (CSharp):
    1. float circle(float2 p, float radius){  
    2.     return length(p) - radius;
    3. }
    to make a smooth outline form distance field you can use function like this
    Code (CSharp):
    1. float stroke(float d, float softness, float offset, float width){
    2.    d = abs(d-offset);
    3.    return clamp((width*.5 +softness*.5 - d)/softness, 0.0, 1.0);
    4. }
    here you'll find my visualization of how outline works
    and
    here you'll find many different shapes and boolean operations
     
    Last edited: Mar 3, 2017
  3. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    That's a tricky one. Your one circle code seems fine, but the 80 pass loop over the whole screen is probably the performance issue.

    You probably don't want a fixed loop of 80 in the shader if the amount of circles is variable. Better to make that 10 and call it 0 to 8 times. That at least gives you some option to reduce computation if the amount of circles is less.

    Since you only draw an outline I'd add a bit of actual geometry, so that many pixels can be skipped. And then just draw the circles one by one. To reduce draw calls you could combine multiple circle geometries and adjust them to the right size in the vertex shader.
     
    bgolus likes this.
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    There are some minor optimizations you can do, like use saturate(val) instead of val > 1 ? 1 : val assuming you don't need the negative value. The saturate is a 0 to 1 clamp that can be basically free where an if is not. But it won't save you too much if at all in this specific case for kind of esoteric reasons I'm not going to go into now.

    Another thing you can do is instead of using an inner and outer radius use the line center radius and line half width. The smoothstep function is fairly cheap, but not free, so we can avoid doing it twice for reach line.

    fixed circleOpacity = saturate(smoothstep(0, -0.003, abs(d - radius) - t * 0.5)));

    That can replace lines 68 through 79.

    However the main problem you're going to have on mobile is they can't do dynamicly sized for loops, so it'll always do 39 iterations of your loop. Also things like your attempt to optimize on line 65 will actually make the shader more expensive on mobile because mobile can't do dynamicly branching and everything always gets calculated.

    Example, on mobile this pseudo shader:
    if (val > 0)
    val = // Do stuff
    else
    val = // Do other stuff

    Gets turned into the equivalent of this:
    stuff = // Do stuff
    otherstuff = // Do other stuff
    val = val > 0 ? stuff : otherstuff

    Similarly loops get unrolled. So:
    for (int x = 0; x < _foo; x++)
    val += // do stuff with x

    Turns into:
    stuff = // do stuff with 0
    val = 0 < _foo ? val + stuff : val

    stuff = // do stuff with 1
    val = 1 < _foo ? val + stuff : val

    // etc for some guess of max iterations made by the compiler, not _foo!

    This is what @jvo3dc is getting at in his comment. Use a smaller array size and draw multiple times, or use keywords to switch between different max iterations.
     
    Last edited: Mar 3, 2017
  5. SubPixelPerfect

    SubPixelPerfect

    Joined:
    Oct 14, 2015
    Posts:
    224
    if rings don't move relative to each other over time
    you can remove loop by passing a barcode-like texture instad of array
    one lookup is definitely cheaper than big loop
     
    bgolus likes this.
  6. Triumanity

    Triumanity

    Joined:
    Feb 28, 2017
    Posts:
    2
    Guys, sorry I just had time to work on the project again. I'm pretty new to the idea of shaders so some of what you suggested is quite hard to understand for me for now. The 'signed distance field function' idea is really interesting, I'll dig into those links and see if I can use them later.

    Actually @Glurth suggested a pretty good idea for my case here; http://answers.unity3d.com/question...ircle-shader-performance.html#comment-1322867

    I really appreciate your efforts.