Metaball(元球)效果学习

时间:2022-04-23
本文章向大家介绍Metaball(元球)效果学习,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

几年前就在网上曾看见过这种效果,但一直不知道叫什么名字

前一阵无意在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);//默认生成一个黑背景的BitmapData
var 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 filled
parameter 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 filter
minValue: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 balls
defaultValue:float3(100.0,100.0,20.0);
description: "ball2, params, x,y,radius";
>;

input image4 src;
output pixel4 dst;

void
evaluatePixel()
{
dst = sampleNearest(src,outCoord());
dst.rbg = float3(0,0,0);//sets the current pixel to black so the image is cleared before redrawing
float2 coord = outCoord(); //get the coordinate of the pixel
float sum = 0.0; //get the sum and set it to 0
sum += (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 example
sum += (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