Metaballs in 2D

Here we go with the questions again. Don’t mind me ;D

Problem:

What is the quickest way to draw a few (3-4) simple 2D Metaballs in a smallish (200 * 100) window?

Metaballs = those blobby things you often see in old demos.

My solution currently is:

  1. Create a BufferedImage with TYPE_INT_ARGB

  2. Obtain the raster array from the DataBuffer

  3. Create a float array with the same length as the raster array. This array is used to store all the iso-values for the metaballs.

The term iso-value is just a number i obtain from a particular function

  1. For each animation loop,

  2. Move the position of the metaballs

  3. I set the raster array to all 0’s using Array.fill()

  4. I set the float array to all 0’s using Array.fill()

  5. Calculate the iso-values using the function g® = r^4 - r^2 + 0.25. This function has a range of 0 - 0.25 with the domain 0 - 0.707. So, a point with radius R < 0.707(relative to the center of a metaball) has iso-value . Otherwise, iso-value is 0. Credit given to: http://www.geisswerks.com/ryan/BLOBS/blobs.html

  6. Instead of going the purely brute force way, for each ball, I get the rectangular bounds of each metaball with threshold radius of R = 0.707 (outside the threshold, the value of is 0). I then calculate and increment the iso-value for each point within the bounds into the float array. So, point b[/b] within the bounds has iso-value:

floatArray[y * WIDTH + x] += g( distance(x-ballX, y-ballY) )

I also set the color of the raster array at the corresponding position given the value of the iso-value:

if( floatArray[y * WIDTH + x] > RENDER_THRESHOLD)
        rasterArray[y * WIDTH + x] = some color value
  1. Draw the image onto the screen

I’ve made it a point to eliminate divisions and sqrts but I’m still worried about the 2 Array.fills() I have each animation loop.

Has anyone got a quicker method to render 2D metaballs?

Write it, [Profile it, [Optimise it.]]

Until you know what the slow bits are,
it is pointless speculating about optimisations.

Yes, but I was afraid that my way of implementing it is already fundamentally slow - ie, no amount of tweaking/profiling is going to make it any faster compared to one implemented with a better algorithm.

I also figured that there a lots of demo-coders out there who have done something like this, so there must be a better algorithm than what I suggested . :slight_smile:

Anyway, I’ve whipped something up with a kernel that is reasonably fast enough to be used in my game. Drawing to screen is still slow (especially if translucency is used) because we’re manipulating per-pixel stuff here, so no hardware acceleration. If anyone wants a reference to start coding simple 2D metaballs , tell me and I’ll post my source here.

The server VM may give you a significant speed boost, as it has some array bounds checking removal.

Post away, Metaballs are funky - 3D metaballs are even funkier :wink:

[quote]The server VM may give you a significant speed boost, as it has some array bounds checking removal.
[/quote]
Yes, I’ve been using -server ever since I found that out last week. The performance boost is SHUPAR!!! I want to emphasize:

ALWAYS USE SERVER VM IF YOU CAN!!!

Okay, 2D metaballs here :


import javax.swing.*; 
import java.awt.*; 
import java.awt.image.*; 
import java.util.Random; 
import java.util.*;

/*
 * 2D METABALLS, by Ben Yeoh, 7/05/04
 *
 * A humble reference for those wanting to write their own metaballs code
 *
 * Please share if you've got a simpler/quicker algorithm than what I have here.
 *
 */

public class MetaballTest extends JFrame implements Runnable 
{ 
 
 
 static final boolean BUFFER_STRATEGY = true; 
 final byte CLEAR = 0; 
 static final int BITMASK_BLITS_PER_FRAME = 1; 
  
 static final int WINDOW_WIDTH = 800, WINDOW_HEIGHT = 600; 
 static final int METABALLS_SCREEN_WIDTH = 400, METABALLS_SCREEN_HEIGHT = 400; 

      Image bb; 
       BufferedImage blobby; 
       DataBufferByte db;
       
       byte[] pixel;
       Metaball[] balls;
      
