# Gooey metaballs using fragment shaders

You can find a fullscreen demo at /projects/gooey/, or a smaller version if you skip to the end of this post. For the full code, see the JS code and vertex and fragment shaders.

The options panel in the top left of this page lets you change the variables in the animations.

# Libraries

twgl.js is a WebGL helper library that's great for simply loading shaders without having to deal with the verbose WebGL API. It's also super small, compared to other WebGL libraries, because it has a more focused feature set. For context, here are some other WebGL libraries:

# Vertex shader

The vertex shader processes individual vertices by mapping input vertices to output vertices. Our shader is just a rectangle, so we don't need to do any fancing vertex processing.

We use the most basic vertex shader, simply returning the input:

```
attribute vec4 position;
void main() {
gl_Position = position;
}
```

# Fragment shader

This is where the fun begins. The fragment shader takes in a pixel position and returns the color of that pixel.

## Uniforms

First, let's define some uniforms, which are global variables that we can use as configurable options. Uniforms are the bridge between JavaScript code and shader code. The JavaScript code will pass in values for the uniforms to the fragment shader.

At the top of the fragment shader:

```
precision mediump float;
uniform float time;
uniform vec2 resolution;
uniform vec2 mouse;
uniform float noise_speed;
uniform float metaball;
uniform float discard_threshold;
uniform float antialias_threshold;
uniform float noise_height;
uniform float noise_scale;
```

## Coordinates

`glFragCoord.xy`

represents the (x,y) coordinates of the fragment on the screen. We want the screen to be responsive, so we normalize `pos.x`

and `pos.y`

to the range `(0,1)`

and then restore the aspect ratio. Given a 1920x1080 resolution, `0 < pos.x < 1`

and `0 < pos.y < 1080/1920`

.

Note that `gl_FragCoord.xy`

places the origin at the lower left of the screen. However, the coordinates we get from `onmousemove`

are based on an *upper* left origin. So, we convert our mouse coordinates to the fragment shader's coordinate system.

```
void main() {
float ar = resolution.x / resolution.y;
vec2 pos = gl_FragCoord.xy / resolution; // normalize coordinates
pos.y /= ar; // keep aspect ratio same
vec2 mouse = mouse / resolution;
mouse.y = (1. - mouse.y) / ar; // reflect y and maintain aspect ratio
// color a small circle around the mouse
float b = step(distance(mouse, pos) * 10., 0.5);
gl_FragColor = vec4(pos.x, pos.y, b, 1.);
}
```

The last two lines visualize `pos.x`

and `pos.y`

as a red-green gradient and color a blue circle around the mouse. The fragment shader so far:

## 3D Perlin noise

The noise function (`snoise`

) is from stegu/webgl-noise, an implementation of 3D simplex noise.

Why do we need **three** dimensional noise when our screen is only two dimensional? The extra dimension is time, because we want a smooth animation. We pass in the position (scaled by `noise_scale`

) and the time (scaled by `noise_speed`

) to the `snoise`

function and get back a float in the `(-1,1)`

range. Then we transform it to be in the `(0, noise_height)`

range.

```
float noise = snoise(vec3(pos * noise_scale, time * noise_speed)); // (-1, 1)
noise = (noise + 1.) / 2.; // (-1, 1) to (0, 1)
float val = noise * noise_height; // (0, noise_height)
```

Setting the fragment color to `val`

(with `gl_FragColor = vec4(vec3(val), 1.)`

), we get:

Observe that this is **psuedo**random noise; it's much less random than JavaScript's `Math.random`

. If you increase the `noise_scale`

uniform, you can see a diagonal striation pattern.

Increasing `noise_speed`

will make the animation faster. Increasing `noise_height`

increases the heights of the "peaks," which appear as lighter areas.

## Mouse metaball

I wrote the code for the metaball mostly by trial and error, tweaking functions and coefficients until I got something that looked good.

First, we calculate the distance `d`

between the mouse and the fragment and scale it using the adjustable `metaball`

uniform.

Finally, we clamp the `mouseMetaball`

value to the `(0,1)`

range to ignore extremes.

```
float d = distance(mouse, pos); // (0=near, 1=far)
float u = d / (metaball + 0.00001); // avoid division by 0
float mouseMetaball = u * max(5., 10. - 25. * u);
mouseMetaball = clamp(1. - mouseMetaball, 0., 1.);
val += mouseMetaball;
```

You can see the unclamped `mouseMetaball`

(in green) and the clamped `mouseMetaball`

(in black) as a function of `d`

(x-axis):

(Click *edit graph on desmos* to see the effect of `metaball`

parameter *m* on the graph.)

Now, visualizing `val`

again, we see an animated white blob around the mouse:

Setting `noise_height`

to 0, you can see the metaball by itself, a spotlight centered around the mouse. Increasing the `metaball`

uniform increases the radius of the spotlight.

## Antialiasing

Now, we use `discard_threshold`

as a cutoff to determine whether or not to color a pixel. We color the pixel (i.e. set `alpha=1`

) iff `val >= threshold`

and otherwise leave it transparent (`alpha=0`

).

But the hard cutoff has a problem: the edges are jagged. We can see the individual pixels on the border because there's no antialiasing. To smooth the edges, we have to add antialiasing manually. If `val`

is between `discard_threshold`

and `discard_threshold + alias_threshold`

, we set `alpha`

to an intermediate value. This works well for small values of `alias_threshold`

, but if it's too high, the edges become too blurry.

Finding the sweet spot for `alias_threshold`

depends on screen resolution, screen size, and DPR. Compare these values of antialias thresholds:

```
// no antialiasing
float alpha = step(discard_threshold, val);
```

```
// antialiasing
float alpha = (val - discard_threshold) / alias_threshold;
alpha = clamp(alpha, 0., 1.);
```

After setting the pixel color, we finally have an animated blob!

```
vec3 blurple = vec3(63., 30., 223.) / 255.; // base color
gl_FragColor = vec4(blurple, alpha);
```

## Gradient coloring

Finally, we have to decide *what* to color the pixel. I want a smooth rainbow-like effect, so the best colorspace to use is HSB, where we can vary the hue and keep saturation and brightness constant. Our fragment shader needs to output an RGBA value, but luckily we can convert HSB to RGB:

```
// Function from Iñigo Quiles
// https://www.shadertoy.com/view/MsS3Wc
vec3 hsb2rgb(in vec3 c) {
vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0,0.0, 1.0);
rgb = rgb*rgb*(3.0-2.0*rgb);
return c.z * mix(vec3(1.0), rgb, c.y);
}
```

Let's very the hue by time and add a bit of noise to make it interesting:

```
float hue = (pos.x + pos.y) / 2. + u_time / 10. + val / 10.;
vec3 color = hsb2rgb(vec3(hue, 0.8, 0.8));
gl_FragColor = vec4(vec3(color), alpha);
```

If we ignore the discard threshold and show every pixel, we see how the gradient changes:

Putting it together, we're finished!