A simple Method for creating 2D Metaballs

Particle-driven demonstration using the implementation of 2D metaballs presented here in the Godot Engine. Click and drag to cast fire. You can download all project files by clicking the button below; they are under the MIT license.

Metaballs

While I was working on my entry Orbits for Ludum Dare 47 (which you can check out here, by the way), I eventually wanted to add some cool looking effects to the sun that is a the center of the screen. My game uses a relatively minimalistic 2D artstyle but I still wanted to achieve the illusion of a fiery, lively corona.

Metaballs are used for the sun’s corona in my Ludum Dare 47 entry, Orbits.

Thinking about this, I went to sleep and just as I was drifting away, I remembered something that I had experimented with a while earlier: metaballs. These organic looking shapes have been around for a while and are well known for their ability to create organic looking “blobs” of spheres that smoothly merge. You might know them from Portal 2, where they are used for the different gels.

Two merging metaballs. Edited image from Wikipedia.

There are many, very similar ways of defining metaballs. However, they all include constructing isosurfaces from a sum of falloff functions, one for each metaball in the scene.

Metaballs and Isosurfaces

An isosurface \(S\) of a scalar function \(\mathrm{f}: \mathbb{R}^3 \rightarrow \mathbb{R}\) is a surface that contains only points of constant value, i.e. \(\mathrm{f}(s) = \mathrm{const.}\) for all \(s \in S\).

By defining a falloff function \(\mathrm{g}_i\) for each metaball \(i\) and adding those together, we can construct a new isosurface:

\[\begin{align} \sum_{i = 0}^n \mathrm{g}_i(x, y, z) = \mathrm{const.} \end{align}\]

An intuitive way of defining the falloff function is the inverse squared distance to each metaball:

\[\begin{align} \mathrm{g}_i(x, y, z) = \frac{1}{(x_i - x)^2 + (y_i - y)^2 + (z_i - z)^2} \end{align}\]

However, there are numerous more efficient and smoother functions.

Techniques for creating metaballs in 3D include raymarching or marching cubes. In their simplest forms, they require looping through every metaball in the scene for every sample point (although much of this can be optimized).

Metaballs in 2D

Godot vs. other Engines

While this article mainly covers the Godot Engine, I see no reason why the technique presented here shouldn’t work in most other available engines like Unity or Unreal. However, since I am not as aware of their feature sets, you may be able to find more efficient ways of doing things.

While 2D metaballs are very similar to their 3D counterparts, we can circumvent complicated algorithms and use a trick to make our life much easier:

Instead of mathematically defining a falloff function, we use a texture. In figure 3, the alpha value of each pixel depends linearly on its distance to the texture centre: The further away a pixel, the lower its alpha value. (I simply used paint.NET’s radial gradient tool for this.)

Base texture of the metaballs. The black background signifies transparency.

We can render this texture to a Viewport with a black background, retrieve the viewport’s content from a ViewportTexture and then apply a fragment shader to only color in pixels above a certain brightness threshold. This will result in a single circle that grows larger as we lower the threshold and vice versa.

The reason we use transparent textures is that we can overlap textures this way (and again apply the thresholding) to create new shapes as shown in figure 4. While not perfect, these are very similar to the metaballs shown in figure 2.

Merging 2D metaballs. The original viewport texture is on the right, the shaded result on the left.

But we don’t have to stop here. We can combine more textures (and even different shapes if we wanted) to create even more complex structures. For my use, I used a CPUParticles2D node and assigned the texture as the draw texture. Of course, nothing stops you from using the GPU-based Particles2D instead (except in my case GLES2 compatibility). The result is quite mesmerizing, I think, and the imperfections from before are barely noticeable:

Using CPUParticles2D to create many metaballs.

Right now, the particles just instantly disappear after exceeding their lifetime. This does not look very convincing, especially if we want to emulate the look of soft flames. We could just change the size of the particles over time but as far as I know, Godot doesn’t support this feature. I stand corrected: You can use the scale_amount_curve property! Instead, I’m using a different trick: By lowering the alpha of the entire texture, we “pull” it below the threshold. This results in the metaball shrinking:

Changing the size of the metaball by fading the texture.

We can now assign a gradient to CPUParticles2D.color_ramp to fade each individual particle over time. The result we get are soft, organic fringes.

Fading out particles using CPUParticles2D.color_ramp.

By using a gradient texture where each pixel corresponds to a certain alpha range, instead of just coloring everything above a certain threshold in a solid color, we can get even more interesting effects. In this case, the gradient texture is just 5 pixels large and was imported with filering disabled to again create sharp thresholds (and suit the minimalistic look of my game).

Adding a gradient to the heightmap.

Figure 8 shows pretty much the final product that I also used my in jam submission (again, check it out!)

The final (Godot) shader code is just a few lines long. We can assign a gradient texture as a uniform and we only use the red channel to sample the brightness. All project files are available here under the MIT license.

shader_type canvas_item;

uniform sampler2D gradient;

void fragment() {
	float b = texture(TEXTURE, UV).r;
	COLOR = texture(gradient, vec2(b, 0.0f));
}

Further resources

There are many more in-depth resources about metaballs and possible rendering techniques out there.

I found a very good article by Myopic Rhino on gamedev.net about metaballs in 2D: Exploring Metaballs and Isosurfaces in 2D. It goes more in-depth about algorithms and applications and even experiments with different meta-shapes. While the techique I present here is a bit different, I took some inspiration from it.

If you are interested in 3D rendering techniques like raymarching and marching cubes, I can recommend Sebastian Lague’s videos on these topics: Coding Adventure: Ray Marching and Coding Adventure: Marching Cubes.