Multiphase flow(little optimization + bitmap filter)
So played around with code to optimize it + played with adding effects to it.
With optimization biggest problem is finding neighbors. It seems to take something like 70-90% of frame code execution time and there is not so much to optimize.
Also played with some combinations of filters to achieve metaballs like effect but ended up using just bevel, cheap and does not look so bad :)
/**
* Copyright wonderwhyer ( http://wonderfl.net/user/wonderwhyer )
* MIT License ( http://www.opensource.org/licenses/mit-license.php )
* Downloaded from: http://wonderfl.net/c/kx0I
*/
package {
import flash.utils.*;
import flash.text.*;
import flash.filters.*;
import flash.geom.*;
import flash.events.*;
import flash.display.*;
[SWF(frameRate = "60",backgroundColor = "0")]
public class Fluid extends Sprite {
public static const GRAVITY:Number = 0.05;
public static const RANGE:Number = 8;//Radius of influence
public static const RANGE2:Number = RANGE * RANGE;
public static const DENSITY:Number = 1.8;//fluid baeline density
public static const PRESSURE:Number = 1;//Pressure coefficient
public static const PRESSURE_NEAR:Number = 1;//Close-pressure coefficient
public static const VISCOSITY:Number = 0.1;//Viscosity
public static var NUM_GRIDS_X:int;//number of grid cells by x
public static var NUM_GRIDS_Y:int;//number of grid cells by y
public static var NUM_GRIDS:int;//number of grid cells
public static var GRID_CELL_SIZE:Number = 9;//aproximate grid cell size, will be rounded to match size of the box
public static var INV_GRID_SIZE:Number;
public static var BOX_SIZE_X:Number;
public static var BOX_SIZE_Y:Number;
public var bevel:BevelFilter = new BevelFilter(4,45,0xFFFFFF,0.7,0,0.7,6,6,2,1,"inner",false);
private var particles:Vector.<Particle>;
private var numParticles:uint;
private var neighbors:Vector.<Neighbor>;
private var numNeighbors:uint;
private var count:int;
private var press:Boolean;
private var bitmap:BitmapData;
private var view:Bitmap;
private var grids:Vector.<Grid>;
private const COLOR_TRANSFORM:ColorTransform = new ColorTransform(0.5, 0.5, 0.5);
public var particleCount:TextField;
public var lastMouseX:Number = stage.mouseX;
public var lastMouseY:Number = stage.mouseY;
public var fps:FPS;
public function Fluid() {
initialize();
}
private function initialize():void {
stage.doubleClickEnabled = true;
particles = new Vector.<Particle>();
numParticles = 0;
neighbors = new Vector.<Neighbor>();
numNeighbors = 0;
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;
view = addChild(new Bitmap()) as Bitmap;
view.scaleX = view.scaleY = 2;
initGrid();
count = 0;
addEventListener(Event.ENTER_FRAME, frame);
stage.addEventListener(MouseEvent.MOUSE_DOWN, function(e:MouseEvent):void {press = true;});
stage.addEventListener(MouseEvent.MOUSE_UP, function(e:MouseEvent):void {press = false;});
stage.addEventListener(MouseEvent.DOUBLE_CLICK,function(e:MouseEvent):void {
particles.length=0;
numParticles=0;
});
stage.addEventListener(Event.RESIZE,function(e:Event):void{
initGrid();
});
particleCount = new TextField();
particleCount.textColor=0xffffff;
particleCount.x = 100;
particleCount.y = 50;
addChild(particleCount);
fps = new FPS();
addChild(fps);
fps.textColor="0xFF";
fps.fpsColor="0xFF0000";
fps.graphWidth=100;
fps.graphHeight=50;
fps.init();
fps.graphics.beginFill(0x0,0.5);
fps.graphics.drawRect(0,0,200,110);
}
private function initGrid():void
{
var min:Number = Math.min(stage.stageWidth,stage.stageHeight)
BOX_SIZE_X = stage.stageWidth/2;
BOX_SIZE_Y = stage.stageHeight/2;
NUM_GRIDS = Math.round(min/GRID_CELL_SIZE);
GRID_CELL_SIZE = min / NUM_GRIDS;
INV_GRID_SIZE = 1/GRID_CELL_SIZE;
NUM_GRIDS_X = Math.floor(BOX_SIZE_X/GRID_CELL_SIZE);
NUM_GRIDS_Y = Math.floor(BOX_SIZE_Y/GRID_CELL_SIZE);
NUM_GRIDS = NUM_GRIDS_X*NUM_GRIDS_Y;
BOX_SIZE_X = NUM_GRIDS_X*GRID_CELL_SIZE;
BOX_SIZE_Y = NUM_GRIDS_Y*GRID_CELL_SIZE;
grids = new Vector.<Grid>(NUM_GRIDS, true);
for(var i:int = 0; i < NUM_GRIDS; i++) {
grids[i] = new Grid();
}
view.bitmapData = bitmap = new BitmapData(BOX_SIZE_X, BOX_SIZE_Y, true, 0);
view.x = (stage.stageWidth-view.width)*0.5;
view.y = (stage.stageHeight-view.height)*0.5;
}
private function frame(e:Event):void {
fps.clear();
fps.startCounting("Whole Frame");
fps.startCounting("pour");
if(press)
pour();
fps.stopCounting("pour");
lastMouseX = view.mouseX;
lastMouseY = view.mouseY;
move();
particleCount.text = particles.length.toString();
fps.stopCounting("Whole Frame");
}
private function pour():void {
var dx:Number = view.mouseX-lastMouseX;
var dy:Number = view.mouseY-lastMouseY;
var l:Number = Math.sqrt(dx*dx+dy*dy);
if(dx==0 && dy==0) l=1;
var ang:Number = Math.atan2(dx,dy);
for(var i:int = -4; i <= 4; i++) {
particles[numParticles++] = new Particle(view.mouseX+Math.random()*50-25, view.mouseY+Math.random()*50-25,
count / 10 % 5);
const p:Particle = particles[numParticles - 1];
p.vy = 5*dy/l;
p.vx = 5*dx/l;
}
}
private function move():void {
count++;
bitmap.lock();
BOX_SIZE_Y
fps.startCounting("clear screen");
bitmap.fillRect(bitmap.rect,0);
fps.stopCounting("clear screen");
fps.startCounting("updateGrid+findNeighbours");
updateGrids();
fps.stopCounting("updateGrid+findNeighbours");
fps.startCounting("filters");
bitmap.applyFilter(bitmap,bitmap.rect,bitmap.rect.topLeft,bevel);
fps.stopCounting("filters");
bitmap.unlock();
fps.startCounting("calcForce");
calcForce();
fps.stopCounting("calcForce");
}
private function updateGrids():void {
var i:uint;
var j:uint;
numNeighbors = 0;
for(i = 0; i < NUM_GRIDS; i++)
grids[i].numParticles = 0;
for(i = 0; i < numParticles; i++) {
const p:Particle = particles[i];
p.move();
//bitmap.fillRect(new Rectangle(p.x - 1, p.y - 1, 3, 3), p.color);
bitmap.fillRect(new Rectangle(p.x-2,p.y-2,5,5),p.color);
//.setPixel(p.x,p.y,p.color);
p.fx = p.fy = p.density = p.densityNear = 0;
p.gx = p.x * INV_GRID_SIZE; // which grid cell this particle is in
p.gy = p.y * INV_GRID_SIZE;
var xMin:uint;// = p.gx != 0?p.gx - 1:p.gx;
var xMax:uint;// = p.gx != NUM_GRIDS_X - 1?p.gx + 2:p.gx+1;
var yMin:uint;// = (p.gy != 0?p.gy - 1:p.gy)*Fluid.NUM_GRIDS_X;
var yMax:uint;// = (p.gy != NUM_GRIDS_Y - 1?p.gy + 2:p.gy+1)*Fluid.NUM_GRIDS_X;
if(p.gx < 1) {
xMin = p.gx = 0;
} else {
xMin = p.gx - 1;
}
if(p.gy < 1) {
yMin = p.gy = 0;
} else {
yMin = (p.gy - 1)*NUM_GRIDS_X;
}
if(p.gx > NUM_GRIDS_X - 2){
p.gx = NUM_GRIDS_X - 1;
xMax = NUM_GRIDS_X;
}else{
xMax = p.gx+2;
}
if(p.gy > NUM_GRIDS_Y - 2){
p.gy = NUM_GRIDS_Y - 1;
yMax = NUM_GRIDS_Y*NUM_GRIDS_X;
}else{
yMax = (p.gy+2)*NUM_GRIDS_X;
}
// if(p.gy > NUM_GRIDS_Y - 1)
// p.gy = NUM_GRIDS_Y - 1;
p.gi = p.gy * Fluid.NUM_GRIDS_X+p.gx;
var currentGrid:Grid = grids[p.gi];
for(var gx:uint = xMin;gx<xMax;gx++)
{
for(var gy:uint = yMin;gy<yMax;gy+=Fluid.NUM_GRIDS_X)
{
var g:Grid = grids[gx+gy];
for(j = 0; j < g.numParticles; j++) {
var pj:Particle = g.particles[j];
var nx:Number = p.x-pj.x;
var ny:Number = p.y-pj.y;
var distance:Number = nx*nx+ny*ny;
if(distance < RANGE2) {
if(neighbors.length == numNeighbors)
neighbors[numNeighbors] = new Neighbor();
var neighbour:Neighbor = neighbors[numNeighbors++];
neighbour.setParticle(p, pj,nx,ny,distance);
}
}
}
}
currentGrid.add(p);
}
}
private function calcForce():void {
for(var i:uint = 0; i < numNeighbors; i++)
neighbors[i].calcForce();
}
}
}
import flash.display.Sprite;
import flash.events.Event;
import flash.text.TextField;
import flash.display.BitmapData;
import flash.geom.Matrix;
import flash.display.Graphics;
import flash.display.Bitmap;
import flash.text.TextFormat;
import flash.utils.getTimer;
import flash.system.System;
import flash.geom.Point;
class Neighbor {
public var p1:Particle;
public var p2:Particle;
public var distance:Number;
public var nx:Number;
public var ny:Number;
public var weight:Number;
public static const RANGE:Number = Fluid.RANGE;
public static const INV_RANGE:Number = 1/RANGE;
public static const PRESSURE:Number = Fluid.PRESSURE;
public static const PRESSURE_NEAR:Number = Fluid.PRESSURE_NEAR;
public static const DENSITY:Number = Fluid.DENSITY;
public static const VISCOSITY:Number = Fluid.VISCOSITY;
public function Neighbor() {
}
public function setParticle(p1:Particle, p2:Particle,nx:Number,ny:Number,distance2:Number):void {
this.distance = distance;
this.p1 = p1;
this.p2 = p2;
this.nx = nx;
this.ny = ny;
distance = Math.sqrt(distance2);
weight = 1 - distance *INV_RANGE;
var density:Number = weight * weight; // pressure force
p1.density += density;
p2.density += density;
density *= weight * PRESSURE_NEAR;
p1.densityNear += density;
p2.densityNear += density;
const invDistance:Number = 1 / distance;
this.nx *= invDistance;
this.ny *= invDistance;
}
public function calcForce():void {
var p:Number;
if(p1.type != p2.type)
p = (p1.density + p2.density - DENSITY * 1.2) * PRESSURE;
else
p = (p1.density + p2.density - DENSITY * 2) * PRESSURE;
const pn:Number = (p1.densityNear + p2.densityNear) * PRESSURE_NEAR;
var pressureWeight:Number = weight * (p + weight * pn);
var viscosityWeight:Number = weight * VISCOSITY;
var fx:Number = nx * pressureWeight;
var fy:Number = ny * pressureWeight;
fx += (p2.vx - p1.vx) * viscosityWeight;
fy += (p2.vy - p1.vy) * viscosityWeight;
p1.fx += fx;
p1.fy += fy;
p2.fx -= fx;
p2.fy -= fy;
}
}
class Particle {
public var x:Number;
public var y:Number;
public var gx:int;
public var gy:int;
public var gi:int;
public var vx:Number;
public var vy:Number;
public var fx:Number;
public var fy:Number;
public var density:Number;
public var densityNear:Number;
public var color:uint;
public var type:int;
public const GRAVITY:Number = Fluid.GRAVITY;
public function Particle(x:Number, y:Number, type:int) {
this.x = x;
this.y = y
this.type = type;
vx = vy = fx = fy = 0;
switch(type) {
case 0:
color = 0xff6060ff;
break;
case 1:
color = 0xffff6000;
break;
case 2:
color = 0xffff0060;
break;
case 3:
color = 0xff00d060;
break;
case 4:
color = 0xffd0d000;
break;
}
}
public function move():void {
vy += GRAVITY;
if(density > 0) {
vx += fx / (density * 0.9 + 0.1); //
vy += fy / (density * 0.9 + 0.1); //
}
x += vx;
y += vy;
if(x < 5)
vx += (5 - x- vx) * 0.5 ;
if(x > Fluid.BOX_SIZE_X-5)
vx += (Fluid.BOX_SIZE_X - 5 - x - vx) * 0.5;
if(y < 5)
vy += (5 - y - vy) * 0.5;
if(y > Fluid.BOX_SIZE_Y-5)
vy += (Fluid.BOX_SIZE_Y-5 - y - vy) * 0.5;
}
}
class Grid {
public var particles:Vector.<Particle>;
public var numParticles:uint;
public function Grid() {
particles = new Vector.<Particle>;
}
public function add(p:Particle):void {
particles[numParticles++] = p;
}
}
class FPS extends Sprite {
public var fps:TextField;
public var ms:TextField;
public var mem:TextField;
public var output:TextField;
public var startPoints:Object;
public var collectedTime:Object;
public var lastTime:Object;
public var graphColors:Object;
public var textColor:String="0xFF";
public var fpsColor:String="0xFF0000";
public var graphWidth:uint=200;
public var graphHeight:uint=50;
public var maxFPS:uint;
public var col:int;
public var iFPScolor:uint;
public var fpsMultiply:Number;
public var countersMultiply:Number;
public var tt:int;
public var pt:int;
public var bmd:BitmapData;
public var bmp:Bitmap;
public var m:Matrix;
public var tmp:BitmapData;
public var sp:Sprite;
public var g:Graphics;
public var lfp:Number;
public function FPS() {}
public function init():void {
mouseChildren=false;
fps = new TextField();
fps.width=55;
ms = new TextField();
ms.width=35;
ms.x=55;
mem = new TextField();
mem.width=80;
mem.x=90;
output = new TextField();
output.width=170;
maxFPS=stage.frameRate;
col=int(textColor);
iFPScolor=uint(fpsColor);
fpsMultiply = graphHeight/(maxFPS*1.2);
countersMultiply=maxFPS*graphHeight/1050;
startPoints={};
collectedTime={};
graphColors={};
lastTime={};
tt=pt=getTimer();
var tf:TextFormat = new TextFormat(null,12,0xFF);
fps.defaultTextFormat = tf;
ms.defaultTextFormat = tf;
mem.defaultTextFormat = tf;
output.defaultTextFormat = tf;
output.x=0;
output.autoSize=flash.text.TextFieldAutoSize.LEFT;
output.y=20+graphHeight+10;
output.selectable=true;
ms.selectable=true;
mem.selectable=true;
pt=tt;
bmd=new BitmapData(graphWidth,graphHeight,true,0);
bmp=new Bitmap(bmd);
addChild(bmp);
bmp.y=20;
m = new Matrix();
m.tx=-1;
tmp=new BitmapData(bmd.width,bmd.height,true,0);
sp = new Sprite();
g=sp.graphics;
lfp=0;
addEventListener(Event.ENTER_FRAME,frame);
addChild(fps);
addChild(ms);
addChild(mem);
addChild(output);
}
public function setValue(name:String,val:Number):void {
if (! startPoints.hasOwnProperty(name)) {
startPoints[name]=0;
collectedTime[name]=0;
lastTime[name]=0;
}
startPoints[name] = val;
collectedTime[name] = val;
}
public function startCounting(name:String):void {
if (! startPoints.hasOwnProperty(name)) {
startPoints[name]=0;
collectedTime[name]=0;
lastTime[name]=0;
}
startPoints[name]=getTimer();
}
public function stopCounting(name:String):void {
var t:int=getTimer();
collectedTime[name]+=getTimer()-startPoints[name];
startPoints[name]=getTimer();
//trace(collectedTime[name]);
}
public function getData(name:String):Number {
return collectedTime[name];
}
public function clear():void {
for (var name:String in collectedTime) {
lastTime[name]=collectedTime[name];
collectedTime[name]=0;
}
}
public function addToGraph(name:String,color:uint):void {
if (graphColors==null) {
init();
}
graphColors[name]=color;
}
public function removeFromGraph(name:String,color:uint):void {
if (graphColors.hasOwnProperty(name)) {
graphColors[name]=null;
}
}
public function frame(evt:Event):void {
tt=getTimer();
var dt:int=tt-pt;
var fp:Number=Math.round((10000/dt))/10;
fps.text="FPS: "+fp.toString()+" /";
ms.text=(dt).toString()+" ms";
mem.text="mem: "+Math.round(100*System.totalMemory/1048576)/100+" Mb";
output.text="";
for (var name:String in lastTime) {
output.appendText(name+" : "+lastTime[name]+"\n");
}
tmp.fillRect(tmp.rect,0);
tmp.draw(bmd,m);
bmd.copyPixels(tmp,tmp.rect,new Point());
g.clear();
g.lineStyle(0,iFPScolor);
g.moveTo(bmd.width-2,bmd.height-lfp*fpsMultiply);
g.lineTo(bmd.width-1,bmd.height-fp*fpsMultiply);
pt=tt;
lfp=fp;
for (name in graphColors) {
if (graphColors[name]!=null) {
g.lineStyle(0,graphColors[name]);
g.moveTo(bmd.width-2,bmd.height-lastTime[name]*countersMultiply);
g.lineTo(bmd.width-1,bmd.height-collectedTime[name]*countersMultiply);
}
}
bmd.draw(sp);
}
}