      boolean movingToCenter;
      
      final int DISPERSION_DELAY = 30;
      final int CHARGE_STRENGTH = 900;
      final int ISO_RADIUS = 50; // pixels
      final int INVISIBLE_THRESHOLD = 0;
      final int ISO_DIAMETER = (ISO_RADIUS*2) + 1;
       
      byte[] textureMap;
      int[] x,y;
      float[] movingWithAccel;
      float[] movingWithDecel;
      float[] movingWithAccelAndDecel;
      float[] dispersionX;
      float[] dispersionY;
       
      final int NO_OF_BALLS = 32;
      final int NO_OF_STEPS = 800;
      final int NO_OF_DIRECTIONS = 60; // in 360 degrees
      final int BUFFER_FROM_BORDER_WIDTH = 0; // pixels
      final int BUFFER_FROM_BORDER_HEIGHT = 0; // pixels
      final int NO_OF_POINTS = 4;
      final int NO_OF_BALLS_PER_POINT = NO_OF_BALLS/NO_OF_POINTS;

      int delayToDisperse;
      int indexToDisperse;
      Random rand;
        
        ArrayList choiceList;
        private ChoiceNumber[] choices;
 
       int counter;
 
 public static void main(String[] args) 
 { 
   Thread t = new Thread(new MetaballTest()); 
  t.setPriority(Thread.MIN_PRIORITY);    
  t.start(); 
 } 
   
