Signed Distance Fields Part 1: Unsigned Distance Fields

Welcome to the first blog post on shaderfun.com. It probably won’t be all shaders, and probably not always fun, but I promise lots of interesting code and stuff. Signed distance fields are so awesome that my friends claim I bring them up every time I go to the pub. Whilst this is not true (and neither is the fact that I never buy a pint), they are one of my favourite programming tools these days, so I’ve decided to start this blog off with some signed distance field fun (SDFF).

(Not Signed) Distance Fields

Typically when we create a texture we think of it as a grid of numbers, each of which represents a ‘brightness’. 0 is black, 1 is white. Coloured textures simply extend this concept to store 3 values (or 4 if transparency is included).

Whilst typical textures are very useful for general purpose imagery, they are not particularly good at expressing geometric shapes. This is predominantly down to 2 problems:

  • If the geometry is not perfectly aligned to the pixel grid, it must be approximated, resulting in aliasing effects
  • Low resolution textures scale up very badly, forcing us to store high res textures if we want an accurate representation of our geometry

Distance fields are another way of utilising textures that aim to describe geometry (such as a circle, line or more complex combination of shapes). To achieve this, the value of each pixel represents the distance from the centre of that pixel to the nearest geometry.

In this diagram we can see the distance field for a simple line segment at the centre of a grid:

HorzLineWithDistances
Distances to a line segment at the centre of the image

The same image can be better visualised using a colour to represent the distance of a pixel from our geometry:

HorzLineDistanceField
Distances visualised as shades of red

This technique, originally popularised in games for font rendering has some real benefits. With access to this texture a shader has a lot of extra information about the geometry it contains, and we can use that extra information to achieve a variety of effects.

Bilinear Filtering

Before proceeding, another aspect of shaders and textures needs a mention – texture sampling. Thus far in the above examples we’ve been using a 16×16 texture in point sampling mode. That is, when the GPU wants to get the colour at a given coordinate in the texture, it simply reads the value of the closest pixel, resulting in a blocky effect.

These days most of the time we use textures in bilinear (or trilinear) sampling mode, in which the GPU automatically blends across pixels to get a smoother texture:

SamplingComparison
Comparison of point sampling (left) to bilinear sampling (right) of a simple 64×64 pixel circle drawn in paint

In the case of a distance field, this is equivalent to smoothly sampling the distances stored in our texture:

SDFSamplingComparison
Comparison of point sampling (left) to bilinear sampling (right) when applied to a distance field

Rendering The Geometry

Ok! Armed with a distance field, and a GPU that utilises bilinear filtering to smoothly sample the pixels of that field, lets see what we can build. Our first and simplest shader samples the distance, and outputs a ‘solid’ colour if the pixel is close enough to the edge of the geometry.

To start with, here’s the whole unity shader just to put everything in context:

Shader "Custom/SimpleDistanceField" 
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _BorderWidth("BorderWidth", Float) = 0.5
        _Background("Background", Color) = (0,0,0.25,1)
        _Border("Border", Color) = (0,1,0,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

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

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            //texture info
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _MainTex_TexelSize;

            //field controls
            float _BorderWidth;

            //colours
            float4 _Background;
            float4 _Border;
                        
            //simple vertex shader that fiddles with vertices + uvs of a quad to work nicely in the demo
            v2f vert(appdata v)
            {
                v2f o;
                v.vertex.y *= _MainTex_TexelSize.x * _MainTex_TexelSize.w; //stretch quad to maintain aspect ratio
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.uv = 1 - o.uv; //flip uvs
                return o;
            }

            //distance field fragment shader
            fixed4 frag (v2f i) : SV_Target
            {
                //sample distance field
                float4 sdf = tex2D(_MainTex, i.uv);

                //return border or background colour depending on distance from geometry
                float d = sdf.r;
                if (d < _BorderWidth)
                    return _Border;
                else
                    return _Background;
            }
            ENDCG
        }
    }
}

I won’t go into huge detail, as most of it is boiler plate unity shader code. The interesting bit is inside our ‘frag’ function (the fragment shader):

//distance field fragment shader
fixed4 frag (v2f i) : SV_Target
{
    //sample distance field
    float4 sdf = tex2D(_MainTex, i.uv);

    //return border or background colour depending on distance from geometry
    float d = sdf.r;
    if (d < _BorderWidth)
        return _Border;
    else
        return _Background;
}

Here we sample our texture as normal, but instead of outputting a colour, we interpret the red channel as a distance. If the distance is lower than the supplied _BorderWidth parameter, we output the _Border colour, otherwise we output the _Background colour.

The result of this extremely simple shader is our distance field ‘line’ in all its glory:

HorzLineSDF1Pix

Here we have rendered the line segment geometry with a radius of 0.5 pixels in the texture (known as texels). Visually this yields a solid line 1 texel wide with 2 rounded ends where the bilinear texture sampling has blended the distances stored in neighbouring pixels.

Next, we can add an extra ‘offset’ value to the shader to bias the sampled distance, allowing us to increase or decrease the distance used by our calculations:

//distance field fragment shader
fixed4 frag (v2f i) : SV_Target
{
    //sample distance field
    float4 sdf = tex2D(_MainTex, i.uv);

    //combine sdf with offset to get distance
    float d = sdf.r + _Offset;

    //return border or background colour depending on distance from geometry
    if (d < _BorderWidth)
        return _Border;
    else
        return _Background;
}

 

The resulting visual effect is that, by adjusting the offset, we can create thinner and fatter lines.

Something really worth remembering here is that the above texture is a mere 16×16 pixels in size, yet the level of detail achieved appears to be much greater. This extremely powerful feature is what makes signed distance fields so great for font rendering – with relatively small amounts of texture space we can efficiently render very highly detailed geometric shapes. The same principle allows us to achieve many other fun useful effects such as outlines, morphing and bloom at a low GPU/memory cost.

Just to finish off this first part, here’s a nice fat line without the nasty grid for you to gaze at:

HorzLineSDF4PixNoGrid

Stay tuned for the next parts of this blog, in which I’m aiming to cover:

  • Full signed distance fields
  • Different techniques (and code!) for generating signed distance fields
  • Some fun effects you can achieve with them

All the code for this series can be found at: https://github.com/chriscummings100/signeddistancefields

Signed Distance Fields Part 2: Solid geometry

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s