My previous article explains how I draw a rounded rectangle using a shader instead of tesselation. The biggest remaining issue is using eight calls to draw; one draw call would be much nicer. Here I show how I merge all these draw calls into one and upload a minimal amount of data to the GPU.
Linear Parameterization
The first step isn’t directly about combining calls, but does make it easier. I want to avoid uploading the vertex data to the GPU each time I draw a rectangle. The previous article shows how to calculate all of the vertices required for a rounded rectangle. Obviously, those move around depending on the radius amount of each corner and the size of the rectangle; the vertex set can’t just be static.
Looking at the calculations, I realize they are all fairly straightforward linear combinations of the input values. There are only 9 variables that control the shape of the rectangle:
- CornerRadius 0…3
- Size X,Y
- Extend X,Y
- Mn = Min(Size.X/2, Size.Y/2)
I didn’t show the Extend
variable in my last article. This is an additional size added to the rectangle which is not part of the rectangle itself. It’s used to expand the edges of the triangles to extend over the region where a stroke will be drawn.
The odd one here is probably Mn
. This is a calculation of the Size
variables and could be done on the GPU. It is however good to avoid conditionals whenever possible — though Min
is likely a cheap conditional operation. More importantly, it makes the next calculations easier.
Dot Product
Every vertex is just a simple linear combination of that input vector. Let the input vector be called Input
, it contains the list of values above in order. We can multiply, dot product, that vector by another to get the value of each vertex.
For example, consider the vertex at CornerRadius[0], Size.Y + Extend.Y
. CornerRadius[0]
can be expressed instead as Input · [1,0,0,0, 0,0, 0,0, 0]
. The second expression is Input · [0,0,0,0 0,1, 0,1, 0]
.
Expressing each vertex with an array of numbers like that would be quite aweful from a code perspective. The next guy looking at my code would hate me: it’d be unreadable. Instead, I tried to define this more procedurally. I defined each component of the input vector on its own first:
|
...
var CornerRadius3 = new float[]{0,0,0,1, 0,0, 0,0, 0 };
var SizeX = new float[]{0,0,0,0, 1,0, 0,0, 0 };
var SizeY = new float[]{0,0,0,0, 0,1, 0,0, 0 };
var ExtendX = new float[]{0,0,0,0, 0,0, 1,0, 0 };
...
|
Then, I defined a few simple vector operations on those arrays: sub
, add
and neg
. That same vertex from before, CornerRadius[0], Size.Y + Extend.Y
, can now be expressed as CornerRadius0, add(SizeY,ExtendY),
. That’s a lot easier to read then a bunch of 0’s and 1’s. Here’s a sample of several vertex definitions:
|
sub( SizeX, CornerRadius1), sub(SizeY, CornerRadius1 ),
add( SizeX, ExtendX), sub(SizeY, CornerRadius1 ),
Mn, sub(SizeY,Mn),
sub( SizeX, Mn), sub(SizeY, Mn),
|
This is one place where a language that supports custom operators is helpful. I’d be able to retain the exact syntax from before and work on custom types instead. Nevertheless, I find the above solution quite readable.
This is a one-time setup step. All these vertices are then stored in a buffer on the GPU. They are constant data. When I draw a rectangle I use them as vertex attributes, specify the Input
as uniforms, and use a dot product to get each vertex.
Distance Function
I need all the distance functions to have the same form. In the multiple draw approach, the corners had one function, and each side had its own specialized function. They must all be merged into a single form. I’m using the circle form as the basis:
|
distance_to_edge = vector.length( pixel_position, circle_center ) - radius
|
This can be generalized to refer to an arbitrary edge:
|
distance_to_edge = vector.length( pixel_position, edge_position ) - edge_offset
|
Where edge_position
is a vertex attribute that specifies where an “edge” of this triangle is. edge_offset
is a value that says how far away this “edge” is from the real shape edge. It is an inverted calculation.
For a corner triangle, the edge_position
is the same for all vertices: it is the single position that defines the center of the circle. For the sides, it is the line through the center of the rectangle that defines the edge.
For each side vertex, I provide the horizontal position along that line in the vertex attributes. The GPU will interpolate along that line for each point in the triangle.
All the distance calculations now have the same form so they can be combined into a single shader program. The actual values are of course distinct per rectangle. Again, I can express them as linear combinations of the same Input
uniforms I used below. I create a second vertex buffer for these (this is just easier to manage from code, obviously all attributes could be combined into one larger buffer).
One Program
All that together results in a single draw call with a unified shader program. And it only requires sending that small set of uniforms, Input
for each rectangle. The vertex buffers are a one-time initialization.
I’ll do one more followup article looking at a few niceties that emerge from my rectangle drawing: anti-aliasing and edge normals.