 public MetaballTest() 
 { 
              setIgnoreRepaint(true); 
              getContentPane().setLayout(null); 
              setBounds(new Rectangle(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT));   
              setVisible(true); 
  
  
  
        /*
      * Declare and create our speed tables and direction vector tables based on sin and cos
           */
          movingWithAccel = new float[NO_OF_STEPS];
          movingWithDecel = new float[NO_OF_STEPS];
          movingWithAccelAndDecel = new float[NO_OF_STEPS];
          double UNIT = (Math.PI/2) / NO_OF_STEPS;
              
        for(int i=1; i <= movingWithAccel.length; i++)
                    movingWithAccel[i-1] = (float) (Math.cos(Math.PI + i*UNIT) + 1);
      
              for(int i=1; i <= movingWithDecel.length; i++)
                    movingWithDecel[i-1] = (float) Math.sin(i*UNIT);

              UNIT = (Math.PI)/NO_OF_STEPS;
              for(int i=1; i <= movingWithAccelAndDecel.length; i++)
                    movingWithAccelAndDecel[i-1] = (float) ((Math.cos(Math.PI + i*UNIT) + 1)/2);      

            dispersionX = new float[NO_OF_DIRECTIONS]; // Normalized direction vector arrays
            dispersionY = new float[NO_OF_DIRECTIONS];
            float radius = 1.0f;
            for(int i=0; i < dispersionX.length; i++)
            {
                  dispersionX[i] = (float) (radius * Math.cos(Math.toRadians(i*(360/NO_OF_DIRECTIONS))));
                  dispersionY[i] = (float) (radius * Math.sin(Math.toRadians(i*(360/NO_OF_DIRECTIONS))));
            }
            
            /*
             * Declare our color pallette arrays
             */
            byte[] red = new byte[256];
          byte[] blue = new byte[256];
          byte[] green = new byte[256];
          byte[] alpha = new byte[256];
          int index = 0; // this is used as a pointer to the array values
              
            // BLACK TO DARK BLUE
            for(int i=0; i < 64; i++, index++)
            {
                  red[index] = (byte) 0;
                   green[index] = (byte) 0;
                   blue[index] = (byte)(i * 2.5f);
                    alpha[index] = (byte)255; //( (i * 4f) - ((INVISIBLE_THRESHOLD) * 4f) );
            }
                    
            int blueOffset = blue[index-1] & 0xFF;
            float blueFactor = (256f - blueOffset)/96f;
                    
            int alphaOffset = alpha[index-1] & 0xFF;
            float alphaFactor = (256f - alphaOffset)/(96f/2);
                    
              //DARK BLUE to BRIGHT BLUE  
              for(int i=1; i <= 96; i++, index++)
             {
                    red[index] = (byte) (i * 1.0f);
                    green[index] = (byte) (i * 2f);
                    blue[index] = (byte)( (((i * blueFactor) + blueOffset) > 255)? 255: ((i * blueFactor) + blueOffset) ) ;
                    alpha[index] = (byte) 255; // ( (((i * alphaFactor) + alphaOffset) > 255)? 255: ((i * alphaFactor) + alphaOffset) ) ;
            }
        
            int redOffset = red[index-1] & 0xFF;
            float redFactor = (256f - redOffset)/96f;
                    
            int greenOffset = green[index-1] & 0xFF;
            float greenFactor = (256f - greenOffset)/96f;
                    
            //BRIGHT BLUE to WHITE
            for(int i=1; i <= 96; i++, index++)
            {
                  red[index] = (byte)        ( (((i * redFactor) + redOffset) > 255)? 255: ((i * redFactor) + redOffset) ) ;
                    green[index] = (byte) ( (((i * greenFactor) + greenOffset) > 255)? 255: ((i * greenFactor) + greenOffset) ) ;
                  blue[index] = (byte)  ( 255 );
                  alpha[index] = (byte) ( 255 );
            }
  
              //MAKE INVISIBLE
              for(int i=0; i < INVISIBLE_THRESHOLD; i++)
              {
                    red[i] = (byte)        0;
                    green[i] = (byte) 0;
                    blue[i] = (byte)  0;
                    alpha[i] = (byte) 0;
              }        
  
              /*
               * Create our customized image and grab the pixel array
               */
          IndexColorModel cm = new IndexColorModel(8, 256, red, green, blue, alpha);
          blobby = new BufferedImage(METABALLS_SCREEN_WIDTH, METABALLS_SCREEN_HEIGHT, BufferedImage.TYPE_BYTE_INDEXED, cm);    
          pixel = ((DataBufferByte) (blobby.getRaster().getDataBuffer())).getData(); 
        
             initTextureMap(); // initialize the texel colors for each metaball

            /*
             * Create and place our metaballs
             */
              balls = new Metaball[NO_OF_BALLS];
          int startX = METABALLS_SCREEN_WIDTH/2;
          int startY = METABALLS_SCREEN_HEIGHT/2;         
          for(int i=0; i < NO_OF_BALLS; i++)
                   balls[i] = new Metaball(startX, startY);

            /*
             * Create our list to randomly assign metaballs to predetermined points
             */
            choices = new ChoiceNumber[NO_OF_POINTS];
          choiceList = new ArrayList(NO_OF_POINTS); // the list of choices for indexes of positions to assign
            for(int i=0; i < NO_OF_POINTS; i++)
            {
                    ChoiceNumber num = new ChoiceNumber();
                    num.number = i;
                    num.occurrences = NO_OF_BALLS_PER_POINT;
                    choices[i] = num;
                    choiceList.add(num);
            }
            
            /*
             * Misc 
             */
            rand = new Random(); // random number gen
            x = new int[NO_OF_POINTS]; // predetermined points to form the shapes
            y = new int[NO_OF_POINTS];
            
            indexToDisperse = 0; // pointer to the ball in the array to disperse this frame
            delayToDisperse = 0; // delay before another ball gets dispersed

            counter = 0;
          changeToZ();

  
  if(BUFFER_STRATEGY) 
  { 
   createBufferStrategy(2); 
  } 
  else 
  { 
   bb = getGraphicsConfiguration().createCompatibleVolatileImage(WINDOW_WIDTH, WINDOW_HEIGHT); 
  } 
  
  setDefaultCloseOperation(EXIT_ON_CLOSE); 
 } 
  
