2D Fire Effect in ActionScript 3

I had a little bit of time today to do some me-coding (as opposed to work-coding), so I knocked up a quick 90’s era 2D fire effect. I’d actually written it C++ and OpenGL the other week and was meaning to transfer it into WebGL so it can run live rather than having a captured video, but my WebGL kung-fu is pretty weak at the moment so just to get it done I translated it to Flash.

How it works

The effect itself is incredibly simple, as the video below explains. You randomly add “hot-spots” to the bottom of the pixel array, then the new temperature value for a pixel is just the average of the pixel and the 3 pixels below it with a small amount subtracted so that the flames “cool”, which you then map to a colour gradient.

Sneaky!

Flash implementation

In the OpenGL version I’d made the colours for the flames into a 1D texture which can be easily interpolated, but I had to find some functions to do that in AS3 as there don’t appear to be 1D textures! Also, getting and converting the colours from uints and hex-codes and stuff was a pain – but I finally nailed it after googling around and pulling functions from various places (referenced in the source).

I think the most important thing I learnt from getting this working in Flash was how to and how NOT to convert colours from uints into red/green/blue components. For example, a lot of code online will use a function like the following to convert red/green/blue values into a 24-bit uint:

// NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO!!!!!!!!!
function rgbToUint(red:uint, green:uint, blue:uint):uint
{
	var hex = "0x" + red.toString(16) + green.toString(16) + blue.toString(16);
  	return hex;
}

The above function looks like it works, and in fact in most cases does work – but if you get single digit values they aren’t zero-padded properly and craziness ensues!

The correct way to convert RGB to uint is by using the two functions below which ensure that values are capped AND zero-padded as appropriate, which means that everything works under all possible conditions:

function convertChannelToHexStr(hex:uint):String
{
	if (hex > 255)
	{
		hex = 255;
		//trace("hex val overloaded");
	}
 
	var hexStr:String = hex.toString(16);
 	if (hexStr.length < 2)
	{
		hexStr = "0" + hexStr;
	}
 
	return hexStr;
}
 
function getHexColourFromRGB(r:uint, g:uint, b:uint):uint
{
	var hexStr:String = convertChannelToHexStr(r);
	hexStr           += convertChannelToHexStr(g);
	hexStr           += convertChannelToHexStr(b);
 
	var strLength = hexStr.length;
	if (strLength < 6)
	{
		for (var j:uint; j < (6-strLength); j++)
		{
			hexStr += "0";
		} 
	}
 
	var finalStr = "0x" + hexStr;
	return finalStr;
}

Wrap up

Overall, it’s a nice simple effect – but it’s pretty CPU intensive. In the above example I’m only using a stage size of 250px by 120px and the framerate takes a hit even then.

I could use a single loop instead of embedded x and y loops, and I could manipulate more colours directly as uints rather than pulling out the RGB components, but it’s prolly not going to improve more than about 10-15%. As usual, there’s source code after the jump, so if you have a play with it and manage to speed it up a bunch, feel free to let me know how you did it!

Cheers!

Download Link: 2D-Fire-Effect.fla (Flash CS4 fla – but CS5 and later will convert on load).

Source Code

import flash.display.BitmapData;
 
// Set up BitmapData instances the size of the screen, no transparency and filled with black
var temperatureBMD:BitmapData = new BitmapData(stage.stageWidth, stage.stageHeight, false, 0x000000);
var flameBMD:BitmapData       = new BitmapData(stage.stageWidth, stage.stageHeight, false, 0x000000);
 
// Create Bitmap instances to display the BitmapData visually
var temperatureBM:Bitmap = new Bitmap(temperatureBMD);
var flameBM:Bitmap = new Bitmap(flameBMD);
 
// Add the bitmap for the colour flame drawing to the stage
stage.addChild(flameBM);
 
// Display the controls message
var instructions:ControlsMessage = new ControlsMessage;
instructions.x = stage.stageWidth  / 2;
instructions.y = stage.stageHeight / 2;
stage.addChild(instructions);
 
// Flags to keep track of showing flames or temperature, whether we're pause and whether to display instructions
var showFlames:Boolean          = true;
var displayPaused:Boolean       = true;
var showingInstructions:Boolean = true;
 
