AngelCode.com > Developer resources

Fast metaballs
Andreas Jönsson, April 2001

What are metaballs?

One way of describing metaballs is that they are a way of visualizing gravity or energy fields. Take a point in space and measure the energy there. The energy in that point will be the sum of each balls contribution, where the contribution of one ball is higher the closer to that ball the point is. You get the special look of metaballs by rendering a surface that goes through each point in the space that have the energy value of a specified threshold.


Picture of 2d metaballs with several levels of energy.

The needed formulas

The first thing you need is a formula to compute the energy contribution from one ball. This formula should be based on the distance and must be continuous. Preferably it should never get below zero as that will lead to complications later on. Actually it might give a cool effect to have some of the balls give a negative energy contribution, however the algorithm presented in this article will not work with such balls. One perfect candidate for a formula like this is the gravity formula.

g = c m / d2

Where g is the gravity at a certain point, c a constant, m is the mass, and d the distance from the center of mass.

If you want to have some kind of lighting on your metaballs you'll also need a formula for computing the normal in a point in space based on the energy. Some of you might already know that that is simply done by deriving the energy formula, however I suspect some of you don't. I'm a nice guy so I'll give the formula right here so you don't have to do anything.

n = 2 m v / d4

Here n is the normal vector and v is the vector from the center of the ball to the point of interest. One thing to note is that you should only normalize the normal vector after summation of all the parts. This is not only because it is faster but more because you would get the wrong result otherwise.

How to render the metaballs

There are many techniques that can be used for rendering surfaces for this kind of application. If you are interested I suggest you search for techniques for finding iso-surfaces. One of the easier way of doing it is by using a technique called "marching cubes". I'm not going to give a detailed description of how marching cubes work, because there are so many other people that have already done that. If you look at the end of this article you'll find links to some of the better articles that I've found on marching cubes.

In short the marching cubes algorithm can be said to work like this. Take a regular 3D grid of values (volume data). Examine each voxel in this grid to see if the surface goes through it. If it does, compute the vertices by interpolating the values over the edges and render triangles between the vertices.


The cases of marching squares, the 2D brother of marching cubes.

The brute force way of rendering the metaballs would be to compute the energy for every point in the 3D grid and then let marching cubes go through each voxel to compute the surface. This would however be highly inefficient since the metaballs' surface will only go through approximatly 10% of the voxels, perhaps even less.

The algorithm

I will here present an algorithm that computes only the voxels that actually contain the surface, plus a few more to find the surface. I've found this algorithm to be incredibly efficient. I actually managed to create a small demo that has 20 metaballs moving in the scene at the same time, and that was without optimizations. I've previously only seen a maximum of 7 metaballs at the same time, and these most likely used many optimizations, such as efficient assembler code and lookup tables.

RenderMetaballs()
{
  for each ball
  {
    pos = ball's position in the grid
    isComputed = false
    found = false
    while not found
    {
      if voxel is computed
      {
        isComputed = true
        found = true
      }
      else
      {
        case = ComputeVoxel(pos)
        if case < 255
          found = true
        else
          pos = move one step up
      }
    }

    if not isComputed
    {
      AddNeighborsToOpenList(case,pos)
      while open list is not empty
      {
        pos = position of last voxel in open list
        case = ComputeVoxel(pos)
        AddNeighboursToOpenList(case,pos)
      }
    }
  }
}

RenderMetaballs() traces a line from the center of each metaball out until it reaches the surface. If the voxel where the line intersects the surface hasn't been computed yet it will compute that voxel and then progress to the neighbors until all interlinked voxels have been computed. If the voxel has already been computed then this metaball is connected to another, thus the surface doesn't have to be computed again.

ComputeVoxel(pos) uses the marching cubes algorithm to compute the vertices and triangles in this voxel. It then returns the marching cube case so that we can see how the surface lie within the voxel and which neighbors also contain the surface.

AddNeighborsToOpenList(case,pos) adds all neighbors to the list of open voxels unless they have already been added once. It uses the case variable to identify which neighbors actually contain the surface.


A screenshot from my demo.

A word of warning

The marching cubes algorithm used in this article is a patented algorithm, so make sure you know what you're doing when you use it. For a non-commercial demo you should be quite safe in using this technique but if you plan on using it in a commercial product you might want to investigate into the patent to know just exactly what the patent holder is allowed to do.

Conclusion

I hope that you found this article interesting and perhaps useful. Should you find something that bothers you, perhaps something that you think is an error or just something that's not quite clear then feel free to contact me. I will be happy to discuss it with you.

Thanks to Maciek, a.k.a. Shyha, for pointing out a minor error in the RenderMetaballs() algorithm.