The Pixel Swarm

October 3, 2012

Until quite recently, I was working a lot with Qt. One of the best and most fun features in the latest version - Qt5 - is the QML ShaderEffect, which lets you easily draw and animate almost anything to the screen using just a tiny OpenGL shader program.

If you aren't familiar with shaders, they are small programs - typically only a few lines of code - which are run directly on a GPU. But, be warned... once you start learning and writing them it's hard to stop!

(When I first heard about shaders, I have to admit they sounded like scary arcane magic that I would never be able get to grips with. But it turned out that the magic was mostly just boilerplate setup code you needed to get started, and once you got past that they were actually quite simple.)

In the dim distant past of software history (say, 5 years ago) it has been common to create complex graphic scenes by making many calls for drawing primitive shapes on top of each other, giving positions, sizes and colours each time. These were the GDI or (in Qt's case) Painter APIs.

However, when you want to get the most out of that shiny GPU in your computer or phone, this is not always the best way to do things. Every time you touch a pixel, it potentially has to run another program.

If you have many items on top of each other, this means the hardware has to change state very often - something GPUs really don't like to do - and in the worst case will do a lot of work which is never even seen if the shape on top is not transparent.

An alternative to that traditional approach is to create a single program for the whole scene which will result in the desired image emerging after it is run just once, without any expensive state changes, for every pixel.

It's like this: imagine a swarm of exotic chameleon bees buzzing in front of you.

Through millions of years of genetic programming (if you believe in that sort of thing), each bee has been programmed to change colour depending only on how high up it is in the air.

Near the ground, each bee makes itself more brown, and in the air it makes itself more blue. The bees cannot communicate with each other, and yet this is what you see:

/images/1_0.jpg

Probably not so surprising when described that way.

Now lets take that idea of a swarm of independant pixels and start to program them into something interesting. We'll start with a simple GLSL shader that takes an interpolated 2D (x and y) coordinate "c" which is (-1,-1) in the top left, and smoothly changes to (+1,+1) at the bottom right. I'll leave out the boilerplate to keep things clearer (f is the same as gl_FragColor).:

f = vec4(c.x, c.y, 0.0, 1.0);

f expects 4 values - red, green, blue and alpha (transparency), each in the range 0 to 1, and collected into a "vec4". It then sets the pixel to that colour.

This program just sets the red and green components of each pixel to the coordinate. Since these colour values can only go from 0 to 1, and not -1 to +1, that makes the top left region (with all negative coordinates) all black, and the bottom right corner yellow, like this:

/images/1_1.jpg

Now lets try something more complex::

float d = length(c.xy);
f = vec4(d, d, d, 1.0);

This time we've created another variable d, and set that to the distance of every pixel from the centre using the built in "length" function. Then we set the colour to that value. The result is a circular gradient, black in the centre, and white at the edges, like this:

/images/1_2.jpg

Ok, so we've now done linear and radial gradients with just 3 lines of code. From here, it's just a few steps to go from a gradient, to a shape.:

float d = step(0.1, 1.0-length(c.xy));
f = vec4(0.0, 1.0, 0.0, d);

First we subtract the length from 1, so that it is 1 at the centre and smaller as it gets further away.

Then we add a step function. This returns either 0 or 1 depending on whether the argument is above or below the threshold (0.1 in this case).

Finally, we set the green colour value to 1, and instead change only the final "alpha" or transparency value. Together, this gives us a green circle with a transparent background:

/images/1_3.png

Remember, none of the bees, err, I mean pixels, have communicated with each other to make the circle. It just emerged by running the same program for each pixel, telling each one only where it is in the picture.

That's important for performance, because it means this program can be run in parallel on many independant processors (modern desktop GPUs have hundreds of them), without each one needing to wait to know the result of any of the other pixels.

Sadly the edges of the circle that emerged are horribly jagged, and I really hate the jaggies. But we can fix that with another small change::

float d = smoothstep(0.08,0.1,1.-length(c.xy));
f = vec4(0.0, 1.0, 0.0, d);

The smoothstep function is almost like step, but takes 2 thresholds. Below the lower it is zero, and above the upper it is one. But in between it smoothly interpolates between 0 and 1. Now the jaggies are gone:

/images/1_4.png

Mmmm, smoooth.

Well, I think that should be just enough for now to start to whet your appetite for shaders.