Mixing it up a bit

October 29, 2012

With just a few shader functions like length, mod and max, I can make simple repeating shapes like squares, circles, and lines. With stretching and skewing of the coordinates, these can be moulded in all kinds of interesting ways.

But still, when I'm trying to create a more complex image, more techniques are needed.

One function that opens up a whole new world of possibilities is the GLSL blending function mix.

This takes 3 parameters - two values, which can be either scalar floats or vectors like colour values - and a blend value, which is a float between 0.0 and 1.0.

When the blend value is zero, the result is the first value, and when it is one, it is the second value. But in between, the result is a linear mix of both values.

This probably becomes clearer with an example:

f=vec4(mix( vec3(.0,.0,.5), vec3(.0,.5,1.), c.y*.5+.5),1.)
/images/9_0.jpg

In this case we are mixing solid colours depending on the height in the image- dark blue at the top, and sky blue at the bottom.

Ok, this would make a nice background for something, so how do I now put something else as foreground?

I'll try mix again with a circle and put it on top of the sky:

vec3 sky=mix( vec3(.0,.0,.5), vec3(.0,.5,1.), c.y*.5+.5);
float circle=smoothstep(.25,.3,1.-length(c.xy));
f=vec4(mix( sky, vec3(0.6), circle),1.);
/images/9_1.jpg

Notice how I assigned the circle to a float to use as the blend value between the background sky colour, and the light grey I chose for the circle. The smoothstep made sure they were blended smoothly at the edges of the circle.

I can also use mix to take things away:

vec3 sky=mix( vec3(.0,.0,.5), vec3(.0,.5,1.), c.y*.5+.5);
float circle=smoothstep(.25,.3,1.-length(c.xy));
float shadow=smoothstep(.25,.3,1.-length(c.xy+vec2(.5,0.)));
f=vec4(mix( sky, vec3(.6),mix(circle,0.,shadow)),1.);
/images/9_2.jpg

Now I made another copy of the circle, the same size, but shifted to the left by 0.5, called shadow. Then I used shadow as the blend value for another mix, selecting the background sky instead of the grey wherever the shadow is. This ends up taking a big chunk out of the original circle making it look a bit more moon-like, as I intended.

As you can see, mix allow much more complex images to be built up by different combinations of shapes and colour.

One interesting consequence of this way of image construction is that every pixel must calculate all the values it could possibly have - all the paths of the program flow - selecting the final one based on the mixing values.

This might seem rather expensive, and you might be wondering why I'm not making use of conditional statement such as if.

It is true that shaders support if, but it's worth remembering that usually shaders are not run on normal processors, but rather on GPUs which are made up from very many simple processors called shader units.

Very often these processors work quite inefficiently when they have to choose between two different program flows - pipelines can be stalled, and many cycles of processing time wasted.

By just calculating every path and selecting the final output with mix - which is not a conditional statement - the execution time is predictable and no time is wasted.

This is not to say a conditional version might not be faster in some cases on some GPUs. But it won't always automatically be the case.

Another approach for combining is the traditional approach to have more than one shader, and draw each item with a seperate OpenGL draw operation. In some cases this might deliver much better performance.

But it also limits the kind of combining operations that can be done because the only per-pixel data you can easily pass between the shader instances is an alpha value. It can be more complex to set up than a single shader, and is less easy to reuse in different ways than a single shader program.