In the first blog of this series I wrote an introduction to distance fields – a technique for representing geometry in a texture. For each pixel of a distance field texture, we store the distance from that pixel to the edge of the geometry. Using a special shader we can then reconstruct and render that geometry very efficiently. In this episode I’ll expand distance fields to signed distance fields, which are used to represent solid shapes.
The code for this series can be found on git hub here.
From unsigned to signed fields
Previous we saw this distance field for a line segment, and how it could be used to render not just the original line segment, but also fatter/thinner versions of it:
However, what if we want to represent a solid circle with a field. Simple distances are all well and good with a line, but to reconstruct a solid shape, our GPU needs 1 extra piece of information per pixel – is it inside or outside the shape? We achieve this quite simply by using the sign of the distance. A negative value represents a point inside the geometry, and a positive one represents a point outside it:
Here you can see both the stored distances in our field, with those distances also represented as a pixel colour, where green is negative and red is positive. The things to note are:
- Just as with our earlier distance fields, points on the edge of the circle have distances closest to 0 (though as it happens, in this diagram none of the pixel lies perfectly on the edge of the circle).
- Points on the inside of the circle have a negative distance, and are shown as green
- Points on the outside of the circle have a positive distance, and a shown as red
Simple Rendering
The simplest first step to take is to try running our earlier shader against this new texture and see what happens. As a reminder, here’s the code we finished with last time:
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;
}
Running against this new field, surpringly, it ‘just works’:
With absolutely no changes to code, the old shader correctly renders our circle shape, and the ‘border width’ and ‘offset’ properties both function as they did before. It is easy to see why – our shader simply tests to see if the distance value is less than our border width. As pixels inside the circle store negative distances, these pass that test and end up being rendered as solid.
However, what if we want to take this a step further and introduce separate ‘fill’ and ‘border’ colours for our shape? As it happens, the modifications to our shader are quite simple:
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;
//if distance from border < _BorderWidth, return border.
//otherwise return _Fill if inside shape (-ve dist) or
//_Background if outside shape (+ve dist)
if (abs(d) < _BorderWidth)
return _Border;
else if (d < 0)
return _Fill;
else
return _Background;
}
Here we have made 3 key changes:
- Added a new shader variable for the fill colour (_Fill)
- The test that outputs ‘border colour’ now uses the absolute value of the distance, restricting it to pixels that are genuinely on the edge of the geometry
- If the pixel is not part of the border, but is negative, we output the ‘fill colour;
The result:
For the first time so far, we can also see a clear difference between adjusting _BorderWidth and _Offset. The offset affects the distance value used in all calculations, making the shape as a whole fatter/thinner:

However the border width only affects the border test, making the border fatter or thinner:

As you can imagine, with these 2 properties we can now create circles with any radius and border size, all using the original circular distance field from the top of this article.
More complex shapes
Up until now we’ve dealt with some very simple primitives, however signed distance fields are extremely flexible and can be used with any sort of geometry. Whilst I will go into more detail on field generation in a later post, for now, know that we can very easily merge 2 shapes together by sampling the distance for each one and picking the lowest of the 2:
Here you can see our field for 2 overlapping circles. Note that pixels on the left contain distances to the left circle’s surface, as it is closest, and visa-versa on the right. When applied to this new more complex field, our shader ‘just works’:
Or some more complex combinations (with an added rectangle!):
As I mentioned in my previous post, it is still worth remembering that the above images are around 650×650 pixels each, however are generated entirely from a simple 16×16 distance field texture. The most visible artefacts as a result of this low detail are the almost ‘polygonal’ straight lines where we might hope for curves:
However, even stepping up to a relatively low resolution 64×64 texture, these artefacts rapidly become hard to spot:
Well, that’s a good intro to the basics of signed distance fields and how they work. In the next post, I’ll be covering a stumbling block for many – once you’ve accepted the awesomeness of signed distance fields, how do you actually generate them?!
All the code for this series can be found at: https://github.com/chriscummings100/signeddistancefields