// Set up our colour array with the flame colours
var flameColour = new Array(8);
flameColour[0] = getHexColourFromRGB(0,   0,   0);   // Black at top
flameColour[1] = getHexColourFromRGB(31,  31,  31);  // Dark grey
flameColour[2] = getHexColourFromRGB(63,  63,  63);  // Medium grey
flameColour[3] = getHexColourFromRGB(255, 69,  0);   // Orange
flameColour[4] = getHexColourFromRGB(255, 255, 0);   // Yellow
flameColour[5] = getHexColourFromRGB(255, 255, 150); // Less bright yellowy-white
flameColour[6] = getHexColourFromRGB(255, 255, 200); // Bright yellowy-white
flameColour[7] = getHexColourFromRGB(255, 255, 255); // White
 
// How fast the flames cool, higher numbers mean more cooling
var coolingFactor:Number = 2;
 
var theStageHeight:uint = stage.stageHeight - 1;
var theStageWidth:uint  = stage.stageWidth - 1;
 
function getRed(uintColour:uint):Number
{
	return uintColour >> 16 & 0xFF;
}
 
function getGreen(uintColour:uint):Number
{
	return uintColour >> 8 & 0xFF;
}
 
function getBlue(uintColour:uint):Number
{
	return uintColour & 0xFF;
}
 
// Function to interpolate between two colours
// Parameters: First colour, second colour, blending percentage (0 is 0%, 1 is 100%)
// Source: http://www.senocular.com/flash/actionscript/?file=ActionScript_3.0/com/senocular/gyro/Interpolate.as
function interpolateColour(colour1:uint, colour2:uint, t:Number):uint 
{
	var r1:int = (colour1 >> 16) & 0xFF;
	var g1:int = (colour1 >> 8) & 0xFF;
	var b1:int = colour1 & 0xFF;
 
	var r2:int = (colour2 >> 16) & 0xFF;
	var g2:int = (colour2 >> 8) & 0xFF;
	var b2:int = colour2 & 0xFF;
 
	return uint(int(r1+(r2-r1)*t) << 16 | int(g1+(g2-g1)*t) << 8 | int(b1+(b2-b1)*t));
}
 
 
// From: http://www.actionscript-flash-guru.com/blog/36-uint-to-6-digit-rgb-hex-actionscript-30-as3
function convertChannelToHexStr(hex:uint):String
{
	if (hex > 255)
	{
		hex = 255;
		//trace("hex val overloaded");
	}
 
	var hexStr:String = hex.toString(16);
 
	if (hexStr.length < 2){
		hexStr = "0" + hexStr;
	}
 
	// Return the string
	return hexStr;
}
 
// Function to get a uint colour from the red/green/blue components
function getHexColourFromRGB(r:uint, g:uint, b:uint):uint
{
	var hexStr:String = convertChannelToHexStr(r);
	hexStr           += convertChannelToHexStr(g);
	hexStr           += convertChannelToHexStr(b);
 
	var strLength = hexStr.length;
	if (strLength < 6)
	{
		for (var j:uint; j < (6-strLength); j++)
		{
			hexStr += "0";
		} 
	}
 
	var finalStr = "0x" + hexStr;
	return finalStr;
}
 
function flameFunc(xVal:uint, yVal:uint):uint
{
	// Get the pixel values for the current pixel and the three pixels below it
	// We end up with the RGB value as a uint (which sucks, but there we go)
	var temperature:uint   = temperatureBMD.getPixel(xVal,   yVal);
	var blTemperature:uint = temperatureBMD.getPixel(xVal-1, yVal+1); // Bottom-left
	var bTemperature:uint  = temperatureBMD.getPixel(xVal,   yVal+1); // Bottom
	var brTemperature:uint = temperatureBMD.getPixel(xVal+1, yVal+1); // Bottom-right
 
	// Just get the blue component (i.e. the last 16 bits) of each uint colour which
	// we'll use as our temperature
	var blue:uint   = temperature   & 0xFF;
	var blBlue:uint = blTemperature & 0xFF;
	var bBlue:uint  = bTemperature  & 0xFF;
	var brBlue:uint = brTemperature & 0xFF;
 
	// Add up all the colour components
	blue += blBlue + bBlue + brBlue;
 
	// Subtract the cooling factor from the flame if it's more than 1 only (so it the
	// colour doesn't wrap around to white
	if (blue > coolingFactor)
	{
		blue -= coolingFactor
	}
 
	// Divide by 4 to get the average
	blue /= 4;	
 
	// Pass our blue value as all three components to getHexColorFromRGB
	// to get the uint version of that shade of gray
	return getHexColourFromRGB(blue, blue, blue);
}
 
