Burn-Console: A Fire-Effect Demo in MSH

Tue, Nov 22, 2005 5-minute read

As a deeper example of interfacing with the MshHostRawUserInterface class, the next few articles will deal with implementing the classic “Fire Effect” seen in many of the demos that I so fondly remember.  As part of the journey, we’ll also explore some of the ways in which you can improve the performance of time-sensitive portions of your script.


[This GIF is animated.  Some Ad blocking software restricts animation]

What is the fire effect?  It’s a relatively simple algorithm to simulate fire – usually in a graphical buffer.  We’re going to do it in the console text buffer.  In the algorithm, you seed the bottom row randomly with hot pixels.  Then, you progress the fire upwards.  As you progress the fire upwards, you cool it off a little bit – and simulate heat dissipation by averaging the heat from nearby buffer cells.

You can find an excellent write-up of the concepts at http://freespace.virgin.net/hugo.elias/models/m_fire.htm

The first problem we run into is properly representing the shades of the fire.  The fire effect traditionally uses 256 shades of red as its palette.  Colour 255 is the hottest, while colour 0 is the coldest.  That’s difficult when your fire-like console palette is restricted to four fire-like colours from [System.Console]::ConsoleColor – Yellow, Red, DarkRed, and Black.  To get around this problem, we’ll dither our colours.  Newspapers use dithering with great success: by carefully controlling the density in which they place dots of ink, they can simulate a passable grayscale.

Similarly, we can generate many new colours using the high ASCII characters ░ (ALT+176), ▒ (ALT+177), ▓ (ALT+178), and █ (ALT+219) to mix foreground and background colours.  For example, a DarkRed foreground and Black background, applied to character ▒ produces a very dark red. 

Now, visual design isn’t my core competency.  I’d be hard pressed to mix these four colours, using only the ratios implicit in the dithering characters, to create a fire-like palette.  So I’ll cheat, and let the computer do it for me.  I’ll assign a number to each colour that represents its luminosity / intensity, let the computer generate all possible mixes of colours and characters, and then have it organize the resulting mixes it order of total visual intensity.

The code below accomplishes that.  You can also download the script: burn-console-1.palette.msh.

###############################################################################
## burn-console.msh
##
## Create a fire effect in MSH, using the Console text buffer as the rendering
## surface.
##
## Great overview of the fire effect algorithm here: 
## http://freespace.virgin.net/hugo.elias/models/m_fire.htm
##
###############################################################################

function main
{
    ## Rather than a simple red fire, we'll introduce oranges and yellows
    ## by including Yellow as one of the base colours
    $colours = "Yellow","Red","DarkRed","Black"
    
    ## The four characters that we use to dither with, along with the 
    ## percentage of the foreground colour that they show
    $dithering = "█","▓","▒","░"
    $ditherFactor = 1,0.75,0.5,0.25
    
    ## Hold the palette.  We actually store each entry as a BufferCell,
    ## since we need to retain a foreground colour, background colour,
    ## and dithering character.
    $palette = `
        @(new-object System.Management.Automation.Host.BufferCell) * 256
    
    ## Generate, then display, the palette
    clear-host
    generatePalette
    displayPalette

    ## Clean up and exit
    $host.UI.RawUI.ForegroundColor = "Gray"
    $host.UI.RawUI.BackgroundColor = "Black"
}

## Generates a palette of 256 colours.  We create every combination of 
## foreground colour, background colour, and dithering character, and then
## order them by their visual intensity.
##
## The visual intensity of a colour can be expressed by the NTSC luminance 
## formula.  That formula depicts the apparent brightness of a colour based on 
## our eyes' sensitivity to different wavelengths that compose that colour.
## http://en.wikipedia.org/wiki/Luminance_%28video%29
function generatePalette
{
    ## The apparent intensities of our four primary colours.
    ## However, the formula under-represents the intensity of our straight
    ## red colour, so we artificially inflate it.
    $luminances = 225.93,106.245,38.272,0
    $apparentBrightnesses = @{}

    ## Cycle through each foreground, background, and dither character
    ## combination.  For each combination, find the apparent intensity of the 
    ## foreground, and the apparent intensity of the background.  Finally,
    ## weight the contribution of each based on how much of each colour the
    ## dithering character shows.
    ## This provides an intensity range between zero and some maximum.
    ## For each apparent intensity, we store the colours and characters
    ## that create that intensity.
    $maxBrightness = 0
    for($fgColour = 0; $fgColour -lt $colours.Count; $fgColour++)
    {
        for($bgColour = 0; $bgColour -lt $colours.Count; $bgColour++)
        {
            for($ditherCharacter = 0; 
                $ditherCharacter -lt $dithering.Count; 
          
      $ditherCharacter++)
            {
                $apparentBrightness = `
                    $luminances[$fgColour] * $ditherFactor[$ditherCharacter] +`
                    $luminances[$bgColour] * 
                        (1 - $ditherFactor[$ditherCharacter])
                    
                if($apparentBrightness -gt $maxBrightness) 
                { 
                    $maxBrightness = $apparentBrightness 
                }
                    
                $apparentBrightnesses[$apparentBrightness] = `
                    "$fgColour$bgColour$ditherCharacter"
            }
       }
    }

    ## Finally, we normalize our computed intesities into a pallete of
    ## 0 to 255.  If a given intensity is 30% towards our maximum intensity,
    ## then it should be in the palette at 30% of index 255.
    $paletteIndex = 0
    foreach($key in ($apparentBrightnesses.Keys | sort))
    {
        $keyValue = $apparentBrightnesses[$key]
        do
        {
            $character = $dithering[[Int32]::Parse($keyValue[2])]
            $fgColour = $colours[[Int32]::Parse($keyValue[0])]
            $bgColour = $colours[[Int32]::Parse($keyValue[1])]
            
            $bufferCell = `
                new-object System.Management.Automation.Host.BufferCell `
                    $character, `
                    $fgColour, `
                    $bgColour, `
                    "Complete"
                    
            $palette[$paletteIndex] = $bufferCell
            $paletteIndex++
        } while(($paletteIndex / 256) -lt ($key / $maxBrightness));
    }
}

## Dump the palette to the screen.
function displayPalette
{
    for($paletteIndex = 254; $paletteIndex -ge 0; $paletteIndex--)
    {
        $bufferCell = $palette[$paletteIndex]
        $fgColor = $bufferCell.ForegroundColor
        $bgColor = $bufferCell.BackgroundColor
        $character = $bufferCell.Character

        $host.UI.RawUI.ForegroundColor = $fgColor
        $host.UI.RawUI.BackgroundColor = $bgColor
        write-host -noNewLine $character
    }
    
    write-host
}

. main

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]