This post is about creating animated GIF loops with GLSL and Visions of Chaos. It will cover the basics of the GL shading language and give some simple examples of how to start creating animated GIFs. I am nowhere near an expert when coding shaders so while hopefully being helpful to someone starting with shaders it is mainly to help myself get better at coding GLSL. There is no better way to learn something than by trying to explain it to someone else.

You have probably seen examples of animated GIFs that loop. Those short few second videos that loop seamlessly. They usually show some sort of interesting graphical display. There are endless possibilities.

__Getting Started__

To begin, start Visions of Chaos and then select Mode->OpenGL Shading Language->Shader Editor. If the menu is disabled you probably need to update your video card drivers so they support GLSL.

Note: All of the shaders in this tutorial are included with Visions of Chaos. They have the same names as under the preview GIF animations. You can load them and play with the code as you go along.

__Creating A New Shader__

Click the New button on the GLSL Shader Editor dialog and give your shader a name. I called mine “Animated GIF Loop 001” but you can use any name you like. The name you specify here is the name of the GL shader file, so pick something memorable.

You will then see a simple basic shader code created for you.

```
#version 120
uniform float time;
uniform vec2 resolution;
void main(void)
{
gl_FragColor = vec4(1.0,1.0,1.0,1.0);
}
```

The first line is a version declaration telling GLSL the code supports version 2.1 of the GLSL language. Version 2.1 is fairly old now so most graphics cards support it.

The two uniform lines are values that are passed to the shader from Visions of Chaos when it is run. The time variable is how many seconds the shader has been running for. Resolution contains the X and Y pixel dimensions of the image.

Next up is the main function. This is where every shader begins running at. The code within the main function is run for every pixel of the image simultaneously. This is why shaders can run so fast. You are not looping through each pixel one at a time. The graphics card GPU calculates multiple pixels at once.

gl_FragColor sets the pixel color. The vec4 is a four component vector of values representing the red, green, blue and alpha intensities. These values should be between 0 and 1. Alpha should always be 1 for our purposes. In the example code, setting all 4 values to 1 results in a white pixel.

When you click the OK button on the GLSL Shader Editor dialog the shader will run and you will see a (boring) plain white image.

__Changes Over Time__

The most important factor in animation is changing “things” over time. For this we use the uniform time variable. Every frame the shader is displayed Visions of Chaos passes in how much time has passed in seconds since the shader was first started. This allows you to use that variable for animation.

For a first example, let’s use the time to animate the background boring white color by changing the gl_FragColor line.

```
#version 120
uniform float time;
uniform vec2 resolution;
void main(void)
{
gl_FragColor = vec4(time,0.0,0.0,1.0);
}
```

Now when you run the shader you will see the image color fade in from black to pure red over the period of 1 second. This is because time starts at 0 and then increases as the shader runs. Once time gets past 1 GLSL clamps it to 1 so the color stays full red intensity.

If you want the fade in to take longer (say 5 seconds), change the time in gl_FragColor to time/5.0 so it takes 5 times as long to reach full intensity red.

We can also use the fract command. Fract returns the fractional part of any number. So 1.34 returns 0.34. 543.678 returns 0.678. If we use fract on the time we get a repeating 0 to 1 value. This causes the black to red to repeat every 2 seconds.

```
#version 120
uniform float time;
uniform vec2 resolution;
void main(void)
{
gl_FragColor = vec4(fract(time/2.0),0.0,0.0,1.0);
}
```

If this shader was saved as an animated gif movie it would be fairly boring. An increase in color from black to red then a sudden jump back to black to then increase once again to red. For animated gifs we want a smooth loop so when the gif restarts it is not noticeable.

__Sine Looping__

Using a Sine wave for animation makes it simpler to do nice repeating animations.

There is some sine math explanations here that helped.

The GLSL shader code now becomes

```
#version 120
uniform float time;
uniform vec2 resolution;
float animationSeconds = 2.0; // how long do we want the animation to last before looping
float piTimes2 = 3.1415926536*2.0;
void main(void)
{
// sineVal is a floating point value between 0 and 1
// starts at 0 when time = 0 then increases to 1.0 when time is half of animationSeconds and then back to 0 when time equals animationSeconds
float sineVal = sin(piTimes2*(time-0.75)/animationSeconds)/2.0+0.5;
gl_FragColor = vec4(sineVal,0.0,0.0,1.0);
}
```

