Fake and cheap 3D metaball

Last weekend, I released a WebGL experiment called Icicle Bubbles. If your browser doesn't render WebGL, here is a youtube video:

The demo can easily run in 60fps in most of the computers and also, it was the first demo I did has the nice rendering. So, I would like to share my little trick here.

First of all, I want to say, my version of metaball is not a real metaball and there is no geometry bending. Here is a good example of a good proper 3D metaball by Jaume Sanchez Elias (@thespite):

As you can see it in his demo, the metaball looks very beautiful and the edges of the metaball are nicely bent toward each other.

Unlike my version, there is no edge bending... at all. In my version, I only bent the normal/uv like this:

In Jaume's demo, he used a technique called marching cubes to create the 3D metaball. Using marching cubes is very expensive especially if you want to have tons of particles in the scene. As I love adding as many particles as possible in my demo, I decided to use a cheaper way to render particles without involving any CPU calculation to reconstruct the 3D blob.

So, how does my approach work? Here is a rough rendering pipeline:

And here is an interactive demo for the steps without the blur pass, you can drag and drop to rotate the metaball and see the bending in different steps:

Sorry, this snippet requires webgl feature.
Please update your browser!

Well, at this point, it doesn't look that appealing. But later on we can make up this issue by adding more particles.

So, let's go through each steps here.

Step 1 - Depth Pass

It is a normal depth pass, nothing fancy here. we just need to draw all of the particles with depth test. I used the pack1K from THREE.JS and pack the depth value into a texture:

// vertex shader

varying float vDepth;

void main() {
    vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
    vDepth = -mvPosition.z;

// fragment shader

varying float vDepth;

vec4 pack1K ( float depth ) {

   depth /= 1000.0;
   const vec4 bitSh = vec4( 256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0 );
   const vec4 bitMsk = vec4( 0.0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0 );
   vec4 res = fract( depth * bitSh );
   res -= res.xxyz * bitMsk;
   return res;

void main() {
    if(length(gl_PointCoord.xy - 0.5) > 0.5) discard;
    gl_FragColor = pack1K(vDepth);


With this depth texture, we can easily obtain the depth of the most front facing particles by using the unpack1K function later on in other passes.

Step2 - Additive Pass

In this additive pass, we will render all of the particles again and we will decide which particles should have influence to the metaball bending. Let's look at this figure:

In this figure, for the center big particle area, we only want the blue area has influence to the big particle's bending. With the depth texture we obtained in the depth pass, we can use it as the threshold and apply the influence of the blue area to the big particle.

As we assumed all particles are sphere, we can get the depth of the influent particles from the gl_PointCoord.xy by using this formula:

Once we got the z value of the particle in the current fragment, we can get the delta z value as the influent weight like this:

vec2 toCenter = (gl_PointCoord.xy - 0.5) * 2.0;
float z = sqrt(1.0 - toCenter.x * toCenter.x - toCenter.y * toCenter.y) * particleRadius;
float dz = unpack1K(texture2D( uDepthTexture, gl_FragCoord.xy / uResolution )) - vDepth + z;

Then, we can multiply the toCenter value with this dz weight value and output the color like this:

gl_FragColor = vec4(toCenter * dz, dz, 1.0 );

By using Additive blending and render it into a float point texture, we will have enough information for the render pass.

In my experiment, for the alpha channel, I used the extension EXT_blend_minmax to get the most front facing surface depth for the fog calculation. As it is not a well supported extension (No support on MS Edge by the time I did the experiment), you might want to use the depth texture with some distortion for the fog calculation.

Step 3 - Render Pass

To get the bent toCenter value from the additive pass texture is very easy. All we need to do is to divide the x and y values by it z value like this:

vec4 merged = texture2D( uAdditiveTexture, coord );
merged.xy /= merged.z;

As I didn't store any position information, there is no real light calculation involved in my metaball. In my demo, I only use a matcap (material capture) texture for the rendering. This is the matcap texture I shamelessly ripped from Jaume's demo :D

In order to create the crystal ball look and feel, I desaturated the texture and added a variable inset to render the image like this:

Here is a live demo without the fog effect:

Sorry, this snippet requires webgl feature.
Please update your browser!

Finally, we can enhance the rendering quality by adding blur effect on the additive texture, adding fog and postprocessing effects to the final output. I also added a metallic matcap texture in this final live demo:

Sorry, this snippet requires webgl feature.
Please update your browser!

Please let me know what you think about my approach ^^

dusan bosnjak
2016-02-23 14:15:54
Reply to
did you see the directx 8.0 sdk demos? There is one that does a very similar thing, but you can easily blend the normals and to lighting instead of using a texture.
2016-03-10 03:09:03
Reply to
Hey. Do you think you could add an RSS feed to your blog? :-)
2017-02-08 11:51:56
Reply to
Dude, I'm looking forward to have some sparetime to implement this ! !
Many thanks :)
What do you think?

All comments require my approval in order to be public. So... Spammers, please get the fuck out of my blog. Cunt!