Procedural Graphics Tutorial: Hexagon Effects

I’ve noticed recently that a lot of the really interesting 2D effects on shadertoy.com involve repeating patterns and grids of some kind. Standard Cartesian grids, triangle grids, and hexagon grids seem to be the most common. Hexagons in particular have a very cool and futuristic look to them, resulting in hexagonal patterns appearing in many sci-fi wallpapers and designs.

Here are a few effects that I created recently using this technique (click the gifs to see the code):

hexgif1

hexgif2

We’ll be exploring two particularly elegant ways to render hexagons. The first technique is better suited for rendering a single hexagon. The second technique generates a tiled hexagonal grid. Both of these techniques are useful if you want to achieve a wide range of hex effects and both of them are used in the effects that I showed above. They’re far from being the only hex rendering techniques out there, but they’re clean, fast, and the first ones that I came across. Before diving in, I need to give credit where it’s due: I learned these mathematical techniques by studying this shader by the shadertoy user Shane (I encourage you to check out the rest of his work by the way – the guy is a shader wizard).

For both algorithms below, assume that our goal is to render a hexagon (or a grid of hexagons) where each hexagon is exactly 1 unit wide. We will be dealing with pointy-topped hexagons rather than flat-topped hexagons, and we will be dealing with hexagonal grids which are staggered by row rather than by column.

Before proceeding, it will also be useful to know the height of the hexagon, which we can easily calculate based on its width:

hexheight

Hex height is 2 / sqrt(3) because it is 2 * the length of the hypotenuse calculated above.

Method 1: Hexagonal Distance Function

To render a single hexagon, we’ll be using a signed distance function (SDF for short). An SDF is a way to describe a shape with a mathematical function. Typically, an SDF will return the distance of a given point from the boundary of the shape. Positive values indicate that the point that is outside of the shape, negative values indicate that the point is inside of the shape, and a value of zero indicates that the point is on the boundary of the shape.

If we wanted, for instance, to draw a red hexagon against a black background, we would implement a distance function for the hexagon and then assign a red color to pixels that are inside of the shape and a black color to pixels that are on the outside of the shape. Here’s the distance function for a hexagon.

float calcHexDistance(vec2 p)
{
     const float hexHalfWidth= .5;
     // 1.7320508 is sqrt(3)
     const float s = vec2(1, 1.7320508);
     p = abs(p);
     return max(dot(p, s * .5), p.x) - hexHalfWidth;
}

Here’s a shader which uses this formula to render a single hexagon. It’s just calculating the distance using the above function and then coloring the pixel based on that distance.

So how does all of this stuff work?

First, let’s just consider the top-right quadrant of the hexagon. Consider that for a hexagon of width 1, any point more than .5 units away from the center along the x-axis will lie outside of the hexagon. That’s why the p.x appears inside of the max function in the return statement. A point also is outside of the hexagon if it is more than .5 units along the diagonal line depicted below (don’t worry about the points in the other quadrants for now – that’s what the abs function call is for):

inouthex

To see how far a point is along this diagonal line, you can simply take the dot product of the point and a unit vector pointing in the same direction as the diagonal. You can find this diagonal by taking advantage of the fact that hexagons are composed of equilateral triangles joined at the center. We’ll do this with a generic hexagon whose aforementioned diagonal is of length 1, since we need it to be a unit vector for the dot product to give us the displacement:

hexdiagonal

The leftmost angle in the red circle is 60 degrees because the red vectors radiating from the center both bisect 60 degree angles. 30 + 30 = 60.

As pictured above, to find the relevant diagonal we form a 30-60-90 triangle from the perpendicular bisector of the diagonal edge and then calculate the length of the sides to create a vec2 (by using half the width of the hexagon, .5, and taking advantage of the relationship between the lengths of the sides of 30-60-90 triangles which always have the same ratios with respect to each other).

The constant “s”, multiplied by .5, represents this diagonal (1.7320508 is the square root of 3). The max function call thus gives us the maximum of the displacements of the current point along the two axes that we care about (the horizontal axis and the diagonal).

You’ll notice that we’re operating on the absolute value of p rather than p. This is because, without the “p = abs(p)” line, we would get the following shape:

nonabs

We don’t want this.

To get it to be an actual hexagon, we need to apply the logic to all four quadrants. Taking the absolute value of p gets all points into quadrant 1 so that we can use the same calculation. That way we don’t have to worry about checking the horizontal displacement of the point in the negative direction, nor do we have to find the displacement along the other three diagonals.

Finally, we subtract .5 from the resulting value to complete the distance function. With this subtraction, any fragment that either extends further than .5 in any horizontal direction or extends further than .5 in the diagonal direction (in any of the four quadrants) will be considered to be outside of the ‘gon. Those that don’t extend .5 or more in those directions will be considered to be inside of the ‘gon.

The value returned from this function gives you the distance of the fragment from the hexagon boundary. You can use this to shade the hexagon (by coloring any pixels with a non-positive distance value) a certain color, as is done in the example. With a bit of tweaking,  you can also use it to draw concentric hexagons/isolines, as Shane does in his shader. I’ll leave the implementation of that as an exercise to the reader.

Method 2: Hexagonal Grid

Interestingly, this next method of rendering a grid that I’m about to discuss doesn’t rely at all on the distance field formula that I described above. This is because we can take advantage of an interesting property of hexagons: if you have a staggered grid of points, as shown below, then the set of all pixels for whom a given point is the closest point forms a hexagon!

staggered_dots