        private void changeToCube()
        {
              x[0] = METABALLS_SCREEN_WIDTH/4;
            y[0] = METABALLS_SCREEN_HEIGHT/4;
        
            x[1] = x[0] + 50;
          y[1] = y[0];
         
         
          x[2] = x[0] + 50;
          y[2] = y[0] + 50;
         
         
          x[3] = x[0];
          y[3] = y[0] + 50;

            // Reset choiceLists
            choiceList.clear();
            for(int i=0; i < choices.length; i++)
            {
                  choices[i].occurrences = NO_OF_BALLS_PER_POINT;
                  choiceList.add(choices[i]);
            }
      
            // Reset ball states
            for(int i=0; i < balls.length; i++)
            {
                  balls[i].dispersed = true;
            }
            
            // Reset dispersion index             
            indexToDisperse = 0;
               delayToDisperse = 0;
      } 

cont…



      private void changeToZ()
      {
            x[0] = METABALLS_SCREEN_WIDTH/4;
            y[0] = METABALLS_SCREEN_HEIGHT/4;
        
            x[1] = x[0] + 50;
          y[1] = y[0];
         
         
          x[2] = x[0] + 50;
          y[2] = y[0] + 50;
         
         
          x[3] = x[0] + 2*50;
          y[3] = y[0] + 50;

            // Reset choiceLists
            choiceList.clear();
            for(int i=0; i < choices.length; i++)
            {
                  choices[i].occurrences = NO_OF_BALLS_PER_POINT;
                  choiceList.add(choices[i]);
            }
      
            // Reset ball states
            for(int i=0; i < balls.length; i++)
            {
                  balls[i].dispersed = true;
            }
            
            // Reset dispersion index             
            indexToDisperse = 0;
               delayToDisperse = 0;
      } 
 
      private void changeToPole()
      {
            x[0] = METABALLS_SCREEN_WIDTH/4;
               y[0] = METABALLS_SCREEN_HEIGHT/4;
  
              x[1] = x[0] + 50;
              y[1] = y[0];
   
          x[2] = x[0] + 2*50;
          y[2] = y[0];

          x[3] = x[0] + 3*50;
          y[3] = y[0];      
            
            // Reset choiceLists
            choiceList.clear();
            for(int i=0; i < choices.length; i++)
            {
                  choices[i].occurrences = NO_OF_BALLS_PER_POINT;
                  choiceList.add(choices[i]);
            }
      
            // Reset ball states
            for(int i=0; i < balls.length; i++)
            {
                  balls[i].dispersed = true;
            }
            
            // Reset dispersion index             
            indexToDisperse = 0;
               delayToDisperse = 0;
      }
      