The animationSeconds variable allows us to change the length the animation runs for before repeating. In this case it is 2 seconds.

If you look at the above diagram the lower part of the sine wave marked “one wave cycle” (from lowest trough to the next lowest trough) is the shape the sineVal code calculates and scales it to between 0 and 1. So the value starts at 0, curves up to 1 and then back down to 0.

When we run this new shader code we get the screen smoothly fading between black to red and then back to black every 2 seconds.

We can modify the gl_FragColor line to also fade the blue color component in and out. The blue component is specified as 1.0-sineVal which means when red is at its highest intensity blue is at its lowest.

```
#version 120
uniform float time;
uniform vec2 resolution;
float animationSeconds = 2.0; // how long do we want the animation to last before looping
float piTimes2 = 3.1415926536*2.0;
void main(void)
{
// sineVal is a floating point value between 0 and 1
// starts at 0 when time = 0 then increases to 1.0 when time is half of animationSeconds and then back to 0 when time equals animationSeconds
float sineVal = sin(piTimes2*(time-0.75)/animationSeconds)/2.0+0.5;
gl_FragColor = vec4(sineVal,0.0,1.0-sineVal,1.0);
}
```

The above code results in the following GIF animation.

A quick note here. Browsers do not seem to show animated GIFs at the correct frame rate (*or maybe they do not like 60 fps GIFs). Anyway, to see the GIFs at the correct frame rate you may need to download them and open them natively.

__Setting Individual Pixel Colors__

OK, so far we have used the time uniform value to animate the entire image changing color. Now let’s cover how to change each pixel within the image.

This is the new shader code

```
#version 120
uniform float time;
uniform vec2 resolution;
float animationSeconds = 2.0; // how long do we want the animation to last before looping
float piTimes2 = 3.1415926536*2.0;
void main(void)
{
//uv is pixel coordinates between -1 and +1 in the X and Y axiis with aspect ratio correction
vec2 uv = (2.0*gl_FragCoord.xy-resolution.xy)/resolution.y;
// sineVal is a floating point value between 0 and 1
// starts at 0 when time = 0 then increases to 1.0 when time is half of animationSeconds and then back to 0 when time equals animationSeconds
float sineVal = sin(piTimes2*(time-0.75)/animationSeconds)/2.0+0.5;
//shade pixels across the image depending on their X and Y coordinates - animated using sineVal
gl_FragColor = vec4(gl_FragCoord.x/resolution.x,sineVal,1.0-gl_FragCoord.y/resolution.y,1.0);
}
```

The uv calculation gets the current pixel X and Y coordinate scaled to between -1 and +1. So the top left corner of the image is vec2(-1.0,-1.0) and the lower right corner of the image is vec2(1.0,1.0). Note that aspect ratio is corrected here too. That means when the shader is run on a non-square image the pixels will be correctly stretched. This will be more apparent in the next circle part. The circle remains a true circle no matter what size the image is stretched to.

Changing the gl_FragColor line scales the red component on the X axis and blue component on the Y axis. Using sineVal for the green component makes the green of each pixel fade in and out. Running this shader gives you a shaded image now with interpolations of color between black, red, blue and purple tones while mixing with the fading intensities of green. Using gl_FragCoord.x/resolution.x uses the pixel coordinate over the image size in pixels. This means no matter what the image dimensions the red component will go from 0 to 1 across the image.

__Drawing Circles And Squares__

Next up, drawing primitive circle and square shapes.

