Flash Smoke Effect using DisplacementMapFilter
Inspired by David Lenaerts’ awesome smoke simulation using Alchemy, I wondered if a similar effect – pushing smoke around with the mouse – could be achieved using Flash’s DisplacementMapFilter. As with David’s simulation, click to add smoke and move the mouse to create wind.
There’s a great explanation of how DisplacementMapFilter works over at Emanuele Feronato’s blog, so I won’t repeat it here. The smoke effect is on the left, and on the right is the displacement bitmap that gets applied to the smoke image on each frame. To simulate wind, as the mouse is moved I add or subtract from the red and green channels using ColorTransform. The faster the mouse is moved, the more I add or subtract. To simulate the wind settling down, I apply a BlurFilter and another ColorTransform to cause the displacement map to converge towards 0x80C000 (no horizontal movement, slight upwards movement).
While not nearly as impressive as David’s more realistic simulation, I’m pretty happy with what can be achieved with simple bitmap displacement. I experimented with adding Perlin noise to prevent the smoke from getting too static when the mouse isn’t moving and got some really cool results, but have decided to keep things simple for now. Maybe in a future post.
Here’s the source. It was built in FlexBuilder as an ActionScript project, but wouldn’t be hard to convert to compile in the Flash IDE.
{
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.filters.BlurFilter;
import flash.filters.DisplacementMapFilter;
import flash.geom.ColorTransform;
import flash.geom.Point;
import flash.geom.Rectangle;
[SWF(width='1024', height='512', frameRate='30')]
public class DisplacementSmoke extends Sprite
{
internal static var BITMAP_WIDTH:Number = 512;
internal static var BITMAP_HEIGHT:Number = 512;
internal static var WIND_SIZE:Number = 80;
internal static var SMOKE_SIZE:Number = 10;
internal var smokeBitmap:Bitmap;
internal var displacementBitmap:Bitmap;
internal var drawing:Boolean = false;
internal var xPos:Number;
internal var yPos:Number;
internal var displacementFilter:DisplacementMapFilter;
internal var blurFilter:BlurFilter = new BlurFilter(3, 3, 1);
internal var windColorTransform:ColorTransform;
internal var smokeColorTransform:ColorTransform = new ColorTransform(1, 1, 1, 1, 20, 20, 20);
internal var dampingColorTransform:ColorTransform = new ColorTransform(.97, .96, 0, 1, 5, 9, 0, 0);
internal var heatColorTransform:ColorTransform = new ColorTransform(1, 1, 1, 1, 0, 2, 0, 0);
internal var bitmapRect:Rectangle = new Rectangle(0, 0, BITMAP_WIDTH, BITMAP_HEIGHT);
internal var rect:Rectangle;
internal var p:Point = new Point(0, 0);
public function DisplacementSmoke()
{
smokeBitmap = new Bitmap(new BitmapData(BITMAP_WIDTH, BITMAP_HEIGHT, false, 0x000000));
displacementBitmap = new Bitmap(new BitmapData(BITMAP_WIDTH, BITMAP_HEIGHT, false, 0x80C000));
addChild(smokeBitmap);
addChild(displacementBitmap);
displacementBitmap.x = BITMAP_WIDTH;
stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
displacementFilter = new DisplacementMapFilter(displacementBitmap.bitmapData, p, 1, 2, 5, 5);
smokeBitmap.filters = [displacementFilter];
addEventListener(Event.ENTER_FRAME, enterFrameHandler);
}
internal function mouseDownHandler(event:MouseEvent):void
{
drawing = true;
}
internal function mouseUpHandler(event:MouseEvent):void
{
drawing = false;
}
internal function enterFrameHandler(event:Event):void
{
var i:Number;
var j:Number;
var mx:Number = stage.mouseX;
var my:Number = stage.mouseY;
var dx:Number = mx - xPos;
var dy:Number = my - yPos;
var d:Number = Math.sqrt(dx*dx + dy*dy);
var step:Number = .6 - Math.min(.5, d/100);
var xp:Number;
var yp:Number;
windColorTransform = new ColorTransform(1, 1, 1, 1, -dx*.5, -dy*.5);
for(i=0; i<1; i+=step)
{
xp = xPos + dx*i;
yp = yPos + dy*i;
xp = Math.max(xp, WIND_SIZE/2);
xp = Math.min(xp, BITMAP_WIDTH - WIND_SIZE/2);
yp = Math.max(yp, WIND_SIZE/2);
yp = Math.min(yp, BITMAP_HEIGHT - WIND_SIZE/2);
if(!(xPos==0 && yPos==0))
{
rect = new Rectangle(xp - WIND_SIZE/2 + Math.random()*10-5, yp - WIND_SIZE/2 + Math.random()*10-5, WIND_SIZE, WIND_SIZE);
displacementBitmap.bitmapData.colorTransform(rect, windColorTransform);
if(drawing)
{
displacementBitmap.bitmapData.colorTransform(rect, heatColorTransform);
for(j=0; j<4; j++)
{
rect = new Rectangle(xp - Math.random()*SMOKE_SIZE, yp - Math.random()*SMOKE_SIZE, SMOKE_SIZE, SMOKE_SIZE);
smokeBitmap.bitmapData.colorTransform(rect, smokeColorTransform);
}
}
}
}
displacementBitmap.bitmapData.colorTransform(bitmapRect, dampingColorTransform);
displacementBitmap.bitmapData.applyFilter(displacementBitmap.bitmapData, bitmapRect, p, blurFilter);
xPos = mx;
yPos = my;
smokeBitmap.bitmapData.draw(smokeBitmap);
}
}
}














