add sdf aa article

master
Andrew Cassidy 4 years ago committed by Andrew Cassidy
parent 2d416250f3
commit 31364e5009

@ -0,0 +1,141 @@
---
title: Antialiasing For SDF Textures
description: Correctly antialiasing shapes and text rendered using
signed distance fields.
image: aa-diff-small.png
tags: 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](https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf). 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](https://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm) or [sampled using multiple channels to get sharper corners](https://github.com/Chlumsky/msdfgen). 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](https://mortoray.com/2015/06/19/antialiasing-with-a-signed-distance-field/). 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" %}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.
Loading…
Cancel
Save