```
#version 120
uniform float time;
uniform vec2 resolution;
float animationSeconds = 2.0; // how long do we want the animation to last before looping
float piTimes2 = 3.1415926536*2.0;
void main(void)
{
//uv is pixel coordinates between -1 and +1 in the X and Y axiis with aspect ratio correction
vec2 uv = (2.0*gl_FragCoord.xy-resolution.xy)/resolution.y;
// sineVal is a floating point value between 0 and 1
// starts at 0 when time = 0 then increases to 1.0 when time is half of animationSeconds and then back to 0 when time equals animationSeconds
float sineVal = sin(piTimes2*(time-0.75)/animationSeconds)/2.0+0.5;
float circleRadius = 0.5; //radius of circle - 0.5 = 25% of texture size (2.0) so circle will fill 50% of image
vec2 circleCenter = vec2(-0.8,0.0); //center circle on the image
float squareRadius = 0.5; //radius of circle - 0.5 = 25% of texture size (2.0) so circle will fill 50% of image
vec2 squareCenter = vec2(0.8,0.0); //center circle on the image
vec4 color = vec4(0.0); //init color variable to black
//test if pixel is within the circle
if (length(uv-circleCenter)<circleRadius)
{
color = vec4(1.0,1.0,1.0,1.0);
}
//test if pixel is within the square
else if ((abs(uv.x-squareCenter.x)<squareRadius)&&(abs(uv.y-squareCenter.y)<squareRadius))
{
color = vec4(1.0,1.0,1.0,1.0);
}
else {
//else pixel is the pulsating colored background
color = vec4(uv.x,sineVal,1.0-uv.y,1.0);
}
gl_FragColor = color;
}
```

The code is starting to get more lengthy now, but is still relatively simple.

The position and size for a circle and square are specified. The circle is centered to the left of the image and the square is centered to the right of the image.

With a few if else statements we can test if the current pixel being calculated is within the circle or square. Length is a built in GLSL function that returns the length of the vector passed. Basically this is the same as using sqrt(sqr(x2-x1)+sqr(y2-y1)) to find the distance between 2 points. For checking a point within a square simpler absolute value math can be used. If the pixel does not fall within either shape it is shaded using the background color.

Note that the color distortion of the lower right corners of the square and circle flaring out are a side effect of the limited 256 colors for GIF animations. Something to be aware of if you are using a wide gammut of colors like that example.

__Animating Shapes Size And Position__

Now let’s get the shapes moving and changing size.

```
#version 120
uniform float time;
uniform vec2 resolution;
float animationSeconds = 2.0; // how long do we want the animation to last before looping
float piTimes2 = 3.1415926536*2.0;
void main(void)
{
//uv is pixel coordinates between -1 and +1 in the X and Y axiis with aspect ratio correction
vec2 uv = (2.0*gl_FragCoord.xy-resolution.xy)/resolution.y;
// sineVal is a floating point value between 0 and 1
// starts at 0 when time = 0 then increases to 1.0 when time is half of animationSeconds and then back to 0 when time equals animationSeconds
float sineVal = sin(piTimes2*(time-0.75)/animationSeconds)/2.0+0.5;
float circleRadius = 0.2+(1.0-sineVal)*0.4; //radius of circle
vec2 circleCenter = vec2(-0.8+sineVal*1.6,0.0); //center circle on the image
float squareRadius = 0.2+sineVal*0.2; //radius of circle
vec2 squareCenter = vec2(0.8,0.0); //center circle on the image
vec4 color = vec4(0.0); //init color variable to black
//test if pixel is within the circle
if (length(uv-circleCenter)<circleRadius)
{
color = vec4(1.0,1.0,1.0,1.0);
}
//test if pixel is within the square
else if ((abs(uv.x-squareCenter.x)<squareRadius)&&(abs(uv.y-squareCenter.y)<squareRadius))
{
color = vec4(1.0,1.0,1.0,1.0);
}
else {
//else pixel is black
color = vec4(0.0,0.0,0.0,1.0);
}
gl_FragColor = color;
}
```

For this example I have changed the background just to black.

The main change from the last shader code is in the radius and center declarations. We now use the sineVal to animate the sizes and positions.

__Blending Colors__

This next example shows how to have 3 red, green and blue circles’ colors blend as they overlap.

Calculating the positions of the circles is done using the mix function. Mix takes a from and to vector and returns a value between the two. The value is a percentage of the distance passed in. In this case we use the sineVal variable that goes from 0 to 1 and back again.

