Categories
shaders

WebGL for noobs: How to make a square circle

So as I mentioned in the last post I’ve been messing around with OpenGL shaders. To be honest I know next to nothing about shaders or OpenGL but it’s fun to take a look at these things to try and gain some intuition about how they work. Hopefully by doing so I’ll be able to build complex and interesting shaders of my own, but I’m getting ahead of myself.

So let’s start with something simple, a shader of concentric circles. It’s based upon a somewhat more fascinating shader, but there’s more than enough to talk about with just this simple example.

It turns out that the shader code to draw an animated set of concentric colourful circles requires just 2 lines. Well that’s not quite true you could write it one line if you wanted. But that’s also not quite true, there are a lot of lines of WebGL to get the two lines below to do something in the first place. But once those lines of code are done the bit that makes just the drawing is effectively one formula that can be compiled on the GPU.

void mainImage(out vec4 fragColor, in vec2 fragCoord) {   
  vec2 u = vec2(fragCoord/iResolution.xy-.5) * vec2(1.,iResolution.y/iResolution.x);
  fragColor = vec4(fract(length(u)*4. + vec3(0.,1.,2.)/3. + iTime/16.),1.);
}

You’ll notice that there aren’t any loops in the code and that’s because what we’re writing is conceptually the centre of a loop managed by WebGL that will get executed once for every pixel we want to shade.

Too Shadey

I should explain a little bit first. In OpenGL there are a few types of shader that form part of the rendering pipeline. But, for the purposes of this series of posts the OpenGL pipeline is a black box and we’re only really interested in the fragment shader that can be constructed in such a way as to supply window input co-ordinates and produce an associated colour for that co-ordinate.

Co-ordinate Systems

A WebGL fragment shader receives pixel co-ordinate as natural numbers where the origin (0,0) is at the bottom left of the canvas. Therefore the first thing many shaders do is to convert those co-ordinates into a new set where the origin is in the centre of the screen and the range of x and y is some set of values that makes the subsequent maths a bit simpler.

How they do this varies, and is not super interesting, but being able to extract the range from the domain of input values is often important for making sense of what happens next. A lot of OpenGL math involves vector and matrix math. For this example it’s not too important to understand this more deeply than a vec2 is a pair of x,y points.

vec2 u = vec2(fragCoord/iResolution.xy-.5) * vec2(1.,iResolution.y/iResolution.x);

I loved my Atari ST it was the third personal computer I owned (preceded by a ZX81 and a ZX Spectrum) and it had a whopping 320×200 resolution with a mighty 512 colours. All on the same screen at the same time! Amazing. So if our device has that resolution then we simply calculate the range of y by substituting in the display resolution.

\tfrac{0}{200}- \tfrac{1}{2} * \tfrac{200}{320} \leq y \leq \tfrac{200}{200} - \tfrac{1}{2} * \tfrac{200}{320}

When you simplify you get the new range of x,y values which reveal that the range of values and that the pixels have been corrected for the aspect ratio to keep them square.

-\tfrac{1}{2} \leq x \leq \tfrac{1}{2},  -\tfrac{5}{16}\leq y \leq \tfrac{5}{16}

Normally, it’s not magic

The final part, and where the magic actually happens, is in the following code segment. The part that provides the ‘circularity’ is the call to length this is because this tells us how far our point in the plane is from the origin (which we placed in the centre). Length is the same as the linear algebra vector operation ‘normalise’ (or norm), that in two dimensions is also simply the old high school favourite Pythagoras.

fragColor = vec4(fract(length(u)*4. + vec3(0.,1.,2.)/3. + iTime/16.),1.);

If we imagine a slice through our circles along the line y=0 then length(u) is equal to x since clearly:

length(x,0)=\sqrt{x^2+0^2}=x

By removing a dimension we can now reason about what the rest of the calculation must do. Since our length will be x then when we scale it by the constant 4 we will end up with x in the range of between -2 and 2. We then add this to the vector:

\begin{pmatrix}\tfrac{0}{3} & \tfrac{1}{3} & \tfrac{2}{3}\end{pmatrix}

It is this step that essentially gives the circles their colour change. We now have three different ranges for the different colour channels and so we will end up with the following:

\begin{matrix}-2 \leq r \leq 2 \\ -1\tfrac{2}{3} \leq g \leq 2\tfrac{1}{3} \\ -1\tfrac{1}{3} \leq b \leq 2\tfrac{2}{3}\end{matrix}

However OpenGL only shows colours in the range of zero to one (if the value is greater than one it is capped at one) and this is the final step. We take the fract in order to give us just the fractional part of the calculation so our values for each channel will range between zero and one. This gives us a type of oscillation where each colour channel will rise steadily towards full saturation but then drop sharply drop to zero afterwards. The key is that each channel will do this on a slightly different period and this is what gives us the candy type colours. In order to visualise this a bit more clearly I’ve made a plot showing how the three channels vary when y=0.

Time to wrap it up

This has been a long post, but we’re not quite done. The part of the formula that gives the circles their animation is the addition of iTime/16. Since iTime is the amount of seconds since the shader began this will have the effect of gently pushing each peak of the RGB channel toward the centre on every frame. I should also point out that it significantly changes the ranges of the colour channels (RGB) prior to the call to fract but because we call fract it makes no difference to the final output range of those channels of between 0 and 1.

This has been a pretty long post. There were a lot of basic things to explain before actually getting to the point. Subsequent posts will be a lot shorter I hope so that we can focus on the interesting stuff!