Rectangles appear everywhere in user interfaces, from the backgrounds of elements to rounded border decorations. It’s helpful to be able to draw these fast. I set out to draw a rectangle with a small shader program and minimal dynamic data.
Replacing tesselation with distance fields
My primary goal was to replace the tesselated rectangles we were currently drawing. Each rectangle had it’s border calculated, or rather approximated, using line and curve segments. This was then handed off to a triangle tesselator. The resulting drawing was fast, but the generation of the mesh, for the fill and the strokes, took longer than desired.
A colleague gave me the idea to work with distance fields. Rather than try to tesselate the rounded rectangle shape, use blockier triangles and just let the GPU calculate which points are inside or outside of the shape.
Signed distance fields are a common way to do font rendering on the GPU. There an atlas is produced with the distance information and the shader can use it to render scaled sharp letters. In my case I don’t have an atlas, I’ll be calculating the distance directly.
Let me explain with a circle. It’s simple to calculate the distance from a pixel to the center of the circle. I want the distance to the edge, so I subtract the radius from that value.
distance_to_edge = vector.length( pixel_position, circle_center ) - radius
distance_to_edge is positive when outside the circle and negative when inside. I can use this in the shader to decide whether the pixel is opaque or transparent.
pixel_opacity = distance_to_edge < 0 ? 1 : 0
I also had to draw circles as part of this feature. If you’re going to try distance fields this way it’s a decent way to start, since it’s a lot simpler than the rounded rectangle.
The corners of a rounded rectangle can be treated as quadrants of a circle. The radius of the corner tells us the size of the circle, and the position of its center.
The overlayed pink shows the area that completely covers the corner. This is a set of two triangles that I will give to the vertex shader. The pixel shader calculates the distance to the edge of the circle to make the region either opaque or transparent.
As the triangles only cover the one corner of the rectangle that is all that will be drawn. I repeat for the three other corners (each with their own radius values). Now I just need to draw the “rectangular” part of the shape.
The non-corner portions of the rectangle can be broken into a few triangles. The diagram below shows the top region divided into four triangles. It should be clear how to get the vertices for the corners and edge, but what about that center line between the yellow dots?
Let’s first define
mn = min( height, width ) / 2, half of the minimum of the width and height of the rectangle. The left point defining the center line is then
mn, mn and the right point is
width - mn, mn. This provides all the vertices needed to create those four triangles.
I need a distance function. This one is actually quite simple. The top edge of the rectangle is at
y=0 so simply taking the
y coordinate of each pixel gives me the distance from the edge. I’ll have to change this definition later, as I combine the shaders, but it works for now.
Unlike the corners this is a bit trickier to repeat for the other sides. For the bottom side all the values are simply subtracting from the height. For the sides there is no matching vertical center line. And what about a rectangle which is taller than wide, then it’s the vertical sides with this problem!
For this reason it’s better to model that center line as a center box. Each side then uses the points on that side of the box.
The coordinates of each of the corners are:
- mn, mn
- width – mn, mn
- mn, height – mn
- width – mn, height – mn
This let’s me treat each side as though it had a proper center line. On the shorter side some of the triangles will just become degenerate and not render anything. In the worst case of a square several of them will be degenerate. The GPU will just skip over such triangles.
The distance function obviously changes on each side as well. For the top it is
Y, and for the left it is
X. The other two sides are a subtraction,
height - Y and
width - X.
Strokes, and the purpose of the distance
The center portions fill the entire triangle, so why am I bothering with the distance function? This is where the fun part of the distance fields comes in: strokes.
If I am filling the rectangle I simply color in all the pixels that have a negative value (they are inside the rectangle). If I am drawing a stroke then I want to further limit which pixels are colored. The distance field isn’t just an on/off toggle, but an actual distance from the edge. To draw a stroke I simply limit the range which is opaque.
1 2 3 4 5
//Fill pixel_opacity = distance < 0 ? 1 : 0 //Stroke pixel_opacity = distance < 0 && distance > -stroke_width ? 1 : 0
Those two approaches, repeated for each corner and each side, results in a nicely rendered rectangle. The rectangle can have a distinct radius for each corner. Of course, it involves 8 drawing calls, which isn’t good. And it involves calculating all those vertices on the CPU, which is also kind of wasteful.
I will followup soon with how to pack all these bits into a single shader program with just one draw call and all calculations done of the GPU.