You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
drewcassidy.me/_posts/2020-6-26-sdf-antialiasing.md

6.4 KiB

title description image tags
Antialiasing For SDF Textures Correctly antialiasing shapes and text rendered using signed distance fields. aa-diff-small.png KSP conformal-decals gamedev

SDF textures are commonly used for rendering text and simple graphics in both 2D and 3D applications. They allow for smooth and crisp graphics using small raster graphics as an input, and relatively simple shaders. If you're unfamiliar with them, I recommend reading the Valve whitepaper. Antialiasing when rendering SDFs seems to be a recurring problem, and I set out to find a good method that worked for any input.

As a demo, I'll be using the following SDF texture. I'm using white to represent negative distances (inside the shape) and black to represent areas outside the shape, but the inverse is also common.

In this article I'll be using the term "SDF size" to refer to the distance between black and white pixels in the SDF gradient, and "SDF units" to refer to the arbitrary units the textures are expressed in. 2D SDFs can also be generated by a function inside the shader or sampled using multiple channels to get sharper corners. As long as you have some input with a known value where the edge should occur, you can use these methods.

{% include figure-gallery.html src="sdf-1.png" caption="An SDF texture of some text." %}

No antialiasing

For reference, lets look at what the shader looks like with no antialiasing. If the distance is negative, make the alpha 1, otherwise, its 0. The simplest possible SDF/cutoff shader:

{% highlight glsl linenos %} fixed4 frag (v2f i) : SV_Target { fixed4 color = _Color;

// sdf distance from edge (scalar)
float dist = _Cutoff - tex2D(_MainTex, (i.uv)).r;

color.a = dist < 0 ? 1 : 0;

return color;

} {% endhighlight %}

This looks pretty bad:

{% include figure-gallery.html src="no-aa.png" caption="Rendered without any antialiasing. Looks terrible, especially on smaller text" %}

Adding Smoothness

The Valve whitepaper suggests using a smoothstep function to soften the edges to achieve antialiasing. This works when the image is a fixed size and distance, but when zoomed in or out the image becomes blurry or remains aliased. This method is also relative to the SDF size, so different SDF sizes will result in different smoothnesses when rendered.

{% highlight glsl linenos %} fixed4 frag (v2f i) : SV_Target { fixed4 color = _Color;

// sdf distance from edge (scalar)
float dist = _Cutoff - tex2D(_MainTex, (i.uv)).r;

color.a = smoothstep(_Smoothness, -_Smoothness, dist);

return color;

} {% endhighlight %}

{% include figure-gallery.html src="smoothness.png" caption="Rendered with a _Smoothness value of 0.1. Smaller text looks ok but big text is now fuzzy" %}

Antialiasing with a known SDF size

The solution then is to adjust the smoothness value with the size of the texture on screen. We can use the fwidth function and the texture size to determine the texels (texture pixels) per pixel on screen. Instead of using smoothstep, we directly calculate the pixel distance from the edge, as described in this blog post by Edaqa Mortoray. We also need to pass in the size of the SDF so we can convert SDF units to texels:

{% highlight glsl linenos %} fixed4 frag (v2f i) : SV_Target { fixed4 color = _Color;

// texel distance from edge (scalar)
float dist = (_Cutoff - tex2D(_MainTex, (i.uv)).r) * _SDFsize;

// uv distance per pixel density for texture on screen
float2 duv = fwidth(i.uv); 

// texel-per-pixel density for texture on screen (scalar)
// nb: in unity, z and w of TexelSize are the texture dimensions
float dtex = length(duv * _MainTex_TexelSize.zw); 

// distance to edge in pixels (scalar)
float pixelDist = dist * 2 / dtex; 

color.a = saturate(0.5 - pixelDist); 

return color;

} {% endhighlight %}

(I'm actually not sure why the 2 on line 15 is there, but it seems to be necessary)

{% include figure-gallery.html src="aa-1.png" caption="Rendered with a _SDFsize of 11, which matches the texture" %}

As long as our SDF is of a known size, this works great! The text looks good at all sizes. The problem comes when we have an unknown SDF size, such as when using user-provided textures, textures that are a mix of multiple scaled SDF textures, or when the SDF generation tool adjusts the sdf size dynamically to best utilize the available space.

To demonstrate, I'll add some triangles to our test texture with multiple SDF sizes:

{% include figure-gallery.html src="sdf-2.png" caption="Triangles with different SDF sizes added." %}

{% include figure-gallery.html src="aa-1-triangles.png" caption="The triangle on the left looks fuzzy and the one on the right looks aliased" %}

Antialiasing with an unknown SDF radius

We need to be able to determine the SDF size dynamically within the shader, so fwidth, or rather, ddx and ddy come into play again (or dFdx and dFdy in glsl, but I'm using Unity's Shaderlab system). In fact, we dont even need to use the fwidth of the uv anymore, because the gradient of the sdf is already in pixels!

{% highlight glsl linenos %} fixed4 frag (v2f i) : SV_Target { fixed4 color = _Color;

// sdf distance from edge (scalar)
float dist = (_Cutoff - tex2D(_MainTex, (i.uv)).r);

// sdf distance per pixel (gradient vector)
float2 ddist = float2(ddx(dist), ddy(dist));

// distance to edge in pixels (scalar)
float pixelDist = dist / length(ddist);

color.a = saturate(0.5 - pixelDist); 

return color;

} {% endhighlight %}

{% include figure-gallery.html src="aa-2.png" caption="Rendered using the SDF gradient method. All the triangles now look good" %}

The only differences in the result between this method and the last, besides the correct antialiasing of the triangles, is that smaller text ends up "tighter". This seems to be due to mipmapping coming into play and affecting the SDF gradient calculation, though I think the result still looks fine and perfectly readable.

{% include figure-gallery.html src="aa-2-diff.png" caption="Difference between this and the last method. Large text is almost unchanged" %}