      /*
       * Randomly chooses a pre-specified point to form the current shape for a metaball
       */
      private void changeTo(Metaball ball)
      {
            int random = rand.nextInt(choiceList.size());
            ChoiceNumber choice = (ChoiceNumber) choiceList.get(random);
            ball.setDest(x[choice.number], y[choice.number], Metaball.ACCEL, true);
            
            choice.occurrences--;
            if(choice.occurrences == 0)
            {
                  choiceList.remove(random);
            }
      }      
                              
  
 public void run() 
 { 
        Random rand = new Random(); 
        long lastTime = System.currentTimeMillis(); 
        int frameCount = 0; 
        String fps = "n/a"; 
        while(true) 
        { 
              
               frameCount++; 
               long time = System.currentTimeMillis(); 
               if(time-lastTime > 1000) //once a second 
               { 
                //update the fps counter 
                      fps = Integer.toString(frameCount); 
                      frameCount=0; 
                lastTime+=1000; 
     
              }
    
              Graphics bg = BUFFER_STRATEGY?getBufferStrategy().getDrawGraphics():bb.getGraphics(); //buffers Graphics 
               bg.setColor(Color.GRAY);
               bg.fillRect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
    
               if(counter == 3000)
               {      
                     changeToPole();
               }
               else
               if(counter == 6000)
               {
                     changeToZ();
               }
               else
               if(counter == 9000)
               {
                     changeToCube();
                     counter = 0; // reset counter
               }
               
            delayToDisperse++;
            /*
             * Disperse the balls at the current index and at the correct delay time
             */
            if(indexToDisperse < balls.length && (delayToDisperse == DISPERSION_DELAY))
               {
                     int num = rand.nextInt(NO_OF_DIRECTIONS); // choose a random direction
                  float xMag, yMag;
                  
            //      System.err.println("CurX: " + balls[indexToDisperse].x + " CurY: " + balls[indexToDisperse].y);            
                  
                  /*
                   * If the x-axis direction component is going leftwards
                   * then get the magnitude of the distance to the left border from the current position of the ball
                   *
                   * Else, get the right border instead
                   */
                  if(dispersionX[num] < 0) // get the left border
                        xMag = (0f + BUFFER_FROM_BORDER_WIDTH - balls[indexToDisperse].x)/dispersionX[num];
                  else                               // get the right border
                      xMag = ((METABALLS_SCREEN_WIDTH*1.0f - 1 - BUFFER_FROM_BORDER_WIDTH) - balls[indexToDisperse].x)/dispersionX[num];
                  
                  /*
                   * If the y-axis direction component is going upwards
                   * then get the magnitude of the distance to the top border from the current position of the ball
                   *
                   * Else, get the bottm border instead
                   */
                  if(dispersionY[num] < 0) // get the top border
                        yMag = (0f + BUFFER_FROM_BORDER_HEIGHT - balls[indexToDisperse].y)/dispersionY[num];
                  else                                // get the bottom border      
                        yMag = ((METABALLS_SCREEN_HEIGHT*1.0f - 1 - BUFFER_FROM_BORDER_HEIGHT) - balls[indexToDisperse].y)/dispersionY[num];
      
            //      System.err.println("Disperse xMag: " + xMag + " yMag: " + yMag);
            //      System.err.println("DestX: " + (int)(balls[indexToDisperse].x + (dispersionX[num] * xMag)) + " DestY: " + (int)(balls[indexToDisperse].y + (dispersionY[num] * xMag)));                  
                  
                  /*
                   * Now check which magnitude is smaller, which means that the ball will bump into the border
                   * at that direction 1st
                   *
                   * So, multiply both direction vectors to get the position of the ball just before the 
                   * bump to the border in question
                   */
                  if(xMag < yMag) // will reach the width border 1st
                        balls[indexToDisperse].setDest((int)(balls[indexToDisperse].x + (dispersionX[num] * xMag)), 
                                                                     (int)(balls[indexToDisperse].y + (dispersionY[num] * xMag)), Metaball.DECEL, false);
                  else // will reach the height border 1st
                        balls[indexToDisperse].setDest((int)(balls[indexToDisperse].x + (dispersionX[num] * yMag)), 
                                                                     (int)(balls[indexToDisperse].y + (dispersionY[num] * yMag)), Metaball.DECEL, false);            
      
                  /*
                   * Now set the state of the ball to show that it has not reached it's destination yet
                   */
                  balls[indexToDisperse].dispersed = false;
                  indexToDisperse++; // increment the index to point to the next ball
                  delayToDisperse = 0;  // reset the delay disperse counter
            }      
            
            /*
             * Move each ball in the scene and update it's status
             */
            for(int i=0; i < balls.length; i++)
            {
                  balls[i].move();
                  if(balls[i].reachedDest())
                  {
                        if(!balls[i].dispersed)
                        {
                              balls[i].dispersed = true;      // update state                        
                              changeTo(balls[i]); // assign the shape change position to the ball as it's next destination
                        }
                        else
                              balls[i].jiggle(); // randomly choose the next position of the ball
                  }
            }      
                    

cont again…



/*
               * THE KERNEL
               *
               * Draw each pixel on the screen based on the position of each metaball
               * 
               */      
          
          Arrays.fill(pixel, CLEAR); // clear the array of pixels
               
             int dx, dy, tRowIndex, rowIndex;
             
             for(int i=0; i < balls.length; i++) // for each ball
             {                  
                 /*
                  * Get the bounds of each ball based on it's bounding box, making sure
                  * that the bounds is not larger than the borders
                  */
                   int minY = (balls[i].bounds.y<0)? 0: balls[i].bounds.y;
                   int minX = (balls[i].bounds.x<0)? 0: balls[i].bounds.x;
                   
                   int maxY = ((balls[i].bounds.y + balls[i].bounds.height - 1) < METABALLS_SCREEN_HEIGHT)? (balls[i].bounds.y + balls[i].bounds.height - 1) : METABALLS_SCREEN_HEIGHT-1; 
                   int maxX = ((balls[i].bounds.x + balls[i].bounds.width - 1) < METABALLS_SCREEN_WIDTH)? (balls[i].bounds.x + balls[i].bounds.width - 1) : METABALLS_SCREEN_WIDTH-1; 
                   
                   int rangeX = maxX - minX;
                   int rangeY = maxY - minY;
                   
                   dx = minX - balls[i].x + ISO_RADIUS;  // the horizontal displacement of the current position from the ball's center
                   dy = minY - balls[i].y + ISO_RADIUS;  // The vertical displacement of the current position from the ball's center 
                   
                   rowIndex = minY * METABALLS_SCREEN_WIDTH + minX;  // The row pointer for the actual pixel location to draw
                   tRowIndex = dy * ISO_DIAMETER + dx;   // The row pointer for the texture to draw for the ball      
                   
                   int maxRowIndex = rowIndex + (rangeY * METABALLS_SCREEN_WIDTH); // maximum index to draw on the pixel array
                   
                   for(; rowIndex <= maxRowIndex; rowIndex+=METABALLS_SCREEN_WIDTH)
                   {      
                         int index = rowIndex; // current pointer to the pixel on the image
                         int tIndex = tRowIndex; // current pointer to the ball texture
                         int maxIndex = index + rangeX; // maximum index to draw on the 
                                                  
                         for(; index <= maxIndex; index++)
                         {      
                               /*
                                * Increment the value already at the pixel with the value of the metaball texture
                                * to the maximum of 255
                                *
                                * In case you're wondering, we need to use AND with 0xFF because the byte primitive is signed
                                * and we only want values 0 - 255. 
                                */ 
                               int newpix = (pixel[index] & 0xFF) + (textureMap[tIndex] & 0xFF); 
                               
                               if(newpix > 255)
                                     newpix = 255;
                               
                               pixel[index] = (byte) newpix;
                                     
                              tIndex++;                               
                         }       
                         tRowIndex+=ISO_DIAMETER;            
                   }      
             }
         
          bg.drawImage(blobby, (WINDOW_WIDTH - METABALLS_SCREEN_WIDTH)/2, (WINDOW_HEIGHT- METABALLS_SCREEN_HEIGHT)/2, null); 
         
         
         
          bg.setColor(Color.RED); 
          bg.drawString(fps, WINDOW_WIDTH/2 - 50, WINDOW_HEIGHT/2); 
          bg.dispose(); 
          
          if(BUFFER_STRATEGY) 
          { 
                  getBufferStrategy().show(); 
          } 
          else 
          { 
                 Graphics g = getGraphics(); 
                 g.drawImage(bb, 0, 0, null); 
                 g.dispose(); 
          } 
         
          Thread.yield(); 
          counter++;
       }  
 } 
 
