Signed Distance Fields Part 3: Bringing it together

Before proceeding with more interesting subjects like generation or fancier effects, I want to pull together the work from the previous 2 blog posts into a more flexible / useful form, consisting of:

  • 1 shader that can do all the tricks we’ve talked about so far (and is easy to add to)
  • a mono behaviour that sets up / controls it
  • a demo scene for easy visualisation

The code for this series can be found on git hub here.

SignedDistanceField.shader

The first step is the new shader (SignedDistanceField.shader). In addition to the previous parameters, we now have 2 new ones:

  • _Mode: specifies the effect to render from the previous posts – distance visualiser / border / filled / border+filled, plus a few more we’ll go over in later posts
  • _Grid: opacity of the debug pixel grid to render over the top of the texture

The new frag function is as follows

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

    //blend in grid
    if (_Grid > 0)
    {
        float2 gridness = cos(3.1415926 * i.uv * _MainTex_TexelSize.zw);
        gridness = abs(gridness);
        gridness = pow(gridness,100);
        gridness *= _Grid;
        res = lerp(res, fixed4(0, 0, 0, 1), max(gridness.x,gridness.y));
    }

    return res;
}

Here we simply:

  1. sample the field as normal
  2. call through to ‘sdffunc’ to get a colour
  3. blend in a cheeky grid effect over the top if requested

The sdffunc function takes the sdf field value and uses it to return a colour:

//takes a pixel colour from the sdf texture and returns the output colour
float4 sdffunc(float4 sdf)
{
    float4 res = _Background;

    if (_Mode == 1) //Raw
    {
        return sdf;
    }
    else if (_Mode == 2) //Distance
    {
        //render colour for distance for valid pixels
        float d = sdf.r*_DistanceVisualisationScale;
        res.r = saturate(d);
        res.g = saturate(-d);
        res.b = 0;
    }
    else if (_Mode == 3) //Gradient (ignore me for now!)
    {
        res.rg = abs(sdf.gb);
        res.b = 0;
    }
    else if (_Mode == 4) //Solid
    {
        float d = sdf.r + _Offset;
        if (d < 0)
            res = _Fill;
    }
    else if (_Mode == 5) //Border
    {
        float d = sdf.r + _Offset;
        if (abs(d) < _BorderWidth)
        {
            res = _Border;
        }
    }
    else if (_Mode == 6) //SolidWithBorder
    {
        float d = sdf.r + _Offset;
        if (abs(d) < _BorderWidth)
        {
            res = _Border;
        }
        else if (d < 0)
        {
            res = _Fill;
        }
    }

    return res;
}

This should be pretty familiar, as it is just a reorganised and slightly tweaked version of the code from the previous posts. One tweak to be aware of is in our distance visualisation we’ve added a scale value to help with debug visualisation of different distances.

SignedDistanceField.cs

The core parts of the new mono behaviour (SignedDistanceField.cs) are pretty simple too:

  • A new ‘mode’ enum that corresponds to the _Mode values in the shader
  • A set of render options that can be tweaked in the inspector
  • An OnRenderObject function that ensures the required mesh/material exist, then simply pushes all render options into the material

The main part is this

//OnRenderObject calls init, then sets up render parameters
public void OnRenderObject()
{
    //make sure we have all the bits needed for rendering
    if (!m_texture)
    {
        m_texture = Texture2D.whiteTexture;
    }
    if (!m_material)
    {
        m_material = new Material(m_sdf_shader);
        m_material.hideFlags = HideFlags.DontSave;
        GetComponent().sharedMaterial = m_material;
        GetComponent().sharedMesh = BuildQuad(Vector2.one);
    }

    //store texture filter mode
    m_texture.filterMode = m_filter;
    m_texture.wrapMode = TextureWrapMode.Clamp;

    //store material properties
    m_material.SetTexture("_MainTex", m_texture);
    m_material.SetInt("_Mode", (int)m_mode);
    m_material.SetFloat("_BorderWidth", m_border_width);
    m_material.SetFloat("_Offset", m_offset);
    m_material.SetFloat("_Grid", m_show_grid ? 0.75f : 0f);
    m_material.SetColor("_Background", m_background);
    m_material.SetColor("_Fill", m_fill);
    m_material.SetColor("_Border", m_border);
    m_material.SetFloat("_DistanceVisualisationScale", m_distance_visualisation_scale);
}

This OnRenderObject simply ensures the required texture/mesh/material exist, then sets up the texture and material based on our tweakable values.

Looking in that c# file, you might also notice a custom editor for our mono behaviour. This is simply there to allow for easy addition of buttons to the inspector to trigger some test actions (such as generate a circle).

If you’ve read the previous posts and have a minimal understanding of unity/c#/shaders none of that so far should be too tricky. If it is, feel free to ping me and I’ll try to clarify on this blog.

In the next post, I’ll move on to a subject that is a bit trickier to find information on – some different approaches to generating signed distance fields.

Signed Distance Fields Part 4: Starting Generation

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 )

Facebook photo

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

Connecting to %s