Metaball(元球)效果学习

简介: 几年前就在网上曾看见过这种效果,但一直不知道叫什么名字   前一阵无意在9ria(天地会)论坛上看到了一篇专门讲这个的文章:AS3 元球(Metaball),不过有点遗憾的是那篇文章上的代码直接复制下来都不能调试,花了点时间整理了一下,终于调试通过了,贴在这里分享一下: Metaball的公式: 其中,x、y是舞台上的任意一个点,x0、y0是metaball的位置,R为半径。

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

img_c57e594b0a4f91dfc7dab4b5254214bd.png 

前一阵无意在9ria(天地会)论坛上看到了一篇专门讲这个的文章:AS3 元球(Metaball),不过有点遗憾的是那篇文章上的代码直接复制下来都不能调试,花了点时间整理了一下,终于调试通过了,贴在这里分享一下:

Metaball的公式:

img_ffed4c1f9786cf51d8df70693be0b09a.gif

其中,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:

img_708b0c3dc807a8f842f9243d3d2c53ea.jpg
在线演示

实心Metaball:

img_deb3d81d770274e7efb52b9a9c3c13ea.jpg
在线演示

正如大家所看到的,效果虽然不错,但是运行效率也是极低的,因为要逐像素处理。

如何提高性能?这个就得借助上一篇里刚学过的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;
			}
		}

	}
}

img_d497d97e30d612ab531fb0b316aaf50f.jpg
在线演示

很明显,现在看上去流畅多了。

上面提到的都是极其精确的标准做法,如果要求不高,其实这种效果可以直接用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();

img_41a48bd8a1ca51c972b1d6a012c1c508.jpg
在线演示

文中所用源代码下载:http://cid-2959920b8267aaca.office.live.com/self.aspx/Flash/metaball.rar

目录
相关文章
|
关系型数据库 MySQL
Mysql连接无效(invalid connection)解决方案
Mysql连接无效(invalid connection)解决方案
1774 0
Mysql连接无效(invalid connection)解决方案
|
机器学习/深度学习 人工智能 安全
人工智能浪潮下的隐私保护:挑战与策略
【8月更文挑战第13天】在数字化时代,人工智能技术飞速发展,给人们的生活带来了极大的便利。然而,随之而来的个人隐私泄露问题也日益严重。本文将探讨在AI技术广泛应用的背景下,如何有效保护个人隐私,包括面临的主要挑战和可能的解决策略。
|
数据采集 中间件 调度
Scrapy:高效的网络爬虫框架
Scrapy是Python的网络爬虫框架,用于快速构建和开发爬虫。它提供简单API和全功能环境,包括请求调度、HTML解析、数据存储等,让开发者专注爬虫逻辑。Scrapy工作流程包括发起请求、下载响应、解析数据、处理数据和发送新请求。其核心组件有调度器、下载器、解析器(Spiders)和Item Pipeline,广泛应用于数据挖掘、信息监测、搜索引擎和自动化测试。有效技巧包括合理设置请求参数、编写高效解析器、使用代理和防反爬策略,以及利用中间件。随着大数据和AI的发展,Scrapy在爬虫领域的地位将持续巩固。【6月更文挑战第6天】
481 0
|
Web App开发 监控 前端开发
Web Performance Optimization:前端性能优化全方位指南
【4月更文挑战第6天】本文是关于Web Performance Optimization的指南,重点讲述如何优化前端性能以提升用户体验和网站业务表现。关键性能指标包括First Contentful Paint (FCP)、First Meaningful Paint (FMP)、Largest Contentful Paint (LCP)、First Input Delay (FID)和Cumulative Layout Shift (CLS)。优化策略涉及资源压缩、网络配置、代码架构改进、交互渲染优化及性能监控。
2347 0
|
存储 安全 Java
Session和Cookie区别介绍+面试题
session机制属于B/S结构的一部分,主要的作用就是为了保存会话状态。(用户登录成功后,将用户一直登录的状态保存到会话中)
259 0
|
存储 安全 网络协议
|
数据挖掘 大数据 Linux
使用云服务器学习生物信息学
生物信息学是一门基于Linux的学习方法,通过服务器完成生物大数据的整合计算,从而对生物进行探索。本人是博士在读研究生,硕士期间具备一些生物信息学的研究背景。因为实验室缺乏必要的服务器无法从事生物信息学分析,申请了阿里云的服务器试用,完成一些生物信息学研究。
|
机器学习/深度学习 存储 Cloud Native
如何在工作中快速成长?致工程师的 10 个简单技巧
精英人数的增长速度持续加快后,很多人开始焦虑,我也焦虑,深知要走出焦虑不容易,我想把走出焦虑快速成长的认知和方法写成文章分享给更多人,做成【技术人成长系列】文章给更多人面对面分享,该系列总共三篇,分别是《完成自己的认知升级》、《自我成长的方法》、《学会自我培养或培养他人》。本文是快速成长第一篇:“完成自己的认知升级”,内容偏长但值得仔细阅读。
如何在工作中快速成长?致工程师的 10 个简单技巧
|
Ubuntu 应用服务中间件 开发工具
git学习------&gt;在CenterOS系统上安装GitLab并自定义域名访问GitLab管理页面
目前就职的公司一直使用SVN作为版本管理,现在打算尝试从SVN迁移到Git。安排我来预言并搭建好相关的环境以及自己尝试使用Git。今天我就尝试在Center OS系统上安装GitLab,现在在此记录一下整个安装过程。
3868 0
|
C语言 Linux
生产者消费者问题 伪代码和C语言多线程实现
生产者消费者问题是操作系统中的一个经典的问题。 他描述的是一个,多个生产者与多个消费者共享多个缓冲区的事情,具体的定义百度。 然后看了操作系统的书籍如何解决书上给的伪代码是这样的 item B[k]; semaphore empty; empty...
2916 0