Burn it!

Hello all!

Fire5 Fire4 Fire3 Fire2 Fire1

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>
 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s