Using Noise in Pixel Shaders

Note: this was written for LÖVE 0.8.0, since 0.9.0 the semantics for shaders have changed with the addition of vertex shaders. I have updated the code snippets so they hopefully with work in 0.9.0 but the downloadable examples will be incompatible.

My name is Mark Wonnacott and I’m a Computer Science student at the University of Bristol in the UK. I’m a hobbyist game developer and I am enamoured with the ease and accessibility of LÖVE and its accompanying libraries.

Like a lot of game development amateurs, I got into programming because I wanted to make games. I’m not an artist, game designer, or audio guy, but I like to have a go at it all. Lately I’ve been playing around with visual effects in LÖVE and having a lot of fun.

In this post I’m going to give a very practical start to playing with visual effects in LÖVE, especially using noise. So many things are possible and so much work has already been done, but hopefully this post will give you a starting point for your own exploration.

Pixel Effect Primer

Pixel effects are one of the most powerful features of LÖVE, allowing you to create advanced per-pixel visual effects that exploit hardware acceleration. They’re fairly simple to pick up, and are really fun to play around with.

Setting up a LÖVE pixel effect is really easy: compile the effect when you load, optionally send data to the effect when you update, and then turn the effect on before you draw things.

Download Example 1

function love.load()
    screen = love.graphics.newShader([[
        vec4 effect(vec4 colour, Image image, vec2 local, vec2 screen)
        {
            // red and green scale with proportion of screen coordinates
            vec4 screen_colour = vec4(screen.x / 512.0,
                                      screen.y / 512.0,
                                      0.0,
                                      1.0);

            return screen_colour;
        }
    ]])

    texture = love.graphics.newShader([[
        vec4 effect(vec4 colour, Image image, vec2 local, vec2 screen)
        {
            // red and green components scale with texture coordinates
            vec4 coord_colour = vec4(local.x, local.y, 0.0, 1.0);
            // use the appropriate pixel from the texture
            vec4 image_colour = Texel(image, local);
            // mix the two colours equally
            return mix(coord_colour, image_colour, 0.5);
        }
    ]])

    image = love.graphics.newImage("love-logo.png")
end

function love.draw()
    love.graphics.setShader(screen)
    love.graphics.triangle("fill", 0, 0, 0, 512, 512, 0)
    love.graphics.triangle("fill", 256, 256, 256, 512, 512, 256)
    love.graphics.triangle("fill", 384, 384, 384, 512, 512, 384)

    love.graphics.setShader(texture)
    love.graphics.draw(image, 256, 256, 0.5, 3, 3, 44.5, 44.5)
end

Left: A pixel effect that colourizes the image based on the texture coordinates; the closer x is to 1, the redder the pixel, the closer y is to 1, the greener the pixel. Right: Three triangles with colour entirely determined by screen coordinates; the closer x is to the screen width, the redder the pixel, the closer y is to the screen height, the greener the pixel (notice that screen coordinates count from the bottom in pixel effects).

In simple terms, a LÖVE pixel effect is a function for choosing the colour and transparency of each pixel of a polygon when it is drawn (when you draw an image, you are actually drawing a rectangle where the colour and transparency of pixels is determined from the image data as well as the draw colour). Effects can base that choice on the current draw colour, the screen coordinates of the pixel, and any data sent from the LÖVE program itself; if a sprite is being drawn, then the full image data and the texture coordinates of the pixel are also available. This might seem a little difficult to understand at first, but when you start playing around with effects based on these variables, you can quickly get a feel for how they work.

Pixel effects are written in a language called GLSL, and LÖVE adds some aliases to make things a little more friendly. It’s really not very difficult to learn, the syntax is a lot like C and there are many helpful functions to help you do common things.

Introduction to Noise

Noise is unpredictable data, and is used in computer graphics to generate natural looking structures, patterns and textures. By incorporating noise into pixel effects you can create visual effects that don’t have obvious repetition or geometric pattern but also don’t depend on pre-made textures and images; you can also very easily animate the effects.

