Antialiasing circle shader

Hi,

i’ve created a simple circle shader to render a quad:

Shader "Unlit/Circle-Smooth"
{
    Properties
    {
        _Color("Tint", Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags
        {
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
            "PreviewType" = "Plane"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha // Traditional transparency

        Pass
        {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
            };

            fixed4 _Color; // low precision type is usually enough for colors
            float _Smooth = 0.045;
            static const float RADIUS = 0.5;

            v2f vert(appdata_t IN)
            {
                v2f OUT;
                OUT.vertex = UnityObjectToClipPos(IN.vertex);
                OUT.texcoord = IN.texcoord - fixed2(0.5, 0.5);
                OUT.color = IN.color * _Color;
                return OUT;
            }

            float calcAlpha(float distance)
            {
                float alpha = 1.0 - step(RADIUS, distance);
                return alpha;

            }

            fixed4 frag(v2f IN) : SV_Target
            {
                float distance = sqrt(pow(IN.texcoord.x, 2) + pow(IN.texcoord.y,2));
                return fixed4(IN.color.r, IN.color.g, IN.color.b, IN.color.a * calcAlpha(distance));
            }

            ENDCG
        }
    }
}

Now, I want to make it a bit smoother with “antialiasing” or just a gradient…
BUT this needs to be scale independent. So, when I scale the quad, the gradient must not scale. In other words, if I make the circle bigger, it must not become blurry. It must just have the same smooth edge than the small one. Maybe you have an idea how to do that.

I’ve just noticed that multisampling doesn’t affect this shader… I don’t know why but this could be a solution, too…

Thanks for the Help!

you can look into the screen space derivative functions ddx(.) and ddy(.) in the fragment shader. Those two functions compute the screen space derivative of the value you are putting in, i.e. how much the value changes from one pixel to its neighbouring pixel. (it’s quite magic whats going on behind the scenes, if you think about it for a second :smile:).

With the derivatives, you can now find out, how far you are away from the border, and thus anti alias ad libitum.

more info: What does ddx (hlsl) actually do? - Game Development Stack Exchange
and: http://http.developer.nvidia.com/Cg/ddx.html

PS: MSAA only multisamples the coverage of a pixel and it only affects polygon outlines, not the inside of polys,

Thank you for the explanation with the multisampling. That makes sense.

I will look at the ddx and ddy functions later.

I achieved good results with the image effect antialiasing component, too. Maybe I will use it.

Yes, FXAA should be able to do quite a good job, since there should be enough information available to reconstruct and filter the edges.

Using fwidth() (another derivative function) to do circles anti-aliased is super cheap and basically allows for pixel perfect smooth circles. Kind of surprised I couldn’t find an example anywhere (for Unity specifically at least) because this is a seriously simple bit of shader code. For your shader you just need to do something like this:

float distance = sqrt(dot(IN.texcoord, IN.texcoord)); // mathematically identical to your use of Pythagorean theorem, but GPU-ified, length(IN.texcoord) honestly also works and might even be faster
float alpha = saturate((0.5 - distance) / fwidth(distance)); // fwidth is the amount of change in between both vertical and horizontal pixels, saturate clamps to 0.0 to 1.0
return fixed4(IN.color.rgb, IN.color.a * alpha);

If you want something slightly nicer, but slightly more expensive then you can do:

float distance = length(IN.texcoord);
float pwidth = length(ddx(distance), ddy(distance)); // fwidth is abs(ddx(x)) + abs(ddy(x)) which is a cheaper approximation of length, so instead just do length
float alpha = smoothstep(0.0, 1.5, (0.5 - distance) / pwidth); // smoothstep with a 1.5 pixel falloff gets you a really smooth looking circle, where a 1 pixel linear falloff can still look minorly aliased
return fixed4(IN.color.rgb, IN.color.a * alpha);

Hopefully those work for you, I’m typing that out from memory on my phone so I can’t test them (and too lazy to write out the full shader code).

Btw, I use length twice in that second example. It’s actually faster to use float pscale = rsqrt(dot(ddxy, ddxy)); and (0.5 - distance) * pscale than length and a divide, but I can’t remember if Unity likes rsqrt functions or not.

7 Likes

Here’s a complete shader with optimized setups. Turns out it’s faster to do some manipulation of the smoothstep inputs rather than using an rsqrt.

Shader written for Unity 5.4

Shader "Unlit/Circle"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
    }
    SubShader
    {
        Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask RGB
        ZWrite Off
        Cull Off

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
          
            #include "UnityCG.cginc"

            // Quality level
            // 2 == high quality
            // 1 == medium quality
            // 0 == low quality
            #define QUALITY_LEVEL 1

            struct appdata
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
          
            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord - 0.5;
                return o;
            }

            fixed4 _Color;

            fixed4 frag (v2f i) : SV_Target
            {
                float dist = length(i.uv);

            #if QUALITY_LEVEL == 2
                // length derivative, 1.5 pixel smoothstep edge
                float pwidth = length(float2(ddx(dist), ddy(dist)));
                float alpha = smoothstep(0.5, 0.5 - pwidth * 1.5, dist);
            #elif QUALITY_LEVEL == 1
                // fwidth, 1.5 pixel smoothstep edge
                float pwidth = fwidth(dist);
                float alpha = smoothstep(0.5, 0.5 - pwidth * 1.5, dist);
            #else // Low
                // fwidth, 1 pixel linear edge
                float pwidth = fwidth(dist);
                float alpha = saturate((0.5 - dist) / pwidth);
            #endif

                return fixed4(_Color.rgb, _Color.a * alpha);
            }
            ENDCG
        }
    }
}

You can change the quality level in the shader by changing the QUALITY_LEVEL define.

Quality level 0 is the fastest using fwidth and a 1 pixel linear edge.
Fragment shader instruction count:

  • DX11 : 8
  • DX9 : 14

Quality level 1 is a good mix of quality and performance, uses fwidth and a 1.5 pixel smoothstep edge.
Fragment shader instruction count:

  • DX11 : 13
  • DX9 : 18

Quality level 2 is to get the best quality, uses length of ddx and ddy instead of fwidth and a 1.5 pixel smoothstep edge
Fragment shader instruction count:

  • DX11 : 14
  • DX9 : 21

The visual difference between these three is pretty tiny, but quality “0” might look slightly aliased in some cases that “1” and “2” shouldn’t. The difference between 1 and 2 isn’t very obvious if the circles are large enough on screen, but if you plan on having smaller circles on screen (like <20 pixels wide) quality levels 0 and 1 will start to look slightly diamond shaped instead of circular, something quality level 2 solves. For desktop there’s pretty much no reason to use 0, or even 1 since 2 is only one more instruction. For mobile you probably want to stick to 0 unless the issues are apparent.

16 Likes

Thank you very much!

It works pretty good.

But in my case, I think I will use FXAA. So, I don’t have to worry about every form I try to render. At the moment, it is only a circle but this could change and I have to solve this problem again and again… And I’m not so good in writing shaders.

Non the less, your solutions are very good and I understand them. Thank you for your effort.

Finally, I will post a screenshot of the circle in game with the FXAA solution…

1 Like

Ok, I have to change my decision… because my in game text looks very blurry with fxaa. So, I have to do the smoothing in the shader. Here is the screenshot… Using 4x msaa and the solution above, quality level 2.