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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏游戏杂谈

as3绘制抛物线

一般做页游的过程中,特效的释放可能是不是固定位置的播放,是需要进行“运动的”(其实就是移动特效这个影响剪辑)。举个例子:步兵射箭,不确定箭发射的方向,事先也不...

11220
来自专栏walterlv - 吕毅的博客

用动画的方式画出任意的路径(直线、曲线、折现)

发布于 2017-11-20 00:49 更新于 2017-11...

10820
来自专栏HT

基于HTML5的3D网络拓扑树呈现

在HT for Web中2D和3D应用都支持树状结构数据的展示,展现效果各异,2D上的树状结构在展现层级关系明显,但是如果数据量大的话,看起来就没那么直观,找到...

281100
来自专栏JarvanMo的IT专栏

浅扒Android动态设置字体大小

Android开发中,TextView类的控件应该说是很常用了。一般来说我们是通过android:textSize="20sp" 来设置字体大小,但是很多时候...

15730
来自专栏GIS讲堂

Arcgis for JS之Cluster聚类分析的实现

在做项目的时候,碰见了这样一个问题:给地图上标注点对象,数据是从数据库来的,包含XY坐标信息的,通过graphic和graphiclayer 的方式添加到地图上...

21730
来自专栏hightopo

基于HT for Web的3D树的实现

11520
来自专栏数据科学学习手札

(数据科学学习手札42)folium进阶内容介绍

  在上一篇(数据科学学习手札41)中我们了解了folium的基础内容,实际上folium在地理信息可视化上的真正过人之处在于其绘制图像的高度可定制化上,本文就...

32840
来自专栏游戏开发那些事

【python游戏编程之旅】第四篇---pygame中加载位图与常用的数学函数。

本系列博客介绍以python+pygame库进行小游戏的开发。有写的不对之处还望各位海涵。

14820
来自专栏华章科技

技巧:Excel用得好,天天没烦恼

分析公司DarkHorse Analytics 从美国劳工统计处获得数据,并制作了这张二十四小时会唿吸的地图,显示曼哈顿的工作与在宅人口。

12340
来自专栏数据小魔方

一款脑洞大开的表格可视化神器

今天跟大家介绍一款任坤大神写的新包——formattable。 这个包的功能很简单,但是却很具创意性,它颠覆了R语言data.frame数据表的呈现方式,允许在...

51980

扫码关注云+社区

领取腾讯云代金券