// Function to update each pixel on the stage
function updateStage(e:Event):void
{
	var randVal:Number;	
	var tempColour:uint
 
	// Lock does NOT stop a BitmapData object from being modified - it
	// merely means the modifications are not visible until it's unlocked!
	// For further details see:
	// http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/BitmapData.html#lock%28%29
	temperatureBMD.lock();
	flameBMD.lock();
 
	// Set the new hotspots along the bottom of the stage
	for (var loop:uint = 1; loop < stage.stageWidth-1; loop++)
	{
		randVal = Math.random();
		//randVal = getRandomNumber();
 
		// 1 in 25 chance of hotspot
		if ((randVal > 0.25) && (randVal <= 0.29))
		{
			// White hotspot
			temperatureBMD.setPixel(loop, theStageHeight, 0xFFFFFF);
 
		}
		// 1 in 25 chance of coldspot
		else if ((randVal > 0.54) && (randVal <= 0.58))
		{
			temperatureBMD.setPixel(loop, theStageHeight, 0x000000);
		}
	}	
 
	// Loops to go over every pixel on the stage and calculate new temperature values and hence colours
	for (var yLoop:uint = 1; yLoop < theStageHeight; yLoop++)
	{
		for (var xLoop:uint = 1; xLoop < theStageWidth; xLoop++)
		{
			// Set the new temperature value of the pixel using our flameFunc
			temperatureBMD.setPixel(xLoop, yLoop, flameFunc(xLoop, yLoop));
 
			// Read back what the temperature was
			tempColour = temperatureBMD.getPixel(xLoop, yLoop);
 
			// Just get the blue component (in the range 0 to 255)
			tempColour = getBlue(tempColour);
 
			// Get the temperature as a value between 1 and 32
			var percentageMod32:uint = (tempColour % 32) + 1;
 
			// Turn that value into a percentage for use w/ the interpolation function
			var percentageNum = percentageMod32 / 32.0;
 
			// Set the flame colour from the array of flame colours depending on the current temperature of the pixel
			// 8 colours, 256 brightness values --> so chunked into regions of 32
			// Looks very 8 coloury because, er, yeah... it is.
			/*if       (tempColour < 32)                          { flameBMD.setPixel(xLoop, yLoop, flameColour[0]); }
			else if ((tempColour > 32)  && (tempColour <= 64))  { flameBMD.setPixel(xLoop, yLoop, flameColour[1]); }
			else if ((tempColour > 64)  && (tempColour <= 96))  { flameBMD.setPixel(xLoop, yLoop, flameColour[2]); }
			else if ((tempColour > 96)  && (tempColour <= 128)) { flameBMD.setPixel(xLoop, yLoop, flameColour[3]); }
			else if ((tempColour > 128) && (tempColour <= 160)) { flameBMD.setPixel(xLoop, yLoop, flameColour[4]); }
			else if ((tempColour > 160) && (tempColour <= 192)) { flameBMD.setPixel(xLoop, yLoop, flameColour[5]); }
			else if ((tempColour > 192) && (tempColour <= 224)) { flameBMD.setPixel(xLoop, yLoop, flameColour[6]); }
			else if (tempColour  > 224)                         { flameBMD.setPixel(xLoop, yLoop, flameColour[7]); }*/
 
			// Set the flame colour based on interpolation of the nearest two flame colours! Nicerer! =P
			// 8 colours, 256 brightness values --> so chunked into regions of 32
			if (tempColour < 32)
			{
				flameBMD.setPixel(xLoop, yLoop, flameColour[0]);
			}
			else if ((tempColour >= 32)  && (tempColour < 64))
			{
				flameBMD.setPixel(xLoop, yLoop, interpolateColour(flameColour[0], flameColour[1], percentageNum));
			}
			else if ((tempColour >= 64)  && (tempColour < 96))
			{
				flameBMD.setPixel(xLoop, yLoop, interpolateColour(flameColour[1], flameColour[2], percentageNum));
			}
			else if ((tempColour >= 96)  && (tempColour < 128))
			{
				flameBMD.setPixel(xLoop, yLoop, interpolateColour(flameColour[2], flameColour[3], percentageNum));
			}
			else if ((tempColour >= 128) && (tempColour < 160))
			{
				flameBMD.setPixel(xLoop, yLoop, interpolateColour(flameColour[3], flameColour[4], percentageNum));
			}
			else if ((tempColour >= 160) && (tempColour < 192))
			{
				flameBMD.setPixel(xLoop, yLoop, interpolateColour(flameColour[4], flameColour[5], percentageNum));
			}
			else if ((tempColour >= 192) && (tempColour < 224))
			{
				flameBMD.setPixel(xLoop, yLoop, interpolateColour(flameColour[5], flameColour[6], percentageNum));
			}
			else if (tempColour  >= 224)
			{
				flameBMD.setPixel(xLoop, yLoop, interpolateColour(flameColour[6], flameColour[7], percentageNum));
			}
 
		} // End of xLoop
 
	} // End of yLoop
 
	// Unlock the BMDS so they  can update the display
	flameBMD.unlock();
	temperatureBMD.unlock();
 
} // End of updateScene function
 
