Rendering Metaballs in 2D

2D Metaballs

Metaballs are blobby objects that "glue togheter" when they came close. They were Invented by Jim Blinn for the 1980 documentary TV show Cosmos to represent atoms and molecules. Metaballs later became a popular effect in the demoscene in the early 90s. In this article we'll see how render them in 2D.

Info

In this page you'll find shaders written with Shadertoy and Desmos graphs. Read how to use the interactive content in this site.

If you're curious, there is a link in the References section to the original episode of Cosmos 1 where the metaballs were used for the first time.

2D Metaballs

Isolines of a scalar field

Figure 1 show the output of the function of two variables:

$$f(x,y) = \frac{1}{r}$$

where r is the distance from the orgin, computed as $r=\sqrt{x^2+y^2}$. The higher values correspond to brighter pixels, which become darker as the distance from $(0,0)$ increases. The output of such a function is called a scalar field.


One scalar fieldThe metaballTwo fields summedThe metaball
One scalar field centered in the origin
The border isoline of the metaball
Two fields summed
The border isoline of the metaball

Now we can define the metaball as:

The portion of the scalar field where its value is above a threshold.

Figure 2 shows in blue the pixels with the value of the threshold we choose, this is called an isoline. All the pixels inside the isolines are part of the metaball.

Translating the function slightly to the left and adding another one equal to it on its right (Figure 3), generate a scalar field that is the sum of both, and a new isoline.

Moving one function with respect to the other will result in a countinous change of the isolines, which "glue togher" when they came close, as shown in the following shader.


The falloff function

$f(x,y) = \frac{1}{r}$ is inversely proportional to the distance from the origin. For this reason we'll call it the falloff.

This particular funciton isn't the best choiche for some reasons:

  • It's $+\infty$ in the origin, so we have to clamp it.
  • There is a division that is expensive (at least on old hardware :))
  • It never became zero, so we have always sum up all the functions to compute the field, no matter how far.

The following Desmos graph shows in the falloff in one dimension, as function of the distance from the origin $r$. In red the previous one and in blue the clamped version.

Actually we will use the green one, a cubic polynomial $f(r) = 1-(3r^2-2r^3)$, where $(3r^2-2r^3)$ is also known as the smoothstep function.

Using the cubic one we can ignore functions that are farther that the falloff lenght, avoid division and any discontinuity.

The lenght of the falloff can be changed by multipling the falloff argument by a value $a>0$ to increase the size of the metaballs, or $a<0$ to decrease it. (i.e. computing $f(ar)$);


ShaderToy code

Finally, here is the code that implement the previous shader, you can also click on the shader title and experiment directly in ShaderToy to see the result in real time. Comments have been added to explain the code in detail.

 1// This function is called for each pixel
 2void mainImage( out vec4 fragColor, in vec2 fragCoord)
 3{
 4    // Coordinated of the current pixel
 5    float x = fragCoord.x;
 6    float y = fragCoord.y;
 7    
 8    // In ShaderToy the point (0,0) is the bottom left angle,
 9    // but we want (0,0) at the center of the screen so translate it there.
10    vec2 Coord = (2.*fragCoord-iResolution.xy) / iResolution.y;
11    
12    // Scale up coordinates a little bit (zoom out)
13    Coord = Coord * 5.; 
14    
15    // Define the position of the two scalar fields
16    vec2 CHARGES_1_POS = vec2(2,0);
17    vec2 CHARGES_2_POS = vec2(-2,0);
18    
19    // Compute the first field value for this pixel 
20    float f1=0.;
21    CHARGES_1_POS.x *= 3.*sin(iTime);   // Move one of the two charges at each frame
22    float r1 = length(CHARGES_1_POS - Coord);
23    if(r1>3.) f1 = 0.;                  // Ouside the falloff, do not evaluate the function
24    f1 = 1.-smoothstep(0.,3.,r1); // Using 1-3x^2 - 2x^3 as falloff, that goes from 0 to 1 when r goes from 0 to 3 (scaled a little bit)
25    
26    // Compute the second field value for this pixel 
27    float f2=0.;
28    float r2 = length(CHARGES_2_POS - Coord);
29    if(r2>3.) f2 = 0.;                       // Ouside the falloff, do not evaluate the function
30    else f2 = 1.-smoothstep(0.,3.,r2); // Using 1-3x^2 - 2x^3 as falloff, that goes from 0 to 1 when r goes from 0 to 3 (scaled a little bit) 
31    
32    // Compute the sum of all fields
33    float fTotal = f1 + f2;
34    
35    // The surface of the metaball will be defined for the values less then 0.8.
36    // We'll show the isoline fTotal = 0.3, rendering the pixel that has
37    // fTotal value "near" 0.3 in blue
38    if(abs(fTotal-0.4) < 0.03)
39    {
40        fragColor = vec4(0,0,1,1);    
41    }
42    else 
43    {
44        fragColor = vec4(fTotal);
45    }
46}

Experiment

Now that we know the base effect, it's possible to add variations.

More fields and movements:

Or colors:

You can even experiment changing the falloff function. And more ...


This concludes our exploration of 2D metaballs. In the next article, we'll see how to render 3D metaballs!


  1. Cosmos Ep. 2. https://youtu.be/SYpYB2f9kGk?t=2419. To give an idea what we are talking about, the following is an example implementations I made in ShaderToy: ↩︎

Posts in this series