Task #15: Playing with Fire

In racking my brain for Tasks, I realized that we’ve done water a few times, but no fire yet. So let’s make Fire the theme for this one.

What, the sun is a big ball of fire? And we did that… Oh yeah… But…

There are a handful of good patterns in the pattern library: Fireflies, and fireworks, and sparkfire… And even a 1D fire …

But a 2D fire? Maybe a fireplace? Or a burning log? Or a burning tree? Or a Burning Man?

Show us some fire!

1 Like

Early again with this one – it took way less time than I’d estimated. Because almost everybody else out there uses Perlin noise for this sort of thing, I gave myself an artificial constraint: No gradient noise of any flavor. This one is entirely convolution based. It has a lot in common with the “raindrops” pattern from a few weeks ago.

The video shows both of its operating modes: “dragon’s breath” and “normal”. It also has controllable color, also shown in the video. The diffuser is a piece of blank printer paper – fire effects look best with a little diffusion, and most of the fastled fire I found used paper too. (it actually looks pretty good even without a diffuser, and of course, better IRL than in the video…)

Code is hidden below the video. I’ll upload to the library tomorrow after I’ve added a bit more polish.

Source Code
/* DOOM Fire

 2D Fire effect, with "enhanced" dragon's breath mode. The method is inspired by the low-res 
 fire in the prehistoric PSX port of DOOM!  It uses no Perlin or other gradient, value or
 fractal noise.
 Details: https://fabiensanglard.net/doom_fire_psx/
 
 Requires a 2D display and an appropriate mapping function.

 MIT License
 
 Version  Author        Date      
 1.0.0    JEM(ZRanger1) 05/13/2021
*/ 

// display size - enter the dimensions of your matrix here
var width = 16;
var height = 16;

// array is sized one row larger than display so we can permanently
// store the "source" fire in the last row.
var arrayWidth = width + 2;
var arrayHeight = height + 1;

// Global variables for rendering
var buffer1 = array(arrayWidth);   // main drawing surface
var buffer2 = array(arrayWidth);   // secondary drawing surface
var pb1, pb2;                      // buffer pointers for swapping

var baseHue = 0;
var baseBri = 0.6;
export var maxCooling = 0.34;  // how quickly flames die down  
var dragonMode = 0;     // 0: plain old fire, 1: dragon's breath
var breathTimer;        // dragon's breath cycle time
var wind = 0;           // variable indicating direction of wind.
var frameTimer = 9999;  // accumulator for simulation timer
var perturb = perturbNormal;  // pointer to fn that plays with fire

// UI
export function hsvPickerHue(h,s,v) {
  baseHue = h;
  baseBri = v;
}

export function sliderFlameHeight(v) {
  maxCooling = 0.25+((1-v) * 0.2)
}

export function sliderDragonMode(v) {
  dragonMode = (v > 0.5);
  
  if (dragonMode) {
    perturb = perturbDragonBreath;
  } else {
    initBuffers();
    perturb = perturbNormal;
  }
}

// create 2 x 2D buffers for calculation, and one
// to hold our background image.
function allocateFrameBuffers() {
  for (var i = 0; i < arrayWidth; i ++) {
    buffer1[i] = array(arrayHeight);
    buffer2[i] = array(arrayHeight);
  }
  pb1 = buffer1;
  pb2 = buffer2;
}

function initBuffers() {
  for (var i = 0; i < arrayWidth; i ++) {
    pb1[i][arrayHeight - 1] = 1;
    pb2[i][arrayHeight - 1] = 1;    
  }
}

function perturbDragonBreath() {
 for (var i = 0; i < arrayWidth; i ++) {
   pb2[i][arrayHeight - 1] = breathTimer+wave(-.21+(i/arrayWidth));
  }
}

// change the base heat in a slow wave
function perturbNormal() {
 for (var i = 0; i < arrayWidth; i ++) {
   pb2[i][arrayHeight - 1] = 0.8+wave(triangle(time(0.3))+(i/arrayWidth))/3;
  }
}

// change wind direction occasionally, always with a short reset to
// zero wind between changes, to give us the look of periodic gusts.
function getWindDirection(w) {
  if (random(1) < 0.333) {
    return (w != 0) ? 0 : random(3) - 1;
  }
}

function swapBuffers()  {
  var tmp = pb1; pb1 = pb2; pb2 = tmp;
}


// Fire is hottest at the bottom, and "cools" as it rises. Each pixel
// calculates it's value based on the one below it, with allowance for
// the current wind direction.
function doFire() {
  swapBuffers();
  
  wind = getWindDirection(wind);

  for (var x = 1; x < width+1; x++) {
    // weight wind effect -- high towards outside, low at center.
    var c = (1-abs((x / width + 1) - 0.5)) * wind;
    
    // cooling effect decreases with height, so very hot particles
    // that don't cool early on get "carried" farther.  It just looks better.
    for (var y = 1; y < arrayHeight-1; y++) {
      var r = (maxCooling * random(1)) * (y/(arrayHeight-1));
      pb2[x+c][y] = max(0,pb1[x][y+1] - r);
    }
  }  
}

// Initialization
allocateFrameBuffers();
initBuffers()

export function beforeRender(delta) {
  frameTimer += delta;
  breathTimer = wave(time(0.1));

  if (frameTimer > 60) {
    doFire();  
    perturb();

    frameTimer = 0;
  }
}

export function render2D(index, x, y) {
  x = 1+floor(x * width);  
  y = floor(y * height);
  bri = pb2[x][y]; bri = bri * bri * bri;
  hsv(baseHue+((0.05*bri)), 1.3-bri/4,bri * baseBri);
}
5 Likes

Another amazing pattern.

Let’s see Fire (above), Earth (flowers), Water (rainfall, also Oasis) and Air (solar flares, also counts as Fire). You’ve got the Elements prize for sure. You’re on a roll!

Wow. Seriously amazing!

Ha! If I’m up for the Elements prize, you should definitely get “Polar Explorer”!

1 Like

Super cool @zranger1, looks awesome!

When I run it on my 24x24 panel I noticed that it ran much smoother if I changed the frame timer stuff so that it didn’t skip as many frames. Maybe based on panel size, where things would move too quickly for smaller panels? Getting like 44 FPS too!

You’re right, @wizard. With patterns like this, I should probably scale the simulation frame timing based on delta so it scales with panel size. It’s got a definite frame rate sweet spot in terms of appearance. Too fast is just as weird looking as too slow. (I think I can optimize the row/column evaluation order more, to skip some regions that are just going to be all dark, and squeeze a few more fps out of it for bigger panels.)

Just uploaded Doom Fire.epe to the library. The library version has a few minor optimizations, and most notably, a “speed” slider which lets you control the frame rate of the simulation.

The simulation frame rate is (mostly) independent of the Pixelblaze’s output frame rate. By default, the simulation runs at about 16fps, which looks good on my 16x16 matrix. (The Pixelblaze is rendering at 77 fps). As @wizard pointed out, on larger displays, the overall frame rate may drop enough that you’ll get a better look by slowing the simulation down a bit. If you like, you can speed the simulation up too. The extremes get silly, but there’s a wide range of good looking fire speeds in there…

3 Likes