Upcoming Events
VIEW Conference 2009
11/4 - 11/7 @ Turin, Italy

Project Horseshoe
11/5 - 11/8 @ Burnet, TX

Independent Game Conference West
11/5 - 11/6 @ Los Angeles, CA

IGDA Leadership Forum
11/12 - 11/13 @ San Francisco, CA

More events...


Quick Stats
6258 people currently visiting GDNet.
2337 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!



Link to us

Link to us

  search:   

Exploring Metaballs and Isosurfaces in 2D


Creating Meta-Things

A Simple 2D Implementation

Before stepping into more explanations and equations, here is the basic algorithm that we will be using for rendering metaballs to the screen:

  1. Iterate through every pixel on the screen:
  2. Iterate through every Metaball in the world:
  3. Calculate that Metaball's function for the current pixel, and add it to that coordinate's current value.

So, for every frame we want to render featuring Metaballs, we want to examine every pixel, and do a summation of all of the Metaballs' functions on each of these pixels. What do we mean by a 'summation', and 'the Metaballs's function'?

Each Metaball (or "Meta-Thing") is defined by a function over the X,Y plane. Like we said in the previous example, we create a circular isosurface with the following function:

F(x,y) = (x - x0)^2 + (y - y0)^2 = R^2
In order to have Metaballs influence other Metaballs that are nearby (thus creating that 'gooey' effect that we are ultimately aiming for), we need to add a little more complexity to the equation in order to achieve the effect we want.

Equation of a Metaball

As an end result, we want to eventually achieve something like this:


(Four metaballs, all influencing eachother's overall shape. Metaballs that are closer to each other provide greater attraction.)

As you can see, the circles are contributing to each other directly, and creating a unique isosurface in the end that is more complex – and much more interesting – than just our one plain circle. However, how is this creating the 'gooey' effect that we sought? It seems like the Metaballs tend to attract each other more strongly depending on how close they are to one another.

The typical equation for a Metaball is as follows:

M(x,y) = R / sqrt( (x-x0)^2 + (y-y0)^2 )
It seems to vaguely resemble our equation for a circle, but we are instead dividing the radius of the Metaball by the distance the point is from its centre. This equation is based on the equation for calculating the strength of an electrical field in science, which is why this function will provide the largest value in the centre of the Metaball (positive infinity) and then drop off quickly approaching zero as the distance from the Metaball gets larger and larger. If we were to look at what these values look like on the 2D plane, it would resemble this:


(Notice the banding that occurs around each of the Metaballs, and the way they combine to form 'layers' of Meta-things.)

In order to define the curves that we have above, we need to also define a threshold value (with a minimum and maximum) to have the pixels appear along the perimeter of our newly created isosurface. We need to use this minimum and maximum threshold because our screen, unlike a mathematical real-valued 2D plane, only has a finite amount of accuracy and a finite number of points. If we only used a single value for our threshold (eg. F(x,y) = C), many points would be missed by our algorithm, resulting in a much less accurate image:


(Badly chosen MAX and MIN threshold values can result in very thick metaballs, or ones that are thin and flicker as they move.)

An ideal threshold is usually found by trial-and-error, based on the average size of the Metaballs in the game world.

Writing a 2D Implementation

After discussing the ideas, equations, and algorithm behind Metaballs, let's examine some code that will provide us with a 2D implementation to work with. We'll start by defining a structure for a Metaball object, and an array to hold all of our Metaballs in:

struct METABALL
{
   float _x, _y;
   float _radius;

   METABALL(float startx, float starty, float radius)
   {
      _x = startx;
      _y = starty;
      _radius = radius;
   }

   float Equation(float x, float y)
   {
      return (_radius / sqrt( (x-_x)*(x-_x) + (y-_y)*(y-_y) ) );
   }
};

const MAX_METABALLS = 15;
METABALL *ballList[MAX_METABALLS]; // A list of Metaballs in our world
Now, assuming that you already have your graphics library of choice up and running, we jump straight into the core of the implementation, which is just as simple as applying the algorithm discussed:
const float MIN_THRESHOLD = 0.99f;
const float MAX_THRESHOLD = 1.01f; // Minimum and maximum threshold for an isosurface
...

void draw_metaballs()
{
   // Value to act as a summation of all Metaballs' fields applied to this particular pixel
   float sum;

   // Iterate over every pixel on the screen
   for(int y = 0; y < SCREEN_HEIGHT; y++)
   {
      for(int x = 0; x < SCREEN_WIDTH; x++)
      {
         // Reset the summation
         sum = 0;

         // Iterate through every Metaball in the world
         for(int i = 0; i < MAX_METABALLS && ballList[i] != NULL; i++)
         {
            sum += ballList[i]->Equation(x,y);
         }

         // Decide whether to draw a pixel
         if(sum >= MIN_THRESHOLD && sum <= MAX_THRESHOLD)
            draw_pixel(x, y, COLOR_WHITE);
      }
   }
}
This is the real work-horse of the entire Metaballs implementation – with this, one can easily create and tinker with one's own Metaballs. If you'd like to see the full source code of a working implementation, you can download several examples and demos in the References section.



Other Meta-Shapes


Contents
  Introduction
  Creating Meta-Things
  Other Meta-Shapes
  Optimizations and Improvements
  Conclusion

  Printable version
  Discuss this article