The Life of a Programmer

Quickly drawing a rounded rectangle with a GL shader

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.

rectangle_signed_circle
1
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.

1
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.

Rounded corners

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.

rectangle_corner

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.

Rectangular bits

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?

rectangle_topsegment

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.

rectangle_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

Unification

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.

Please join me on Discord to discuss, or ping me on Mastadon.

Quickly drawing a rounded rectangle with a GL shader

A Harmony of People. Code That Runs the World. And the Individual Behind the Keyboard.

Mailing List

Signup to my mailing list to get notified of each article I publish.

Recent Posts