       /*
       * Initializes the textureMap (ie, values from 0 - 255) of a metaball
       * This is crucial because we don't want to keep calculating the values for each pixel in the kernel algorithm
       *
       * For the current texture map, we will use the formula 1/sqrt(r^2) * CHARGE_STRENGTH to determine the value
       * of the charge at the point relative to the center of the charge. This is basically the electric field strength
       * formula and it gives a nice value that decreases as the point moves further away from the center of the charge
       *
       * r = distance of the current point from the distance of the center of the charge
       */
      private void initTextureMap()
       {
             textureMap = new byte[ISO_DIAMETER*ISO_DIAMETER]; 
             float temp = ( 1f/((float)Math.sqrt((ISO_RADIUS*ISO_RADIUS) + 1)) ); 
             float minus = temp * CHARGE_STRENGTH;
             
             for(int y=0; y < ISO_DIAMETER; y++)
             {
                   for(int x=0; x < ISO_DIAMETER; x++)
                   {
                         int dy = (y - (ISO_RADIUS)); 
                         int dx = (x - (ISO_RADIUS)); 
                         int x2 = dx*dx; 
                         int y2 = dy*dy; 
                         
                         int isoValue;
                         if(x2 == 0 && y2 == 0)
                               isoValue = 255;
                         else      
                         {
                               float field = (float) (1f/(Math.sqrt(x2+y2)));
                               isoValue = (int) (field * CHARGE_STRENGTH  - minus) ;
                               if(isoValue < 0)
                                     isoValue = 0;
                               if(isoValue > 255)
                                     isoValue = 255; 
                         }
                         
                         textureMap[y*ISO_DIAMETER + x] = (byte)isoValue;
             //            if(isoValue == 255)
             //                  System.err.println("MAP Y: " + y + " X: " + x + " VALUE: " + isoValue + " tRowIndex: " + y *ISO_DIAMETER + " DX: " + x);
                   }
             }
       }
      
      


/*
       * This class is needed to simulate random choices for the metaball, see method changeTo(Metaball ball)
       */
      private class ChoiceNumber
      {
            public int number;
            public int occurrences;
      }
                   