// Function to pause and unpause the flames when space is pressed
function togglePause(e:KeyboardEvent)
{
	if (displayPaused == true)
	{
		stage.addEventListener(Event.ENTER_FRAME, updateStage);
		displayPaused = false;
 
		// Remove the instructions and don't show them again
		if (showingInstructions == true)
		{
			stage.removeChild(instructions);
			showingInstructions = false;
		}
	}
	else
	{
		stage.removeEventListener(Event.ENTER_FRAME, updateStage);
		displayPaused = true;
	}
}
 
// Function to toggle between displaying the coloured flames and the greyscale temperature
function toggleDisplay(e:MouseEvent)
{
	if (displayPaused == false)
	{
		if (showFlames == true)
		{
			stage.removeChild(flameBM);
			stage.addChild(temperatureBM);
			showFlames = false;
		}
		else
		{
			stage.addChild(flameBM);
			stage.removeChild(temperatureBM);
			showFlames = true;
		}
	}
}
 
stage.addEventListener(KeyboardEvent.KEY_DOWN, togglePause);
 
stage.addEventListener(MouseEvent.MOUSE_DOWN, toggleDisplay);
 
stop();

3 thoughts on “2D Fire Effect in ActionScript 3”

  1. Nice

    I think a way of improving performance on this code is to try and remove the conditional ‘nest’. I’ve noticed that removing conditionals and simplifying code can speed things up, however doing this will make tweaking things much harder. So, swings/roundabouts and all that

    Anyway, I was thinking something like:

    var interpolateIndex:uint = tempColour / 32;
    if (tempColour < 32)
    {
    	flameBMD.setPixel(xLoop, yLoop, flameColour[0]);
    }
     else
    {
    	flameBMD.setPixel(xLoop, yLoop, interpolateColour(flameColour[interpolateIndex-1], flameColour[interpolateIndex], percentageNum));
    }
  2. Also having these functions may save you time in the future.
    You could generalise it by having source be an int or a String or whatever. Don’t know if this language is OO’esque, but if so then that would be simple.

    function leftPadNum(source:int, pad:String, size:int):String {
       var ret:String = "" + source;
       while (ret.length < size) {
           ret += pad;
       }
       return ret;
    }
     
    public function leftPadStr(source:String, pad:String, size:int):String {
       var ret:String = source;
       while (ret.length < size) {
           ret = pad + ret;
       }
       return ret;
    }
     
    public function rightPad(source:int, pad:String, size:int):String {
       var ret:String = "" + source;
       while (ret.length < size) {
           ret += pad;
       }
       return ret;
    }

    So it could be used by getHexColourFromRGB like…

    leftPadStr(hexStr, "0", 6);

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.