```
#version 120
uniform float time;
uniform vec2 resolution;
float animationSeconds = 2.0; // how long do we want the animation to last before looping
float piTimes2 = 3.1415926536*2.0;
void main(void)
{
//uv is pixel coordinates between -1 and +1 in the X and Y axiis with aspect ratio correction
vec2 uv = (2.0*gl_FragCoord.xy-resolution.xy)/resolution.y;
// sineVal is a floating point value between 0 and 1
// starts at 0 when time = 0 then increases to 1.0 when time is half of animationSeconds and then back to 0 when time equals animationSeconds
float sineVal = sin(piTimes2*(time-0.75)/animationSeconds)/2.0+0.5;
float circle1Radius = 0.2+(1.0-sineVal)*0.2; //radius of circle
vec2 circle1Center = mix(vec2(-0.8,0.0),vec2(0.8,0.0),sineVal);
float circle2Radius = 0.2+(sineVal)*0.2; //radius of circle
vec2 circle2Center = mix(vec2(0.0,-0.8),vec2(0.0,0.8),sineVal);
float circle3Radius = 0.2+(1.0-sineVal)*0.2; //radius of circle
vec2 circle3Center = mix(vec2(-0.8,-0.55),vec2(0.8,0.8),sineVal);
vec4 color = vec4(0.0); //init color variable to black
//default pixel color is black
color = vec4(0.0,0.0,0.0,1.0);
//test if pixel is within the circle
if (length(uv-circle1Center)<circle1Radius)
{
color += vec4(1.0,0.0,0.0,1.0);
}
if (length(uv-circle2Center)<circle2Radius)
{
color += vec4(0.0,0.0,1.0,1.0);
}
if (length(uv-circle3Center)<circle3Radius)
{
color += vec4(0.0,1.0,0.0,1.0);
}
gl_FragColor = color;
}
```

__Smoothing Out Rough Edges__

To help reduce the aliasing jagged edges you can use the smoothstep function to blend between two other values. Similar to the mix function.

For this shader the circles are changed into torus shapes.

```
#version 120
uniform float time;
uniform vec2 resolution;
float animationSeconds = 2.0; // how long do we want the animation to last before looping
float piTimes2 = 3.1415926536*2.0;
void main(void)
{
//uv is pixel coordinates between -1 and +1 in the X and Y axiis with aspect ratio correction
vec2 uv = (2.0*gl_FragCoord.xy-resolution.xy)/resolution.y;
// sineVal is a floating point value between 0 and 1
// starts at 0 when time = 0 then increases to 1.0 when time is half of animationSeconds and then back to 0 when time equals animationSeconds
float sineVal = sin(piTimes2*(time-0.75)/animationSeconds)/2.0+0.5;
float torus1Radius = 0.2+(1.0-sineVal)*0.4;
vec2 torus1Center = mix(vec2(-0.8,0.0),vec2(0.8,0.0),sineVal);
float torus2Radius = 0.2+(sineVal)*0.4;
vec2 torus2Center = mix(vec2(0.8,0.0),vec2(-0.8,0.0),sineVal);
float torus3Radius = 0.2+(1.0-sineVal)*0.2;
vec2 torus3Center = vec2(0.0,0.0);
float torusWidth = 0.1;
float torusSmoothsize = 0.03;
vec4 color = vec4(0.0); //init color variable to black
//default pixel color is black
color = vec4(0.0,0.0,0.0,1.0);
float c;
c = smoothstep(torusWidth,torusWidth-torusSmoothsize,(abs(length(uv-torus1Center)-torus1Radius)));
color += vec4(c,0.0,0.0,1.0);
c = smoothstep(torusWidth,torusWidth-torusSmoothsize,(abs(length(uv-torus2Center)-torus2Radius)));
color += vec4(0.0,c,0.0,1.0);
c = smoothstep(torusWidth,torusWidth-torusSmoothsize,(abs(length(uv-torus3Center)-torus3Radius)));
color += vec4(0.0,0.0,c,1.0);
gl_FragColor = color;
}
```

__Checkerboard__

Thanks to this post on StackOverflow we can get a simple function working to calculate if a pixel should be black or white on a checkerboard pattern.