       /*
        * This class encapsulates all the needed properties of a metaball, particularly it's movement.
        */      
      private class Metaball
       {
             public int x,y; // current location of the center of the metaball
             
              public Rectangle bounds; // the bounding box
            private int origX, origY; // original position of the metaball when a new destination is set
            private int displacementX, displacementY; // the displacement vector to the destination position
            
            private int movementCounter; // movement counter - incremented each time it moves
            public boolean dispersed; // to have reached the dispersed state means that the ball has already 
                                                  // reached the dispersed position for the shape change destination 
            
            public final static int ACCEL = 0; // state variables
            public final static int DECEL = 1;
            public final static int BOTH = 2;
            
            private int speedFactor; // the speed at which the metaball moves
            private float[] speedGraphX; // the current speed graph table to use for each axis
            private float[] speedGraphY;
            private final int DEFAULT_JIGGLE_RATIO = 4; // larger the value, smaller the distance
            private final int DEFAULT_JIGGLE_DISTANCE = 50 / DEFAULT_JIGGLE_RATIO; // The maximum displacement to jiggle for each axis
            private final int SHAKING_JIGGLE_DISTANCE = 50 / 2;
            private Rectangle jiggleBounds; // the bounds of where it should be allowed to jiggle
            
              public Metaball(int x, int y)
              {             
                    this.x = x;
                    this.y = y;
                    
                    origX = this.x;
                    origY = this.y;
      
                    displacementX = 0;
                    displacementY = 0;
                    movementCounter = NO_OF_STEPS; // reached it's destination
                                              
                    bounds = new Rectangle();
                    bounds.x = this.x - ISO_RADIUS;
                    bounds.y = this.y - ISO_RADIUS;
                    bounds.width = ISO_DIAMETER;
                    bounds.height = ISO_DIAMETER;
                  
                  jiggleBounds = new Rectangle();
                    jiggleBounds.x = this.x - (DEFAULT_JIGGLE_DISTANCE >> 1);
                    jiggleBounds.y = this.y - (DEFAULT_JIGGLE_DISTANCE >> 1);
                    jiggleBounds.width = DEFAULT_JIGGLE_DISTANCE;
                    jiggleBounds.height = DEFAULT_JIGGLE_DISTANCE;
                    
                    dispersed = true; // assume state that the ball has been dispersed and reached it's destination, ie jiggling
                  speedGraphX = movingWithAccelAndDecel; 
                  speedGraphY = movingWithAccelAndDecel;
                  speedFactor = 8;
                  
              }
              