If you simply select a color for each pixel based on which of the staggered points is the closest, you automatically get a colored hex grid for free:

colored_hex

Here’s an example shader which does exactly that. Let’s talk about how it works.

At a high level, this is what we’re doing:

  1. First, we divide the space into two different Cartesian grids – one whose cell centers represent the unstaggered points described above and another whose cell centers represent the staggered points (recall that these points represent the hexagon centers). The result of this is two staggered Cartesian grids who, if you collectively look at each cell center (the center of each “grid square” in the Cartesian grid), gives us the dotted pattern shown above in the image with the red and black dots.
  2. Next, we figure out the closest hexagon center to the fragment by comparing the distance of the nearest cell center in each of the two grids (the staggered grid and the unstaggered grid).
  3. FInally, we return the unique ID (in this case, we’re just using the position) of the closest hexagon center in the .zw components of the returned vec4 and returning the distance from the closest hexagon center in the .xy components of the returned vec4. We can later use this unique ID to uniquely color each hexagon and we can use the distance to draw isolines and render smaller hexagons within each cell.

The magic happens in the calcHexInfo function, displayed below.

vec4 calcHexInfo(vec2 uv)
{
     // remember, s is vec2(1, sqrt(3))
     vec4 hexCenter = round(vec4(uv, uv - vec2(.5, 1.)) / s.xyxy);
     vec4 offset = vec4(uv - hexCenter.xy * s, uv - (hexCenter.zw + .5) * s);
     return dot(offset.xy, offset.xy) < dot(offset.zw, offset.zw) ? 
          vec4(offset.xy, hexCenter.xy) : vec4(offset.zw, hexCenter.zw);
}

Let’s go over it line-by-line.

The first line essentially splits the space into a grid of squares of size (1, sqrt(3)), by dividing the UV coordinate by s.xy, and using round to figure out which grid square is the closest. The resulting value, stored in hexCenter, is the index of the nearest grid square. But why does the grid consist of squares of size (1, sqrt(3))?

Think of the grid squares as containing the non-staggered rows of the hexagonal grid (in our hexagonal grid, every other row will be staggered). We want these squares to be sized such that the hexagons are completely packed together horizontally, but leave enough space between the hexagons to fit in the staggered row. The cell width of 1 is obvious – our hexagons (as we decided early on in the article) are of width 1.  We use a height of sqrt(3) because sqrt(3) is exactly 3/2 of the height of each hexagon that we’re rendering (recall that the height of each hexagon is 2 / sqrt(3)). This allows us to stagger the rows perfectly, since the height 3 / 2 * hexHeight gives us exactly enough space between each vertically aligned hexagon to squeeze in a staggered hexagon. See the diagram below to see why this is the case:

Hex (1)

.1/2 * h needs to equal n in order to perfectly fit the staggered hex row. We know that n is 1 / sqrt(3) – see the first diagram in this article to see why (simply double the .5 / sqrt(3) side). We also know that h is 2 / sqrt(3). So 1/2 * h = n.

Now let’s talk about the code. Notice that we pass a vec4 into the round function. Think of this vec4 as two vec2’s concatenated together – we’re sticking them in a single vec4 in a single line just for convenience. In the xy components, we simply store the UV value of the point. The zw components store a different set of coordinates which are used for the staggered rows. You’ll see how exactly these are used in the next step, but for now it’ll suffice to realize that we’re subtracting (.5, 1) in order to make sure that the rounded result of the line contains, in the zw component, the 1 by sqrt(3) cell immediately to the lower-left of any point that lies within any of the staggered hexagons. The number (.5, 1) is somewhat arbitrary – it could’ve been any vec2 that would be guaranteed to bring all points in the staggered hexagon (labeled “B”) shown below into the region indicated by the green rectangle after subtracting it from the fragment’s position:

greenrect

For any point in a non-staggered hexagon, you’ll see that the zw value is irrelevant and will later be discarded.

The division by s.xyxy and the rounding are basically just taking each point (the original UV and the UV value offset to the lower left) and giving you the closest cell center.

In the next line, we calculate the offset of the fragment from the nearest cell center and from the staggered cell coordinate immediately to the top-right of the closest cell center to “fragment minus (.5, 1)”. This latter coordinate will always give you the correct cell center for fragments in staggered hex cells – for those that aren’t in staggered hex cells, this coordinate will be meaningless and will end up being ignored because we compare the offsets and discard the further one. We get these offsets by simply subtracting the positions of the respective hex centers from the uv coordinates. The cell center stored in hexCenter.xy is easy to calculate – simply multiply it by the cell size s (the round function gave us the index of the hex cell, not the actual position). The staggered cell center is calculated by offsetting the cell by half of the cell size s:

hexoffset

We then check which cell is closer to the point (the staggered cell or the unstaggered cell) by comparing the squared distances. Finally, we return the offset and unique ID (basically just the rounded index) of the closer hexagon.

Using these Functions

Armed with these functions, you now have everything you need to create some cool and unique hex-based effects. For instance, the sci-fi hexagon effect that I showed at the beginning of this article used the hex grid function to randomly color the hexagons a different shade of blue. The rotating lines inside of each hexagon are just hexagonal outlines, rendered using the distance function algorithm, that are clipped by rotating masks whose rotations are offset by a random number and by the current time.

Happy shader-writing! The only limit is your imagination. And your coding ability.

Advertisements

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 )

Google+ photo

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

Twitter picture

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

Facebook photo

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

Connecting to %s