```
#version 120
uniform float time;
uniform vec2 resolution;
float animationSeconds = 2.0; // how long do we want the animation to last before looping
float piTimes2 = 3.1415926536*2.0;
vec2 rotate(vec2 v, float a) {
float angleInRadians = radians(a);
float s = sin(angleInRadians);
float c = cos(angleInRadians);
mat2 m = mat2(c, -s, s, c);
return m * v;
}
vec3 checker(in float u, in float v, in float checksPerUnit)
{
float fmodResult = mod(floor(checksPerUnit * u) + floor(checksPerUnit * v), 2.0);
float col = max(sign(fmodResult), 0.0);
return vec3(col, col, col);
}
void main(void)
{
//uv is pixel coordinates between -1 and +1 in the X and Y axiis with aspect ratio correction
vec2 uv = (2.0*gl_FragCoord.xy-resolution.xy)/resolution.y;
// sineVal is a floating point value between 0 and 1
// starts at 0 when time = 0 then increases to 1.0 when time is half of animationSeconds and then back to 0 when time equals animationSeconds
float sineVal = sin(piTimes2*(time-0.75)/animationSeconds)/2.0+0.5;
//rotate the uv coordinates between 0 and 180 degrees during the animationSeconds time length
vec2 rotated_uv = rotate(uv,-time/animationSeconds*180);
//get the pixel checker color by passing the rotated coordinate into the checker function
vec4 color = vec4(checker(rotated_uv.x, rotated_uv.y, 5.0 * sineVal),1.0);
gl_FragColor = color;
}
```

Using the sineVal as previously gets the size of the checkers growing and shrinking for a clean loop.

For the rotation, time divided by animationSeconds is used. This ensures the checkerboard rotates continuously in the same direction for a cleaner loop.

__Multiple RGB Checkerboards__

Now let’s use 3 checkerboards in red, green and blue, overlapped.

```
#version 120
uniform float time;
uniform vec2 resolution;
float animationSeconds = 2.0; // how long do we want the animation to last before looping
float piTimes2 = 3.1415926536*2.0;
vec2 rotate(vec2 v, float a) {
float angleInRadians = radians(a);
float s = sin(angleInRadians);
float c = cos(angleInRadians);
mat2 m = mat2(c, -s, s, c);
return m * v;
}
float checker(in float u, in float v, in float checksPerUnit)
{
float fmodResult = mod(floor(checksPerUnit * u) + floor(checksPerUnit * v), 2.0);
float col = max(sign(fmodResult), 0.0);
return col;
}
void main(void)
{
//uv is pixel coordinates between -1 and +1 in the X and Y axiis with aspect ratio correction
vec2 uv = (2.0*gl_FragCoord.xy-resolution.xy)/resolution.y;
// sineVal is a floating point value between 0 and 1
// starts at 0 when time = 0 then increases to 1.0 when time is half of animationSeconds and then back to 0 when time equals animationSeconds
float sineVal = sin(piTimes2*(time-0.75)/animationSeconds)/2.0+0.5;
//rotate the uv coordinates between 0 and 180 degrees during the animationSeconds time length
vec2 rotated_uv = rotate(uv,time/animationSeconds*180);
//get the pixel checker color by passing the rotated coordinate into the checker function
vec4 color = vec4(checker(rotated_uv.x, rotated_uv.y, 5.0 * sineVal),checker(rotated_uv.x, rotated_uv.y, 3.0 * sineVal),checker(rotated_uv.x, rotated_uv.y, 4.0 * sineVal),1.0);
gl_FragColor = color;
}
```

The checker function is changed to return a single float value. 0.0 for black or 1.0 for white.

Color is now calculated by each of the RGB components being a different sized checkerboard.

__Multiple RGB Checkerboards Tweaked__

Same as previous, but now the red checkerboard does not rotate while the green and blue checkerboards rotate in opposite directions.

