We've spent the last 3 articles putting in place an environment which will allow us to quickly iterate on IQ's Shadertoy and write the SDF functions we want in a way that's easy to debug and understand.

If you didn't follow from the start, we're going to start from this UI material blueprint :

Setup

Today the goal is going to build up a library of SDF functions that we'll be able to reuse. There's two big categories of functions we'll be implementing :

  • The shapes, those take the position input and output distances from the shape : circle, rectangles, triangles
  • Operations, those take at least 2 distances and combine them to output a new shape, union, intersection and difference are good examples

To encapsulate all that we're going to use Material Functions, those seems to be the right way to build reusable building blocks in Unreal material system. We actually already used a bunch ! A lot of nodes are actually material functions built from simpler nodes ! If you go back to the SDF material you can double click on VectorLength for example and see what's inside.

Peek Inside VectorLength

We can see that VectorLength is actually just using a distance node to computes the distance between our input and the origin.

Alright ! So let's setup an SDFFunctions folder and create a Circle function material.

Creating the material

If you look at the details panel, we'll want to set a small description, the category so it's easier to find, and the thumbnail to Plane (although as you'll see the preview isn't that useful).

Set details

Now we can copy the circle computation in our SDF graph into this new material :

Copying to function

And finally replaced the invalid reroute and the radius with FunctionInput nodes, this will automatically add pins to your function node. Don't forget to set proper name to the nodes !

You'll also need to set the input type to Vector2 and Scalar for the position and radius input respectively (see the details panel).

Set node input type

Finally rewire everything and we're good to go ! Our circle node is ready :

Circle rewired

Plop that node back into our original graph and yay ╰(°▽°)╯ ! The SDF computation looks much more readable !

SDF Compute looks all clean now

Moving Forward

Alright ! We now have a good workflow to develop our functions, we can write them in the main graph, and once we're satisfied with the result, create a material function node for them that'll allow us to reuse them whenever we want.

The next step now is to through Inigo Quilez's shape list and implement the ones we'd like to use ! I won't do all of them but I'll go through the ones I find interesting, feel free to try it out ! The math isn't that crazy for most of them, but it does get tedious to make them into materials sometimes.

Rounded Box

This is a very standard shape for UI drawings, the rounded box !

https://www.shadertoy.com/view/4llXD7

There's four parts of the math here that we can split up neatly, I've commented the code so we can get some clear goals :

float sdRoundedBox( in vec2 p, in vec2 b, in vec4 r )
{
    // 1. Find radius of the closest corner from p (stored in r.x)
    r.xy = (p.x>0.0)?r.xy : r.zw;
    r.x  = (p.y>0.0)?r.x  : r.y;

    // 2. Find the distance between the point and a rect deflated by the closest corner radius (stored in q)
    vec2 q = abs(p)-b+r.x;

    // 3. Compute the actual distance between our point and the box
    vec2 d = min(max(q.x,q.y),0.0) + length(max(q,0.0));

    // 4. Inject back the circle distance and return
    return d - r.x;
}

There's no new nodes we haven't seen before so I'm just going to show the resulting graphs, starting with computing the corner radius !

Rounded Box Material, Compute corner radius

Then we get Q:

Rounded Box Material, Q

And finally, the actual box and corner distance :

Rounded Box Material, Dist

Rounded Box Material, Final

You can find the pastebin of this material here, blueprintue doesn't seem to be handling named reroutes correctly, but if you paste it back into unreal you'll the the correct names.

Let's plug our new node back into our SDF graph and appreciate the result !

Plugged RoundedBox node in SDF RoundedBox result

Looks pretty nice ! Inigo is actually animating the radius and size, let's try to reproduce that. The magic happens in this part of the shadertoy :

    vec2 si = vec2(0.9,0.6) + 0.3*cos(iTime+vec2(0,2));
    vec4 ra = 0.3 + 0.3*cos( 2.0*iTime + vec4(0,1,2,3) );
    ra = min(ra,min(si.x,si.y));

We can use a Time node to represent the iTime variable !

Compute si

Compute ra

And after plugging si and ra back into our rounded rect node :

And that's it ! We've got our first non-circle (and animated !) shape \( ̄︶ ̄*\))

Oriented Box

This one is interesting as it involves matrix transformations, we'll see if unreal has some nodes to help us with that.

float sdOrientedBox( in vec2 p, in vec2 a, in vec2 b, float th )
{
    float l = length(b-a);
    vec2  d = (b-a)/l;
    vec2  q = (p-(a+b)*0.5);
          q = mat2(d.x,-d.y,d.y,d.x)*q;
          q = abs(q)-vec2(l,th)*0.5;
    return length(max(q,0.0)) + min(max(q.x,q.y),0.0);
}

I'm going to skip the math and focus on the matrix multiply as the rest is nothing we haven't done before, you can try doing it yourself and check back at the end, I'll paste the full material as usual.

    q = mat2(d.x,-d.y,d.y,d.x)*q;

This is building a rotation matrix using [d.x, d.y] coefficients and rotates q by that matrix. I'll get to how we can build such a matrix from just an angle later, but first we have to figure out how to do the matrix-vector multiplication in the Unreal material system.

There's actually a function node that already does that in 3 dimensions :

Transform3x3 Matrix Node

So we're going to do the transform in 3 dimensions, assuming we're at Z = 0, and then we'll only take the [X, Y] components of the result. In GLSL this is equivalent to :

    vec3 tmp_q = mat3(d.x,-d.y, 0, d.y,d.x, 0, 0, 0, 1) * vec3(q, 0);
    q = tmp_q.xy;

To noodelify, we have 4 things to do :

  • Expand q to a vec3
  • Build the BasisX vector as [d.x, -d.y, 0]
  • Build the BasisY vector as [d.y, d.x, 0]
  • Component mask the result as a vec2

Let's get to it !

Using a bunch of Append and ComponentMask nodes, we get our basis and q :

Filling our Transform Node

Q is straightforward, for the basis the idea is to cut d into d.x, d.y. Compute -d.y and swizzle everything into the 2 vectors we need.

We can finally component mask the matrix output to get the X/Y components and feed that to the rest :

Outputting the result

Finishing writing all the nodes we get this blueprint, which is pretty big, so I won't cut it up in screenshot for my sanity :p. You can check the pastebin here :

The OrientedBox function has 3 parameters, the screen position as usual, the extreme points of our rect, and the width.

Using the OrientedBox node

This says "Put a box going from [-0.25, 0.75] to [0.25, 0] with a width of 0.25", and the result :

OrientedBox result

Funnily enough this node can also be used to draw lines by supplying a width of 0 !

Draw a cross

Tada ヾ(•ω•`)o, but how did I combined two lines though 🤔. I actually used a Union operation ! We'll implement those in the next part :D

Conclusion

Aight ! We've seen some basic shapes, we can now draw circles, boxes, oriented boxes and even lines ! It's a nice starting toolset to do some stuff, but before we're truly free we'll need to implement a bunch of operations. We'll see the main ones in the next part !


Published

Category

Articles

Contact