WebGL Page Backgrounds!

I’ve always been inspired by Shadertoy, so I thought I’d use that idea to make a few interactive backgrounds for my website. The concept is simple: render two triangles and create awesome images in the fragment shader. While Shadertoy has many inputs to query and buffers to draw to, I limited myself to the image resolution, mouse position, and time since the program started. Since I had no persistence or multiple passes, I went with some simple and fun effects.

If you look at the top of the page, there are several icons that you can click on to change the background. The first background is a vertical gradient that changes colors over time. You can move the mouse up and down to affect the interpolation between two gradients. I chose this to be the landing background, so nothing moves or is too distracting if you just want to browse the website. The second background is a metaball grid where you can move a separate metaball within the grid. I chose this because metaballs seem like a good precursor to learning raymarching, which is something I’ve been looking into lately. The final background is a fractal image where you can pan around with the mouse. Fractal patterns have always interested me, and there’s ton’s of examples on Shadertoy to reference.

Implementation

I’m using the html canvas and WebGL to render each background. Currently, WebGL uses OpenGL ES 2.0, which provides some interesting limitations. The vertex shader for every effect simply passes the vertex coordinate along to the fragment shader. The rendering quad has points (-1, 1), (1, 1), (1, -1), and (-1, -1). Here’s the shader code:

// vertex pos passed in as clip pos
attribute vec4 vertexPos;
void main()
{
gl_Position = vertexPos;
}


All of the work is done in the fragment shader. There are 4 uniforms: backgroundAlpha, which is the alpha for fading the backgrounds in and out, globalTime, which is the time in seconds since the program started, resolution, which is the resolution in pixels of the canvas, and mouse, which is the pixel coordinates of the mouse.

Gradient

The gradient shader fades between two different gradients over time. The gradients are the same ones I use for the site’s .favicon.

// lerps between two gradients, using the mouse to slightly alter the fade between them
precision mediump float;

// data sent from the client
uniform float backgroundAlpha;
uniform float globalTime;
uniform vec2 resolution;
uniform vec2 mouse;

// gradient 1
const vec3 topColor1 = vec3(.408, .812, .675);
const vec3 botColor1 = vec3(.945, .671, .984);

// gradient 2
const vec3 topColor2 = vec3(.976, .659, .565);
const vec3 botColor2 = vec3(.251, .521, .749);

// how fast the gradients fade between each other
const float fadeSpeed = 0.01;
// how big of an effect the mouse has on the gradients
const float mouseFactor = 0.3;

void main()
{
float t = (sin(globalTime * fadeSpeed) + 1.) / 2. * (1. - mouseFactor);
float m = mouse.y / resolution.y * mouseFactor;
float v = gl_FragCoord.y / resolution.y;

vec3 gradientColor1 = mix(topColor1, botColor1, v);
vec3 gradientColor2 = mix(topColor2, botColor2, v);
gl_FragColor = vec4(mix(gradientColor1, gradientColor2, t + m), backgroundAlpha);
}

Metaballs

The metaball (or droplet) background is more complex. The metaball function checks the distance between the target position (metaball position) and the fragment position. Larger values will be returned the closer the fragment is to the target position. The values for each metaball in the scene are added together, then used to color the fragment. This is what causes the metaballs to blob together.

// draws a grid of metaballs with a player metaball that can be moved with the mouse
precision mediump float;

// data sent from the client
uniform float backgroundAlpha;
uniform float globalTime;
uniform vec2 resolution;
uniform vec2 mouse;

// grid vars
const int gridSize = 4;
const vec2 startPos = vec2(-0.3, 0.0);
const float widthStep = 0.55;
const float heightStep = 0.3;

// move / size vars
const float edgeCutoff = 0.5;
const float moveSpeed = 0.1;
const float playerSize = 10.;
const float baseSize = 10.;
const float maxSizeMod = 3.;
const float sizeModSpeed = 0.3;

// colors
const vec3 backgroundColor = vec3(0.72, 0.95, 0.95);
const vec3 metaballOuterColor = vec3(0.19, 0.41, 0.95);
const vec3 metaballInnerColor = vec3(0.44, 0.95, 0.84);

float metaball(vec2 tPos, float r)
{
vec2 fPos = gl_FragCoord.xy / resolution.yy;
fPos.x -= (resolution.x - resolution.y) / resolution.y / 2.0;
return pow(max(1.0 - length(fPos - tPos), 0.0), r);
}

void main()
{
// make a grid of metaballs
float v = 0.;
for (int i = 0; i < gridSize * gridSize; i++)
{
float index = float(i);
float x = mod(index, float(gridSize));
float y = floor(index / float(gridSize));
v += metaball(startPos + vec2(x * widthStep, y * heightStep) + vec2(sin(globalTime / (y + 1.)), cos(globalTime / (x + 1.))) * moveSpeed, baseSize + sin(globalTime * (x * y + 1.) * sizeModSpeed) * maxSizeMod);
}

// player metaball
v += metaball(vec2(-0.5 + mouse.x * 2. / resolution.x, 1. - (mouse.y / resolution.y)), playerSize);

// colors
vec3 finalColor = backgroundColor;
if (v > edgeCutoff)
finalColor = mix(metaballOuterColor, metaballInnerColor, v * 2. - 1.);

gl_FragColor = vec4(finalColor, backgroundAlpha);
}

Fractal

While researching fractals I learned of the Julia set, which is similar to the classic Mandelbrot set. To me, it’s more visually appealing, so I decided to use it as the basis for this effect. They are implemented in a nearly identical fashion - the only difference being that the Mandelbrot set calculates new coordinate values for every iteration, where the Julia set calculates one value that is used by every iteration.

// draws a Julia set fractal image, where the player can pan around the image
precision mediump float;

uniform float backgroundAlpha;
uniform float globalTime;
uniform vec2 resolution;
uniform vec2 mouse;

const int iterations = 16;
const float mouseStrength = 0.3;
const float moveSpeed = 0.15;

const float minBackground = 0.8;
const vec3 fractalMod = vec3(1.0, 0.75, 1.0);

void main()
{
// get uv coords
vec2 uv = ((gl_FragCoord.xy - vec2(0.5)) / resolution.yy);
uv.x -= (resolution.x - resolution.y) / resolution.y / 2.0;

// mod the uv coords based on input
vec2 z = uv;
z += vec2(-0.5 + mouse.x / resolution.x, -0.5 + (1. - mouse.y / resolution.y)) * mouseStrength;
z *= vec2(4.0, 4.0);
z -= vec2(2.0, 2.0);
z *= 0.35;

// animate the fractal seed
float timeScaled = globalTime * moveSpeed;
vec2 c = vec2(-0.7 + sin(timeScaled * 1.2) * 0.35, 0.4 + cos(timeScaled * 0.6) * 0.25);

// calculate fractal
float it = 0.0;
for (int i = 0; i < iterations; i++)
{
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
z += c;

if (dot(z, z) > 4.0)
break;

it += 1.0;
}

// color the factal
float t = clamp(it / float(iterations), minBackground, 1.0);
gl_FragColor = vec4(mix(0.0, 1.0, t) * fractalMod * vec3(t * t * t, t * t, t), backgroundAlpha);
}

Conclusion

I had fun creating these over the weekend. Of the three backgrounds, I like the fractal image the most, but I think the metaball background is the most fun. This was a good way to practice creative coding, and I think I’ll be able to use some of the ideas from this project in my future games.