29
Mar 10Metaballs With BlendMode.ADD
Metaballs are organic looking blobs and are often used to model fluid drops as they join together and pull apart. Usually a metaball is described as a source point with a field around it. As the field moves away from the point, the energy of the field diminishes. When you have several metaballs together, the energy at any given point is simply the sum of all the energies of the metaballs in the system at that point.
The edge of a metaball is linking together areas of the same energy, much like the contours on a map. It is these contours that give the metaball its distinctive blobbiness.
Here’s a quick example of what I’m talking about. Click on the image to watch the metaballs in action.
Click on the image to watch some metaballs in action
There are lots of methods for modelling metaballs in two dimensions. One of the best I’ve come across is this one which uses a very efficient method for determining the edge of the metaball and then tracing around that edge to define the shape.
An even faster method for displaying metaballs can be achieved by using BlendMode.ADD which does all the adding up of the energy fields for us. If you represent the energy field as a circular gradient starting with blue in the centre (0xFF) and going out to black at the perimeter (0×00) any pixel’s value in that circle will describe the energy of the field at that point. By overlaying these gradient circles with BlendMode.ADD, the blend mode sums the values of each pixel and displays the sum of energies at that point. All that’s let to do is apply a palette map to convert the blue energy field to something prettier.
The source code for the example is after the click. There are two classes, one controlling the simulation and the other describing each ball.
package { import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.Sprite; import flash.events.Event; import flash.geom.Point; public class Main extends Sprite { private var balls:Array; private var ballSprite:Sprite; private var display:BitmapData; private var zeroPoint:Point; private var displayMap:Array; public function Main() { addEventListener(Event.ADDED_TO_STAGE, init); } private function onEnterFrame(e:Event):void { for each (var ball:Ball in balls) { ball.update(); } // copy the balls to the display bitmap, applying a palette map // to make them look pretty display.lock(); display.fillRect(display.rect, 0xFF000000); display.draw(ballSprite); display.paletteMap(display, display.rect, zeroPoint, null, null, displayMap, null); display.unlock(); } private function init(e:Event):void { ballSprite = new Sprite(); display = new BitmapData(stage.stageWidth, stage.stageHeight, true, 0xFF000000); addChild(new Bitmap(display)); zeroPoint = new Point(); balls = new Array(); displayMap = new Array(256); var i:int; for (i=0; i<9; i++) { var x:Number = Math.random() * stage.stageWidth; var y:Number = Math.random() * stage.stageHeight; var ball:Ball = new Ball(x, y, stage.stageWidth, stage.stageHeight); ballSprite.addChild(ball); balls.push(ball); } // set up an array to be used in the palette map which will convert // the blue-black balls to red and yellow ones for (i=0; i<256; i++) { if (i < 0x3F) { // this corresponds to pixels whose energy value is too low displayMap[i] = 0; } else if (i < 0x4F) { // this corresponds to pixels at the edge of the blob areas // and displays the blobs with a yellow-orange-red gradient displayMap[i] = 0xFF000000 | ((i - 0x3F) << 20); var foo:uint = (i - 0x3F) << 20; } else { // this corresponds to pixels whose energy value is high enough // to be considered part of a blob and colours them yellow displayMap[i] = 0xFFFF0000 | (i << 8); } } addEventListener(Event.ENTER_FRAME, onEnterFrame); } } }
package { import flash.display.BlendMode; import flash.display.GradientType; import flash.display.Sprite; import flash.geom.Matrix; public class Ball extends Sprite { private var xMax:Number; private var yMax:Number; private var vx:Number; private var vy:Number; public function Ball(x:Number, y:Number, xMax:Number, yMax:Number) { this.x = x; this.y = y; this.xMax = xMax; this.yMax = yMax; init(); } public function update():void { // move the ball x += vx; y += vy; // check for the ball hitting the bounds of the display if (x < 0) {x=0; vx = -vx;} else if (x > xMax) {x=xMax; vx = -vx;} if (y < 0) {y=0; vy = -vy;} else if (y > yMax) {y=yMax; vy = -vy;} } private function init():void { // give the ball a random velocity vx = Math.random() * 20 - 10; vy = Math.random() * 20 - 10; // draw a gradient circle, blue in the middle and black at the edge var matrix:Matrix = new Matrix(); matrix.createGradientBox(200, 200, 0, -100, -100); graphics.beginGradientFill(GradientType.RADIAL, [0xFF, 0x00], [1, 0], [0x00, 0xFF], matrix); graphics.drawCircle(0, 0, 100); graphics.endFill(); // set the blend mode to ADD so the balls are overlaid properly blendMode = BlendMode.ADD; } } }
