幾年前就在網上曾看見過這種效果,但一直不知道叫什麼名字
前一陣無意在9ria(天地會)論壇上看到了一篇專門講這個的文章:AS3 元球(Metaball),不過有點遺憾的是那篇文章上的代碼直接複製下來都不能調試,花了點時間整理了一下,終於調試通過了,貼在這裡分享一下:
Metaball的公式:
其中,x、y是舞台上的任意一個點,x0、y0是metaball的位置,R為半徑。從公式上看,可以理解為萬有引力的變種(即引力與距離成反比,與半徑與正比)
先定義一個Metaball類(註:相對於原文而言,增加了vx,vy速度變數,用於後面示範運動的效果):
package {public class Metaball {public var radius:Number;public var x:int;public var y:int;public var original_radius:Number;public var vx:Number;//x軸速度public var vy:Number;//y軸速度public function Metaball(x:Number,y:Number,radius:Number) {this.x=x;this.y=y;this.original_radius = radius;//儲存原始半徑this.radius=radius*radius;//設定為radius的二次方,有助於後面最佳化效能 }public function equation(tx:Number,ty:Number):Number {//Metaball公式的體現: return radius/((x-tx)*(x-tx)+(y-ty)*(y-ty));//因為之前Radius已經是平方值了,這裡就不再用平方根處理(就是上面提到的最佳化效能的解釋)}}}
接下來是如何運用:
var sW:Number=stage.stageWidth;//舞台寬度var sH:Number=stage.stageHeight;//舞台高度var canvas:BitmapData=new BitmapData(sW,sH,false,0xff000000);//預設產生一個黑背景的BitmapDatavar rect:Rectangle=canvas.rect;//canvas的矩形地區var pt:Point=new Point(rect.left,rect.top);//rect的左上頂點var blurFilter:BlurFilter=new BlurFilter(10,10);//定義一個模糊濾鏡var metaballs:Array = new Array();//用於儲存n個metaball的數組var ballNumber:uint=5;//小球數目var i:uint=0;//迴圈變數var minThreshold:int=0x000009;//最小閾值var maxThreshold:int=0x000020;//最大閾值var bitMap:Bitmap = new Bitmap();//最終用來顯示的位元影像對象var isHollow:Boolean=false;//是否空心圖形function init() {for (i=0; i<ballNumber; i++) {var b:Metaball=new Metaball(Math.random()*sW,Math.random()*sH,20 + Math.random()*80);if (b.x>sW-b.original_radius) {b.x=sW-b.original_radius;} else if (b.x < b.original_radius) {b.x=b.original_radius;}if (b.y>sH-b.original_radius) {b.y=sH-b.original_radius;} else if (b.y<b.original_radius) {b.y=b.original_radius;}b.vx = (Math.random()*2-1)*2;b.vy = (Math.random()*2-1)*2;metaballs.push(b);}addChild(bitMap);addEventListener(Event.ENTER_FRAME,enterFrameHandler);}function enterFrameHandler(e:Event):void {for (i=0; i<ballNumber; i++) {var b:Metaball=metaballs[i];b.x+=b.vx;b.y+=b.vy;if (b.x>=sW-b.original_radius) {b.x=sW-b.original_radius;b.vx*=-1;} else if (b.x<b.original_radius) {b.x=b.original_radius;b.vx*=-1;}if (b.y>=sH-b.original_radius) {b.y=sH-b.original_radius;b.vy*=-1;} else if (b.y<b.original_radius) {b.y=b.original_radius;b.vy*=-1;}}canvas.dispose();canvas = new BitmapData(sW,sH,false,0xff000000);canvas.lock();canvas.floodFill(0,0,0);var sum:Number=0;for (var ty:int = 0; ty < stage.stageHeight; ty++) {for (var tx:int = 0; tx < stage.stageWidth; tx++) {sum=0;for (var i:int = 0; i < metaballs.length; i++) {sum+=metaballs[i].equation(tx,ty);}if (! isHollow) {if (sum>=minThreshold) {canvas.setPixel(tx, ty, 0xFFFFFF);}} else {if (sum>=minThreshold&&sum<=maxThreshold) {canvas.setPixel(tx, ty, 0xFFFFFF);}}}}canvas.applyFilter(canvas,rect,pt,blurFilter);canvas.unlock();bitMap.bitmapData=canvas;}init();
大概原理就是根據公式遍曆舞台上的每個像素點,得到一個計算值,如果該值在指定的閾值之間,就設定為白色。
空心Metaball:
線上示範
實心Metaball:
線上示範
正如大家所看到的,效果雖然不錯,但是運行效率也是極低的,因為要逐像素處理。
如何提高效能?這個就得藉助上一篇裡剛學過的PixelBender,原文的作者已經幫我們寫了一個PixdelBender的指令碼:
<languageVersion : 1.0;>kernel Metaballs< namespace : "com.rocketmandevelopment";vendor : "Rocketman Development";version : 1;description : "Fast Metaballs";>{parameter float minThreshold<minValue:float(0.0);maxValue:float(2.0);defaultValue:float(0.9);description: "minThreshold";>;//no max threshold because I want these balls completely filledparameter float3 ball1<//this is where it gets odd. Pixel bender with flash doesn't support loops, so you must pass in each ball individually. This limits the amount of balls to the amount defined in the filterminValue:float3(0.0,0.0,0.0); //storing the data is also odd. We'll use a float3, which is like an array of 3 float values.maxValue:float3(640.0,480.0,50.0); //this first is the x, second is y, and the third is radius.defaultValue:float3(50.0,50.0,20.0);description: "ball1, params, x,y,radius";>;parameter float3 ball2<minValue:float3(0.0,0.0,0.0);maxValue:float3(640.0,480.0,50.0);//this example only supports two ballsdefaultValue:float3(100.0,100.0,20.0);description: "ball2, params, x,y,radius";>;input image4 src;output pixel4 dst;voidevaluatePixel(){dst = sampleNearest(src,outCoord());dst.rbg = float3(0,0,0);//sets the current pixel to black so the image is cleared before redrawingfloat2 coord = outCoord(); //get the coordinate of the pixelfloat sum = 0.0; //get the sum and set it to 0sum += (ball1.z)/((ball1.x-coord.x)*(ball1.x-coord.x)+(ball1.y-coord.y)*(ball1.y-coord.y)); //add to the sum using the formula from the first examplesum += (ball2.z)/((ball2.x-coord.x)*(ball2.x-coord.x)+(ball2.y-coord.y)*(ball2.y-coord.y));if(sum >= minThreshold){dst.rgb = float3(255,255,255); //set it to black if its within the threshold}}}
藉助於PixelBender Toolkit可以將它匯出為flash所需要的二進位檔案metall.pbj,然後在Flash中測試一把:
package {import flash.display.Sprite;import flash.events.Event;import flash.filters.BlurFilter;import flash.filters.ShaderFilter;import flash.utils.ByteArray;import flash.display.Bitmap;import flash.display.BitmapData;import flash.display.Shader;import flash.display.ShaderPrecision;[SWF(height=300,width=300)]public class PixelBenderMetaballs extends Sprite {[Embed(source='metaball.pbj',mimeType='application/octet-stream')]private static const MetaballsFilter:Class;private const canvas:BitmapData=new BitmapData(300,300,false,0);private const blur:BlurFilter=new BlurFilter(10,10,1);private var metaballsFilter:ShaderFilter;private var b1:Metaball;private var b2:Metaball;private var b:Bitmap;public function PixelBenderMetaballs() {b=new Bitmap(canvas);b.smoothing=true;addChild(b);var ba:ByteArray = new MetaballsFilter() as ByteArray;var s:Shader=new Shader(ba);metaballsFilter=new ShaderFilter(s);metaballsFilter.shader.data.src.image=canvas;b1=new Metaball(100,100,Math.random()*20 + 30);b2=new Metaball(150,150,Math.random()*20 + 30);b1.vx = (Math.random()*2-1)*5;b1.vy = (Math.random()*2-1)*5;b2.vx = (Math.random()*2-1)*5;b2.vy = (Math.random()*2-1)*5;metaballsFilter.shader.data.ball1.value=[b1.x,b1.y,b1.radius];metaballsFilter.shader.data.ball2.value=[b2.x,b2.y,b2.radius];metaballsFilter.shader.precisionHint=ShaderPrecision.FAST;b.filters=[metaballsFilter];addEventListener(Event.ENTER_FRAME, enterFrameHandler);}private function enterFrameHandler(e:Event):void {b1.x+=b1.vx;b1.y+=b1.vy;checkWalls(b1);b2.x += b2.vx;b2.y += b2.vy;checkWalls(b2);metaballsFilter.shader.data.ball1.value=[b1.x,b1.y,b1.radius];metaballsFilter.shader.data.ball2.value=[b2.x,b2.y,b2.radius];b.filters=[metaballsFilter,blur];}private function checkWalls(ball:Metaball):void {var sw:Number=stage.stageWidth;var sh:Number=stage.stageHeight;var adjust:uint=15;if (ball.x>sw-ball.original_radius-adjust) {ball.x=sw-ball.original_radius-adjust;ball.vx*=-1;} else if (ball.x < ball.original_radius + adjust) {ball.x=ball.original_radius+adjust;ball.vx*=-1;}if (ball.y>sh-ball.original_radius-adjust) {ball.y=sh-ball.original_radius-adjust;ball.vy*=-1;} else if (ball.y<ball.original_radius+adjust) {ball.y=ball.original_radius+adjust;ball.vy*=-1;}}}}
線上示範
很明顯,現在看上去流暢多了。
上面提到的都是極其精確的標準做法,如果要求不高,其實這種效果可以直接用Bitmap + 模糊濾鏡來模似(不過看上去效果有點假),大概原理就直接把二個圓形進行重疊,然後把最終的(並集)圖形邊緣模糊處理。(該方法是從一位老外的部落格上看到的)
var ballNum:uint=5;var balls:Array = new Array();var sW:Number=stage.stageWidth;var sH:Number=stage.stageHeight;var container:Sprite = new Sprite();var bmd:BitmapData=new BitmapData(sW,sH,false,0x00000000);var bitmap:Bitmap;var i:uint=0;var rect:Rectangle=new Rectangle(0,0,sW,sH);var pt:Point=new Point(0,0);var filter:BlurFilter=new BlurFilter(10,10);function init() {for (i=0; i<ballNum; i++) {var b:Ball=new Ball(15+Math.random()*20,0xffffff);balls.push(b);b.x = (sW - b.width)*Math.random() + b.radius;b.y = (sH - b.width)*Math.random() + b.radius;b.vx=(Math.random()*2-1)*1;b.vy=(Math.random()*2-1)*1;container.addChild(b);}bmd.draw(container);bmd.applyFilter(bmd, rect, pt, filter);bitmap=new Bitmap(bmd);addChild(bitmap);addEventListener(Event.ENTER_FRAME,enterFrameHandler);}function enterFrameHandler(e:Event):void {for (i=0; i<ballNum; i++) {var b:Ball=balls[i];b.x+=b.vx;b.y+=b.vy;var adjust:uint=5;if (b.x>=sW-b.radius-adjust) {b.x=sW-b.radius-adjust;b.vx*=-1;} else if (b.x<b.radius+adjust) {b.x=b.radius+adjust;b.vx*=-1;}if (b.y>=sH-b.radius-adjust) {b.y=sH-b.radius-adjust;b.vy*=-1;} else if (b.y<b.radius+adjust) {b.y=b.radius+adjust;b.vy*=-1;}}bmd.dispose();bmd=new BitmapData(sW,sH,false,0x00000000);bmd.draw(container);bmd.applyFilter(bmd, rect, pt, filter);bitmap.bitmapData=bmd;}init();
線上示範
文中所用原始碼下載:http://cid-2959920b8267aaca.office.live.com/self.aspx/Flash/metaball.rar