In this tutorial, I’ll go over an easy technique to achieve a metaball style effect in Flash/AS3. The intended audience of this text are developers with an intermediate background in Actionscript, but the general procedure is very easy to understand and applicable to any other language/API that lets you perform image filtering (such as blurring and thresholding) on Bitmaps in real time.
First things first, here’s the basic effect in action. We’ll discuss ways of making it more fancy in a bit. Place the mouse over the swf to see it in motion:
The following illustration shows how the algorithm works:
The input to the algorithm is a black and white image. This image is first blurred and then thresholded, so that all color values that are below a specified brightness are set to full black and all other values are set to white.
The blur step, which is computationally expensive in Flash, can usually be optimized away by pre-blurring all elements used to create the input image. For example, if the input image consists of a collection of circles, these circles could be replaced by instances of a pre-generated bitmap of a blurred circle, with the final result looking very similar.
This technique is not always faster: Because the blurred version of an object is larger than the object itself, at large amounts of objects, blitting the smaller unblurred objects might even out the penalty of the blur filter.
The output of the algorithm contains one or more blobs, in which objects that were distinct in the input image blend together with other nearby objects (or disappear, if they were too small and too far away from other objects). As the threshold filter leaves no gradients, the output image won’t have antialiased edges, so another blur pass might be necessary.
The distance at which objects begin to connect depends on two factors: The radius of the blur effect and the threshold level. Since larger blur radii are usually more expensive, it’s a good idea to start off at a low blur radius (such as 8 or 16 pixels) and high threshold level and tweak the effect from there, if you’re not pre-blurring your elements. Be aware that Flash’s blur filter performs best at radii of powers of two (i.e. 4, 8, 16, 32, etc.).
For this tutorial, we’ll be using two classes: the main Metaballs class which will apply the effect, and a class named SourceClip which will contain an animation that the effect can be applied to. For this SourceClip class, you can use any black and white timeline animation you create in Flash, or you can use the supplied SourceClip.as, which creates a very simple particle system. I won’t go into details on how the SourceClip class works, simply because it’s not relevant to the actual effect. I should however point out that no pre-blurring is done.
The Metaballs.as class is a self-contained MovieClip that can be added to the stage or used as the Document class in your Flash project. You can also download both classes in a .zip here. They’re MIT licensed, so feel free to copy from them as you like.
The implementation is very straightforward:
Metaballs.as uses two BitmapData objects. The first one serves as an intermediate canvas: we render the SourceClip into it, then we apply the blur effect. The second BitmapData is the one that will be displayed on the screen – we use it as a target when applying the thresholding operation.
Here’s the relevant section in the code:
// effect parameters: var blurStrength:Number = 32; var threshold:int = 192; // clear the intermediate bmp intermediateBMP.fillRect(new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), 0xffffffff); // render the source MC into the intermediate bmp and blur it intermediateBMP.draw(src); intermediateBMP.applyFilter(intermediateBMP, new Rectangle(0,0,SCREEN_WIDTH,SCREEN_HEIGHT), new Point(0,0), new BlurFilter(blurStrength, blurStrength, BitmapFilterQuality.LOW)); // clear the screen and threshold the intermediate bmp into it screen.fillRect(new Rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), 0xffffffff); screen.threshold(intermediateBMP, new Rectangle(0,0,SCREEN_WIDTH,SCREEN_HEIGHT), new Point(0,0), "<", threshold, 0xff000000, 0x000000ff, false);
Note that we’re not actually attaching the SourceClip to the stage at any point. The only thing needed on stage is a Bitmap object that is linked to the second BitmapData.
Where to go from here
In the sample implementation, we’ve only used black circles on a white background. Experiment with this setup! Try tweaking the effect’s parameters in the enter frame handler. See what happens when you set the blurStrength to 64 and the BlurFilter’s quality to MEDIUM or HIGH. Try using different shapes. Add some white particles to the top. They’ll act as negative metaballs, cutting holes into your blobs.
You can also use several threshold passes (going from higher to lower thresholds) to color your blobs, giving them a toon-shaded appearance. The following replacement for the line that applies the threshold will use three passes to add highlights to the blobs, one of them using an offset to give the impression of directional lighting:
screen.threshold(intermediateBMP, new Rectangle(0,0,SCREEN_WIDTH,SCREEN_HEIGHT), new Point(0,0), "<", 192, 0xff000000, 0x000000ff, false); screen.threshold(intermediateBMP, new Rectangle(0,0,SCREEN_WIDTH,SCREEN_HEIGHT), new Point(0,0), "<", 150, 0xff283FA3, 0x000000ff, false); screen.threshold(intermediateBMP, new Rectangle(0,0,SCREEN_WIDTH,SCREEN_HEIGHT), new Point(-3,-7), "<", 70, 0xff4A65DB, 0x000000ff, false);
Here’s the effect in action:
Taking this a step further, what combinations of the output image and intermediate images can you think of to increase the complexity of the effect?
The following example shows what kind of results you can achieve if you use the intermediate gradient as a displacement map and the resulting blob image as an alpha mask for that displacement map:
The first step here is to create the gradient map and the blob image. A third image is then created, and a texture containing a distorted copy of the background is drawn into it, using the gradient map as a displacement map.
An inverted gradient map is then multiplied with the result to provide the smooth shading (the inverted gradient is offset by a few pixels to give the impression of directional lighting).
The next step is to mask the result so far with the blob image created at the start. Finally, a second blob image with thinner blobs is created, then blurred a bit and added to the result (again with an offset), in order to provide the highlights.
I’m not going to paste the code for this example here, because, frankly, it’s a pretty convoluted mess I arrived at after experimenting with the setup given earlier. ;)
You should however be able to follow the steps I outlined to arrive at something very similar.
The result has some artefacts at the screen edges, because of all the blurring, displacement mapping and offsetting going on. This could be countered by rendering everything at dimensions larger than the screen and then cropping as needed.
Also note that the result as shown here isn’t very performant (about 25fps on my 2010 MacBook Pro). There is however quite a bit of unnecessary blurring done (three separate blur passes in this example), which could be optimized out by using pre-blurred bitmaps instead of circular shapes.
Shameless PlugCheck out my upcoming 80s-cartoon-themed space opera "Ace Ferrara And The Dino Menace"!