Hello all!
Today we’ll going to re-create in c# a very famous effect in the early 90’s: the Fire Effect. It’s logic was very simple but the whole effect was so impressive, and with a very big bunch of possible modifications that makes it so more amazing. Basically, it runs through a whole sprite, and put the average of the color of the surrounding pixels in the upper one, so this way, color decreases in intensity and as well fire rises up.
We will cover then in this post :
- Web’s auto update, using asp:Timer object
- Generate palettes with gradient colors
- The Fire effect itself
- The use of class LockBitmap, improving performance
So, first of all, will define the whole process in detail: we will generate a ‘heat’ array. In this array, we will put, in it’s base, some random values, from maximum heat to minimum possible. Then, at each cycle, we’ll recalculate the next value of an (x,y) point, that will be the average of the point just down of it, that’s it, the (x,y+1) point. To do this avoiding values override, we will use a ‘Back’ or work array as well. Once it is done, we will project it to a bitmap, representing each heat value with a color, that we will pickup from a color table or palette. For a nice representation, colors in this table will have smooth changes between them, so we will realize gradients between pivot colors.
Then, knowing what we must do, let’s go. Let’s create the palette & color degrading function.
private const int _MAXCOLORS = 255; // Maximum palette colors
private static Color[] Palette = new Color[_MAXCOLORS]; // Palette with the degraded colors
Here it is the degrading function: we get the distance between the 2 indexes, and then calculate each color component’s step or differential, to final color (Palette[af]) from initial one (Palette[ao]). We generate then each intermediate color with the proportional part of each component, depending on the distance from the ao initial index.
protected void Gradient (int ao, int af)
{
int d = af - ao; // total distance between colors
float
ro = Palette [ao].R, // Initial r,g,b values
go = Palette [ao].G,
bo = Palette [ao].B,
dr = (Palette[af].R - ro)/d, // diferential of r,g,b
dg = (Palette[af].G - go)/d,
db = (Palette[af].B - bo)/d;
// lets fill each color in palette between range
for (int i=0;i<d+1;i++)
Palette [i + ao] =
Color.FromArgb (
(byte)(ro + i*dr),
(byte)(go + i*dg),
(byte)(bo + i*db)
);
}
Once got the degrading function, let’s fill whole palette, applying some pivot colors and as well, controlling that final color (heat=index=0) will be background’s color.
protected void FillGradients(Color[] C)
{
// counting the back color, 0 indexed, there will be C.Length + 1 color turns.
int m = _MAXCOLORS / (C.Length+1);
// first color (last one, 0, 'coldest' color) will be back page color
Palette[0] = ColorTranslator.FromHtml(PageBody.Attributes["bgcolor"]);
// Lets point each choosen color at position and do gradient between them
for (int i = 1; i < C.Length+1; i++)
{
Palette [i * m] = C [i-1];
Gradient ((i - 1) * m, i * m);
}
}
Perfect! We got right now a way to fill in palette with smoothed colors. Let’s put a pair of buttons, one to generate a random palette, and the other to reestablish a default one.
public void btRandomPalette_Click (object sender, EventArgs args)
{
Color[] C = new Color[6];
for (int i = 0; i < C.Length; i++)
C[i] = Color.FromArgb (r.Next () % 0xff, r.Next () % 0xff, r.Next () % 0xff);
FillGradients (C);
Timer_Tick (null, null);
}
public void btDefaultPalette_Click (object sender, EventArgs args)
{
Color[] C = new Color[6];
C [0] = Color.Navy;
C [1] = Color.OrangeRed;
C [2] = Color.Orange;
C [3] = Color.Yellow;
C [4] = Color.White;
C [5] = Color.White;
FillGradients (C);
Timer_Tick (null, null);
}
Ok. We got know the way to represent each point to a color. Let’s realize the effect itself.
We need, as told, a fore or projection array with the values, that will be the one’s projected to the bitmap, and so on, represented on the web. We need, as well, a same-sized back or work array where to calculate each (x,y) value without loosing existing ones. Note the use of base type byte. We will explain at post’s final the motive of it.
So,
private const int _WIDTH = 100; // 'heat' array size
private const int _HEIGHT = 300; //
private static byte[,] // Working heat arrays
Back = new byte[_WIDTH,_HEIGHT],
Fore = new byte[_WIDTH,_HEIGHT];
Now, let’s burn the base. For a nice effect, we will draw horizontal lines with random colors in the 2 last lines. As well, let’s create a general random object that can be used along the class.
private const int _FIREBASELENGTH = 5; // base line length
private static Random r = new Random (); // Working random object
protected void FireBase()
{
byte c = (byte)(_MAXCOLORS-1);
int x, y;
// Lets do, at image base, some random color lines
for (y = _HEIGHT - 3; y < _HEIGHT - 1; y++)
for (x = 0; x < _WIDTH - 1; x ++)
{
if (x % _FIREBASELENGTH == 0)
c = (byte)(Back [x, y] + r.Next () % _MAXCOLORS);
Back [x, y] = (byte)(c % _MAXCOLORS - 1);
}
}
Got it. Now, we must burn that. As told, we’re going to put in (x,y) the average value of the surrounding pixels of (x,y+1), that’s it, the next downwards.To not lose values, we will use the swap array as destination of the calculus.
Note that we’ll not exhaust limits. We can do it, realizing special calculus in limits, but this way we achieve directly a flame form.
protected void Burn()
{
int x, y;
int c;
for (y = 1; y < _HEIGHT - 1; y++)
for (x = 1; x < _WIDTH - 1; x++)
{
// Get all surrounding color indexs and do mean...
c = (Back [x - 1, y - 1] + Back [x, y - 1] + Back [x + 1, y - 1] +
Back [x - 1, y] + /* (x,y) */ Back [x + 1, y] +
Back [x - 1, y + 1] + Back [x, y + 1] + Back [x + 1, y + 1])
>> 3;
// ...and we put it in the upper pixel. And then, fire grows...
Fore [x, y - 1] = (byte)c;
}
}
Now, let’s project to bitmap. For this, to improve performance, we will use LockBitmap class, from Vado Maisuradze, at www.codeproject.com. With this class we work directly on bitmap’s array, in an unsafe context, avoiding the memory pointer’s protection by default on C#, and so, with fastest results.
Due the small size of current resolution pixels, we’ll add an scale factor. Projection is as easy as go through each fore array pixel, and put palette’s color by fore (x,y) value as index. At end, we put all fore info in back array, preparing next cycle.
protected void DumpToBitmap()
{
int x, y, zx, zy=0;
LockBitmap B = new LockBitmap (BShow);
B.LockBits();
// Go through Fore array and project about to BShow bitmap
for (x = 0; x < _WIDTH; x++)
for (y = 0; y < _HEIGHT; y++)
for (zx = 0; zx < _ZOOM; zx++)
for (zy = 0; zy < _ZOOM; zy++)
B.SetPixel (x * _ZOOM + zx, y * _ZOOM + zy, Palette [Fore [x, y]]);
B.UnlockBits ();
// When Fore array its all projected to bitmap, lets copy it as well to back,
// as source for next Burn
Array.Copy (Fore, Back, _WIDTH * _HEIGHT * sizeof(byte));
}
And that’s a whole cycle! Due this effect need motion, we will achieve it using an ASP Timer object. This is, but, a heavy duty for the server (in examples site there’s an interval of 50ms, that means that page will partial reload from server each 20 part of a second. In next posts, we will try to optimize this effect realizing all heavy operations in clients side via Javascript, but in this example, due the main reason it’s to realize the effect, will keep tasks on server side). Here you can found a monogame version.
Here it is, then, the Timer tick event:
protected void Timer_Tick(object sender, EventArgs e)
{
// Counter
Stopwatch sw = Stopwatch.StartNew();
// Burn step
Burn ();
sw.Stop ();
// Dump it to bitmap
DumpToBitmap ();
// Put fuel
FireBase ();
InfoLabel.Text = "Bitmap generated on " + sw.ElapsedMilliseconds.ToString () + "ms";
// Dump to a string
sw.Start ();
using (MemoryStream m = new MemoryStream ())
{
BShow.Save (m, System.Drawing.Imaging.ImageFormat.Jpeg);
UImage.ImageUrl = "data:image/jpeg;base64," + Convert.ToBase64String (m.ToArray ());
}
sw.Stop ();
InfoLabel.Text += " / total ellapsed time : " + sw.ElapsedMilliseconds.ToString() + "ms";
}
And that’s really all! In this effect, for optimization reasons, both work arrays base type is byte, and so, _MAXCOLORS constant it’s maximum byte value 0xff. You can, but, use base value as int and whatever palette size you want to. Note that in original effect all was byte (unsigned char, unsigned char *) arrays, that got a working time really faster than using ints. In famous mode 0x13, as each byte was mapped to a color, you use directly screen memory 0xA0000 as in this example the Fore array.
There are so many variations of this effect: for example, you can include a random behaviour in average calculation, to make heat of each pixel to rise or to go down in a more randomly manner. As well, you can put average calculation to another pixel than (x,y-1), for example, some lines with (x+1,y-1), anothers with (x-1,y-1). This way, you can simulate ‘wind’. Playing with the location of the calculus you can, as well, realize two sided fire, randomly particles, etc…Another variation its the fire base : you can draw a burning circle, or, for a really amazing effect, you can put text as base, making burning letters, etc…
You can check live this example here
Hope you like it!
Best,
Attached whole source code
LockBitmap.cs
LockBitmap class, from Vado Maisuradze, at www.codeproject.com
FireEffect.aspx.cs
using System.Diagnostics;
namespace OnlineRepository
{
public partial class FireEffect : System.Web.UI.Page
{
private const int _WIDTH = 100; // 'heat' array size
private const int _HEIGHT = 300; //
private const int _ZOOM = 1; // Projection zoom
private const int _FIREBASELENGTH = 5; // base line length
private const int _MAXCOLORS = 255; // Maximum palette colors
private static Color[] Palette = new Color[_MAXCOLORS]; // Palette with the degraded colors
private static byte[,] // Working heat arrays
Back = new byte[_WIDTH,_HEIGHT],
Fore = new byte[_WIDTH,_HEIGHT];
private static Random r = new Random (); // Working random object
private static Bitmap BShow = new Bitmap(_WIDTH*_ZOOM,_HEIGHT*_ZOOM); // projection bitmap
public void Page_Load(object sender, EventArgs args)
{
// First time page load, lets generate a palette & put fire on base
if (!Page.IsPostBack)
{
btRandomPalette_Click (null, null);
FireBase ();
}
}
protected void Gradient (int ao, int af)
{
int d = af - ao; // total distance between colors
float
ro = Palette [ao].R, // Initial r,g,b values
go = Palette [ao].G,
bo = Palette [ao].B,
dr = (Palette[af].R - ro)/d, // diferential of r,g,b
dg = (Palette[af].G - go)/d,
db = (Palette[af].B - bo)/d;
// lets fill each color in palette between range
for (int i=0;i<d+1;i++)
Palette [i + ao] =
Color.FromArgb (
(byte)(ro + i*dr),
(byte)(go + i*dg),
(byte)(bo + i*db)
);
}
protected void FillGradients(Color[] C)
{
// counting the back color, 0 indexed, there will be C.Length + 1 color turns.
int m = _MAXCOLORS / (C.Length+1);
// first color (last one, 0, 'coldest' color) will be back page color
Palette[0] = ColorTranslator.FromHtml(PageBody.Attributes["bgcolor"]);
// Lets point each choosen color at position and do gradient between them
for (int i = 1; i < C.Length+1; i++)
{
Palette [i * m] = C [i-1];
Gradient ((i - 1) * m, i * m);
}
}
public void btRandomPalette_Click (object sender, EventArgs args)
{
Color[] C = new Color[6];
for (int i = 0; i < C.Length; i++)
C[i] = Color.FromArgb (r.Next () % 0xff, r.Next () % 0xff, r.Next () % 0xff);
FillGradients (C);
Timer_Tick (null, null);
}
public void btDefaultPalette_Click (object sender, EventArgs args)
{
Color[] C = new Color[6];
C [0] = Color.Navy;
C [1] = Color.OrangeRed;
C [2] = Color.Orange;
C [3] = Color.Yellow;
C [4] = Color.White;
C [5] = Color.White;
FillGradients (C);
Timer_Tick (null, null);
}
protected void Burn()
{
int x, y;
int c;
for (y = 1; y < _HEIGHT - 1; y++)
for (x = 1; x < _WIDTH - 1; x++)
{
// Get all surrounding color indexs and do mean...
c = (Back [x - 1, y - 1] + Back [x, y - 1] + Back [x + 1, y - 1] +
Back [x - 1, y] + /* (x,y) */ Back [x + 1, y] +
Back [x - 1, y + 1] + Back [x, y + 1] + Back [x + 1, y + 1])
>> 3;
// ...and we put it in the upper pixel. And then, fire grows...
Fore [x, y - 1] = (byte)c;
}
}
protected void FireBase()
{
byte c = (byte)(_MAXCOLORS-1);
int x, y;
// Lets do, at image base, some random color lines
for (y = _HEIGHT - 3; y < _HEIGHT - 1; y++)
for (x = 0; x < _WIDTH - 1; x ++)
{
if (x % _FIREBASELENGTH == 0)
c = (byte)(Back [x, y] + r.Next () % _MAXCOLORS);
Back [x, y] = (byte)(c % _MAXCOLORS - 1);
}
}
protected void DumpToBitmap()
{
int x, y, zx, zy=0;
LockBitmap B = new LockBitmap (BShow);
B.LockBits();
// Go through Fore array and project about to BShow bitmap
for (x = 0; x < _WIDTH; x++)
for (y = 0; y < _HEIGHT; y++)
for (zx = 0; zx < _ZOOM; zx++)
for (zy = 0; zy < _ZOOM; zy++)
B.SetPixel (x * _ZOOM + zx, y * _ZOOM + zy, Palette [Fore [x, y]]);
B.UnlockBits ();
// When Fore array its all projected to bitmap, lets copy it as well to back,
// as source for next Burn
Array.Copy (Fore, Back, _WIDTH * _HEIGHT * sizeof(byte));
}
protected void Timer_Tick(object sender, EventArgs e)
{
// Counter
Stopwatch sw = Stopwatch.StartNew();
// Burn step
Burn ();
sw.Stop ();
// Dump it to bitmap
DumpToBitmap ();
// Put fuel
FireBase ();
InfoLabel.Text = "Bitmap generated on " + sw.ElapsedMilliseconds.ToString () + "ms";
// Dump to a string
sw.Start ();
using (MemoryStream m = new MemoryStream ())
{
BShow.Save (m, System.Drawing.Imaging.ImageFormat.Jpeg);
UImage.ImageUrl = "data:image/jpeg;base64," + Convert.ToBase64String (m.ToArray ());
}
sw.Stop ();
InfoLabel.Text += " / total ellapsed time : " + sw.ElapsedMilliseconds.ToString() + "ms";
}
}
}
FireEffect.aspx
<%@ Page Language="C#" Inherits="OnlineRepository.FireEffect" %>
<!DOCTYPE html>
<html>
<head runat="server">
<title>Fire</title>
<style type="text/css">
p{text-align:center;font-variants: small-caps; font-name=Lucida Console; color:white;}
</style>
</head>
<body id="PageBody" bgcolor="#1A0900" runat="server">
<form id="form1" runat="server">
<asp:ScriptManager runat="server" id="ScriptManager1">
</asp:ScriptManager>
<asp:UpdatePanel runat="server" id="UpdatePanel1">
<ContentTemplate>
<p>
<asp:Timer runat="server" id="Timer" Interval="10" OnTick="Timer_Tick"></asp:Timer>
<asp:Button id="btRandomPalette" runat="server" Text="Random palette" OnClick="btRandomPalette_Click" Width="150" /><br/>
<asp:Button id="btDefaultPalette" runat="server" Text="Default palette" OnClick="btDefaultPalette_Click" Width="150" /><br/><br/>
<asp:Label runat="server" Text="Any image yet generated" id="InfoLabel" font-size="small" font-name="verdana" ></asp:Label><br/>
<asp:Image runat="server" id="UImage" ImageAlign="AbsMiddle"/>
</p>
</ContentTemplate>
</asp:UpdatePanel>
</form>
</body>
</html>
2 thoughts on “Burn it!”