Hello all!
This post is no more that a little optimization of Burn it! post, where we show the 90’s popular effect of Fire rising up. This optimization it’s no more than work only with 256 colors (so reducing all to bytes), not use the zoom feature, and work directly with arrays & in unsafe memory context. As well, we use 8bpp indexed format for the generated bitmap, so this way, the array we use to calculate (Fore array) will be copied directly to bitmap, and as well, we will copy palette generated to that bitmap. This way it’s the way we realize this effect at famous mode 0x13, being video memory indexed at this resolution same way that it’s this bitmap. As well, due this bitmap format it’s smaller (got 256 x 3 bytes for palette, and then width*height bytes for each indexed pixel, instead of the width*height*3 bytes of another formats) transmission from server to client browser it’s faster as well. As told in first post, the fact to use server to generate the images it’s only due teaching & showing purposes for the effect itself: performance it’s not the best. The best way it’s that this effect to be generated directly in client’s side, using Javascript. This will be covered when possible in a new post about. As well, you can try this effect in desktop applications with almost no modifications, when you can observe then the more fast movement.
Please check out as well monogame version for a desktop app
Source code (about the same but with little modifications) are at the end of this post.
So, here it is FireEffect.aspx.cs
public partial class FireEffect : System.Web.UI.Page
{
private const int _WIDTH = 100; // 'heat' array size
private const int _HEIGHT = 300; //
private const int _FIREBASELENGTH = 5; // base line length
private const int _MAXCOLORS = 255; // Maximum palette colors
private static Bitmap BShow = new Bitmap( _WIDTH,
_HEIGHT,
System.Drawing.Imaging.PixelFormat.Format8bppIndexed); // projection bitmap
private static byte[] Back = new byte[_WIDTH*_HEIGHT];
private static byte[] Fore = new byte[_WIDTH*_HEIGHT];
private static Random r = new Random (); // Working random object
private static Color[] Palette = new Color[_MAXCOLORS];
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);
DumpPalette ();
}
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);
DumpPalette ();
}
private void DumpPalette()
{
ColorPalette P = BShow.Palette;
for (int i = 0; i < _MAXCOLORS; i++)
P.Entries [i] = Palette[i];
BShow.Palette = P;
}
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)*_WIDTH] + Back [x + (y - 1)*_WIDTH] + Back [x + 1 + (y - 1)*_WIDTH] +
Back [x - 1 + y*_WIDTH] + /* (x,y) */ Back [x + 1 + y*_WIDTH] +
Back [x - 1 + (y + 1)*_WIDTH] + Back [x + (y + 1)*_WIDTH] + Back [x + 1 + (y + 1)*_WIDTH])
>> 3;
// ...and we put it in the upper pixel. And then, fire grows...
Fore [x + (y - 1)*_WIDTH] = (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; y++)
for (x = 0; x < _WIDTH; x ++)
{
if (x % _FIREBASELENGTH == 0)
c = (byte)(Back [x + y*_WIDTH] + r.Next () % _MAXCOLORS);
Back [x + y*_WIDTH] = (byte)(c % _MAXCOLORS - 1);
}
}
protected void DumpToBitmap(Bitmap BShow)
{
Rectangle R = new Rectangle (0, 0, _WIDTH, _HEIGHT);
BitmapData b = BShow.LockBits(R,ImageLockMode.WriteOnly,System.Drawing.Imaging.PixelFormat.Format8bppIndexed);
IntPtr ptr = b.Scan0;
Marshal.Copy (Fore, 0, ptr, _WIDTH * _HEIGHT);
BShow.UnlockBits (b);
// 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);
}
protected void Timer_Tick(object sender, EventArgs e)
{
// Counter
Stopwatch sw = Stopwatch.StartNew();
// Burn step
Burn ();
sw.Stop ();
// Dump it to bitmap
DumpToBitmap (BShow);
// 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 here, monogame version of it :
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
namespace FireEffect
{
public class Game1 : Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
private const int _WIDTH = 300; // 'heat' array size
private const int _HEIGHT = 350; //
private const int _FIREBASELENGTH = 5; // base line length
private const int _MAXCOLORS = 255; // Maximum palette colors
private byte[] Back = new byte[_WIDTH*_HEIGHT];
private byte[] Fore = new byte[_WIDTH*_HEIGHT];
private Int32 [] w = new Int32[_WIDTH*_HEIGHT];
private static Random r = new Random (); // Working random object
private static System.Drawing.Color[] Palette = new System.Drawing.Color[_MAXCOLORS];
Texture2D texture = null;
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] =
System.Drawing.Color.FromArgb (
(byte)(ro + i*dr),
(byte)(go + i*dg),
(byte)(bo + i*db)
);
}
protected void FillGradients(System.Drawing.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] = System.Drawing.Color.Black;
// 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)
{
System.Drawing.Color[] C = new System.Drawing.Color[6];
for (int i = 0; i < C.Length; i++)
C[i] = System.Drawing.Color.FromArgb (r.Next () % 0xff, r.Next () % 0xff, r.Next () % 0xff);
FillGradients (C);
}
private void SwapRB()
{
System.Drawing.Color c;
for (int i = 0; i < Palette.Length; i++)
{
c = Palette [i];
Palette [i] = System.Drawing.Color.FromArgb (c.B, c.G, c.R);
}
}
public void btDefaultPalette_Click (object sender, EventArgs args)
{
System.Drawing.Color[] C = new System.Drawing.Color[6];
C [0] = System.Drawing.Color.Navy;
C [1] = System.Drawing.Color.OrangeRed;
C [2] = System.Drawing.Color.Orange;
C [3] = System.Drawing.Color.Yellow;
C [4] = System.Drawing.Color.White;
C [5] = System.Drawing.Color.White;
FillGradients (C);
SwapRB ();
}
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)*_WIDTH] + Back [x + (y - 1)*_WIDTH] + Back [x + 1 + (y - 1)*_WIDTH] +
Back [x - 1 + y*_WIDTH] + /* (x,y) */ Back [x + 1 + y*_WIDTH] +
Back [x - 1 + (y + 1)*_WIDTH] + Back [x + (y + 1)*_WIDTH] + Back [x + 1 + (y + 1)*_WIDTH])
>> 3;
// ...and we put it in the upper pixel. And then, fire grows...
Fore [x + (y - 1)*_WIDTH] = (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; y++)
for (x = 0; x < _WIDTH; x ++)
{
if (x % _FIREBASELENGTH == 0)
c = (byte)(Back [x + y*_WIDTH] + r.Next () % _MAXCOLORS);
Back [x + y*_WIDTH] = (byte)(c % _MAXCOLORS - 1);
}
}
protected void DumpToBitmap()
{
int x, y;
System.Drawing.Color c;
for (x = 0; x < _WIDTH; x++)
for (y = 0; y < _HEIGHT; y++)
{
c = Palette [Fore [x + y * _WIDTH]];
w [x + y * _WIDTH] = c.ToArgb();
}
texture.SetData<Int32> (w);
Array.Copy(Fore,Back,_WIDTH * _HEIGHT);
}
public Game1 ()
{
graphics = new GraphicsDeviceManager (this);
graphics.PreferredBackBufferWidth = _WIDTH;
graphics.PreferredBackBufferHeight = _HEIGHT;
graphics.IsFullScreen = false;
graphics.ApplyChanges ();
Content.RootDirectory = "Content";
}
protected override void Initialize ()
{
base.Initialize ();
texture = new Texture2D (graphics.GraphicsDevice, _WIDTH, _HEIGHT);
btDefaultPalette_Click (null, null);
}
protected override void LoadContent ()
{
spriteBatch = new SpriteBatch (GraphicsDevice);
}
protected override void Update (GameTime gameTime)
{
if (Keyboard.GetState ().IsKeyDown (Keys.Escape)) {
Exit ();
}
if (Keyboard.GetState().IsKeyDown(Keys.Space))
{
graphics.PreferredBackBufferHeight = _HEIGHT;
graphics.PreferredBackBufferWidth = _WIDTH;
graphics.ApplyChanges();
}
if (Keyboard.GetState ().IsKeyDown (Keys.R))
btRandomPalette_Click (null, null);
if (Keyboard.GetState ().IsKeyDown (Keys.D))
btDefaultPalette_Click (null, null);
base.Update (gameTime);
Burn ();
DumpToBitmap ();
FireBase ();
}
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw (GameTime gameTime)
{
base.Draw (gameTime);
spriteBatch.Begin ();
spriteBatch.Draw(texture, Vector2.Zero, Microsoft.Xna.Framework.Color.White);
spriteBatch.End ();
}
}
}
Hope you like it!
Best,