              /*
               * Randomly choose a position for the ball to go 
               * (not too far from the assigned the shape position, we need it to still look like a shape) 
               */
              public void jiggle() 
              {
                    origX = x;
                    origY = y;
                    
                    // we only want to increase the speed of the jiggle if the ball is within a certain distance
                    if(jiggleBounds.contains(origX, origY))
                          speedFactor = 8;

                    int destX = jiggleBounds.x + rand.nextInt(jiggleBounds.width); // get random coordinates
                    int destY = jiggleBounds.y + rand.nextInt(jiggleBounds.height);
              
                    displacementX = destX - origX;
                    displacementY = destY - origY;
                    
                    speedGraphX = movingWithAccelAndDecel; // set the appropriate speed graph table and speed
                    speedGraphY = movingWithAccelAndDecel;
                    movementCounter = 0; //  reset the movement counter
             }
              
              public void setShaking(boolean shake)
              {
                    if(shake)
                    {
                          jiggleBounds.width = SHAKING_JIGGLE_DISTANCE;
                          jiggleBounds.height = SHAKING_JIGGLE_DISTANCE;
                    }
                    else
                    {
                          jiggleBounds.width = DEFAULT_JIGGLE_DISTANCE;
                          jiggleBounds.height = DEFAULT_JIGGLE_DISTANCE;
                    }      
              }                   
                          
              public void setDest(int destX, int destY, int movementType, boolean toSquare)
              {
                    if(toSquare) // if it is a pre-determined shape position, store the new jiggle bounds
                    {
                          jiggleBounds.x = destX - (jiggleBounds.width >> 1);
                          jiggleBounds.y = destY - (jiggleBounds.height >> 1);                         
                    }
                    
                    origX = x; 
                    origY = y;
                    
                    displacementX = destX - origX;
                    displacementY = destY - origY;
                    
              //      System.err.println("DISPLACEMENT X: " + displacementX + " DEST Y: " + displacementY);
                    switch(movementType) // choose the speed graph
                    {
                          case ACCEL:
                                speedGraphX = movingWithAccelAndDecel;
                                speedGraphY = movingWithAccelAndDecel;
                                speedFactor = 1;
                                break;
                          
                          case DECEL:
                                speedGraphX = movingWithAccelAndDecel;
                                speedGraphY = movingWithAccelAndDecel;
                                speedFactor = 1;
                                break;
                          
                          case BOTH:
                                speedGraphX = movingWithAccelAndDecel;
                                speedGraphY = movingWithAccelAndDecel;
                                speedFactor = 1;
                                break;
                    }
                                
                    movementCounter = 0; // reset movement counter
              }      
              
              public void move()
              {
              //      System.err.println("BEFORE Current X: " + x + " Current Y: " + y);
                    if(movementCounter < NO_OF_STEPS)
                    {
                          x = origX + (int) (displacementX * speedGraphX[movementCounter]); // calculate current position
                          y = origY + (int) (displacementY * speedGraphY[movementCounter]);
                          
                          bounds.x = this.x - ISO_RADIUS;
                          bounds.y = this.y - ISO_RADIUS;
                          
                    //      System.err.println("AFTER Current X: " + x + " Current Y: " + y);
                        movementCounter+=speedFactor; // increment the movement counter
                    }
                    
              }
                                
              public boolean reachedDest() // query state if the metaball has reached the current destination position
              {
                    return (movementCounter >= NO_OF_STEPS); 
              }
              
      }// finish Metaball
        
} 

Phew. That’s it. Just cut and paste into a file.

I only wish the formatting was better in this blasted forum software.

I just want to add that the general algorithm is the same for what I posted in the 1st post (ie rectangular bounds updating for each metaball), but with quite a few differences in other areas.

Should be easy enough to figure out on yer own, but feel free to ask questions or make improvement suggestions here.

I’m particularly interested to find out how to have translucent metaballs blitted onto the screen at hardware accelerated speeds, if it is at all possible in Java. So if you know how, please share :).