```
#version 120
uniform float time;
uniform vec2 resolution;
float animationSeconds = 2.0; // how long do we want the animation to last before looping
float piTimes2 = 3.1415926536*2.0;
vec2 rotate(vec2 v, float a) {
float angleInRadians = radians(a);
float s = sin(angleInRadians);
float c = cos(angleInRadians);
mat2 m = mat2(c, -s, s, c);
return m * v;
}
float checker(in float u, in float v, in float checksPerUnit)
{
float fmodResult = mod(floor(checksPerUnit * u) + floor(checksPerUnit * v), 2.0);
float col = max(sign(fmodResult), 0.0);
return col;
}
void main(void)
{
//uv is pixel coordinates between -1 and +1 in the X and Y axiis with aspect ratio correction
vec2 uv = (2.0*gl_FragCoord.xy-resolution.xy)/resolution.y;
// sineVal is a floating point value between 0 and 1
// starts at 0 when time = 0 then increases to 1.0 when time is half of animationSeconds and then back to 0 when time equals animationSeconds
float sineVal = sin(piTimes2*(time-0.75)/animationSeconds)/2.0+0.5;
//rotate the uv coordinates between 0 and 180 degrees during the animationSeconds time length
vec2 rotated_uv = rotate(uv,time/animationSeconds*180);
vec2 rotated_uv2 = rotate(uv,-time/animationSeconds*180);
//get the pixel checker color by passing the rotated coordinate into the checker function
vec4 color = vec4(checker(uv.x, uv.y, 5.0 * sineVal),checker(rotated_uv.x, rotated_uv.y, 3.0 * sineVal),checker(rotated_uv2.x, rotated_uv2.y, 4.0 * sineVal),1.0);
gl_FragColor = color;
}
```

The checker function is changed to return a single float value. 0.0 for black or 1.0 for white.

__Multiple RGB Checkerboards Within Checkerboards__

Same as previous, but now the “white” checkbaord squares are further divided into smaller checkboards.

```
#version 120
uniform float time;
uniform vec2 resolution;
float animationSeconds = 2.0; // how long do we want the animation to last before looping
float piTimes2 = 3.1415926536*2.0;
vec2 rotate(vec2 v, float a) {
float angleInRadians = radians(a);
float s = sin(angleInRadians);
float c = cos(angleInRadians);
mat2 m = mat2(c, -s, s, c);
return m * v;
}
float checker(in float u, in float v, in float checksPerUnit)
{
float fmodResult = mod(floor(checksPerUnit * u) + floor(checksPerUnit * v), 2.0);
if (fmodResult > 0.0) { fmodResult = mod(floor(checksPerUnit * u * 4) + floor(checksPerUnit * v * 4), 2.0); }
float col = max(sign(fmodResult), 0.0);
return col;
}
void main(void)
{
//uv is pixel coordinates between -1 and +1 in the X and Y axiis with aspect ratio correction
vec2 uv = (2.0*gl_FragCoord.xy-resolution.xy)/resolution.y;
// sineVal is a floating point value between 0 and 1
// starts at 0 when time = 0 then increases to 1.0 when time is half of animationSeconds and then back to 0 when time equals animationSeconds
float sineVal = sin(piTimes2*(time-0.75)/animationSeconds)/2.0+0.5;
//rotate the uv coordinates between 0 and 180 degrees during the animationSeconds time length
uv.x += sineVal*2.0-1.0;
vec2 rotated_uv = rotate(uv,time/animationSeconds*180);
vec2 rotated_uv2 = rotate(uv,-time/animationSeconds*180);
//get the pixel checker color by passing the rotated coordinate into the checker function
vec4 color = vec4(checker(uv.x, uv.y, 5.0 * sineVal),checker(rotated_uv.x, rotated_uv.y, 4.0 * sineVal),checker(rotated_uv2.x, rotated_uv2.y, 6.0 * sineVal),1.0);
gl_FragColor = color;
}
```

__How To Generate These GIFs Using Visions of Chaos__

1. Start Visions of Chaos.

2. Select Mode->OpenGL Shading Language->Shader Editor

3. You can load any of the above samples “Animated GIF Loop” or click the New button and start creating your own.

4. Once you have a loop you like, check the “Create movie frames” checkbox.

5. Click OK to start the shader running.

6. Watch the status bar and stop the shader once it is beyond the desired frames. For example if you have the loop lasting 2 seconds, then stop after 120 frames.

7. Go into the frame file directory and delete the excess frames, ie from 121 onwards.

8. Back in Visions of Chaos, the Build Movie Settings dialog will be showing.

9. Change the Movie Format dropdown to Animated GIF and then click Build to generate the GIF file.

10. Post your awesome GIF loop on Twitter etc to be the envy of your fellow nerds.

__The Future__

This post has only covered the very basics of 2D GLSL shaders for looped animation purposes. I would like to cover 3D in the future.

If this helped you get some nice animated GIF loops going let me know.

For inspiration, check out the most awesome @beesandbombs Twitter channel. Dave has loads of seamlessly looping GIF animations that show what real talent can produce.

Jason.