Nearly all noise used in computer graphics is gradient noise, which is very different to value noise found in random number generators. Value noise is what you see in visual static; every pixel having a completely random brightness independent of any other. Gradient noise has inherent structure that makes neighbouring pixels similar, giving a smooth noise that is much more suited to creating interesting effects.

The most popular type of noise in computer graphics is Perlin; it is also often used in terrain generation because it has the appearance of random hills. Another type of noise is Worley noise, which is also known as cellular noise because it has the appearance of being divided into cells.


Left: Uniform value noise. Middle: Perlin gradient noise. Right: Worley gradient noise (also known as cellular noise).

Using Noise in Pixel Effects

Using noise in LÖVE pixel effects couldn’t be easier; there are some GLSL implementations of various types of noise online, but better yet I have collected a lot of them together for you. Using noise is as simple as loading the GLSL into a string, prepending it to your existing effect and then calling the noise function within the effect. The noise functions take a vector of inputs; typically spatial dimensions, but you can put in anything you like, and you can get some really great animation by using time as an extra dimension.

Download Example 2

function love.load()
    -- load the noise glsl as a string
    local perlin = love.filesystem.read("perlin2d.glsl")

    -- prepend the noise function definition to the effect definition
    noise = love.graphics.newShader(perlin .. [[
        vec4 effect(vec4 colour, Image image, vec2 local, vec2 screen)
        {
            // scale the screen coordinates to scale the noise
            number noise = perlin2d(screen / 128.0);

            // the noise is between -1 and 1, so scale it between 0 and 1
            noise = noise * 0.5 + 0.5;

            return vec4(noise, noise, noise, 1.0);
        }
    ]])
end

function love.draw()
    -- draw a full screen rectangle using the effect 
    love.graphics.setShader(noise)
    love.graphics.rectangle("fill", 0, 0, 512, 512)
end

Cool Things to Try

People have been using gradient noise to create cool effects for a long time and so there is a wealth of stuff to find online on the subject. I have put a little effect browser together using Quickie to make it easy to play around with different pixel effects.

Download Effect Browser (Zoom with the mouse wheel, pan with right click, press space to move to the next effect, press enter to toggle the stencils)

Left: Using Worley noise as the saturation for a blue hued surface to create a water effect (it looks great when animated in the time dimension). Middle: Stencils can be used to draw the effects onto polygons. Right: By continually adding scaled Perlin noise together you can create a smokey effect. This fractal noise is often used for terrain generation because of the varied scales of natural-looking detail. Bottom: I combine the first two techniques in a game I'm currently working on.

Conclusion

Pixel effects aren’t too difficult to learn, and noise can be easily used to detailed and interesting effects; the real magic is finding new ways to use it. Play with effects and see what you can come up with; my effects browser makes it easy to observe the of effect changing different variables, so modify it for your needs.

If you have any problems, questions comments, or make any cool effects: please comment!

Comments

Roland_Y's picture

Awesome. Since I now have a computer that can run pixel effects, I can enjoy the examples.

About that, maybe you should add some lines to warn people that not all computers (mostly old ones) can run pixelEffects ?

MarekkPie's picture

The "old ones" extends to Intel integrated cards as well. This netbook I am typing on right now cannot use pixel effects, and I just got it about a year ago.

jjmafiae's picture

damn why are all the awsome things like canvas and pixel effects not supported :/

Ragzouken's picture

Shame; but you can still do some interesting things with stencils :)

jjmafiae's picture

i didnt mean it that way i can use the effects im talking about some of the few people playing my games that cant use the effects :| but i think i will use the pixel effect still in a game that needs something that looks like water :D

Ragzouken's picture

Ahhh. I have been playing around with doing reduced graphics modes for support older computers - you can do some static renderings of effects using ImageData's mapPixel; not ideal for anything based on textures or noise (unless you can be bothered to translate the GLSL to lua), but it's turned out to be good enough to do a low-tech version of the rainbow stuff.

CaptainMaelstrom's picture

This is extremely helpful for me. Thank you.