If you have WS2812 or similar programmable RGB leds, and a microcontroller with
N×
M×2+768 bytes of RAM, you can do some really nice fire and plasma effects.
The idea is that you use a good pseudorandom number generator as a seed in suitable cells. I recommend using the 32 or 40 high bits of Xorshift64*, e.g.
#include <stdint.h>
// Any nonzero 64-bit seed will work; randomize it!
uint64_t prng_state = 1;
// Cache for 8-bit pseudorandom numbers.
uint8_t prng_cache[4];
uint8_t prng_cached = 0;
// Obtain a random number between 0 and 255.
uint8_t prng_u8(void)
{
if (prng_cached > 0)
return prng_cache[--prng_cached];
uint64_t x = prng_state;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
prng_state = x;
x = (x * UINT64_C(2685821657736338717)) >> 24;
prng_cache[0] = x;
prng_cache[1] = x >> 8;
prng_cache[2] = x >> 16;
prng_cache[3] = x >> 24;
prng_cached = 4;
return x >> 32;
}
You declare two buffers and an RGB look-up table:
#define N //rows, vertical
#define M //columns, horizontal
typedef struct {
uint8_t cell [N][M];
} grid;
grid g[2];
uint8_t rgb[256][3];
For each inner cell, you apply a 3×3 kernel, centered on the cell, with the weights summing to 257 (or less if you want dimming).
const uint8_t kernel[3][3] = { ... };
void apply(grid *const dst, grid *const src, const uint8_t flicker, const uint8_t bias)
{
const uint_fast16_t cmin = (uint_fast16_t)flicker * (uint_fast16_t)bias;
const uint_fast16_t cmax = (uint16_t)( -(uint_fast16_t)flicker * (uint_fast16_t)(255 - bias) );
for (uint_fast8_t n = 1; n < N-1; n++) {
for (uint_fast8_t m = 1; m < M-1; m++) {
uint16_t c = (uint_fast16_t)(kernel[ 0 ][ 0 ]) * (uint_fast16_t)(src->cell[ n-1 ][ m-1 ])
+ (uint_fast16_t)(kernel[ 0 ][ 1 ]) * (uint_fast16_t)(src->cell[ n-1 ][ m ])
+ (uint_fast16_t)(kernel[ 0 ][ 2 ]) * (uint_fast16_t)(src->cell[ n-1 ][ m+1 ])
+ (uint_fast16_t)(kernel[ 1 ][ 0 ]) * (uint_fast16_t)(src->cell[ n ][ m-1 ])
+ (uint_fast16_t)(kernel[ 1 ][ 1 ]) * (uint_fast16_t)(src->cell[ n ][ m ])
+ (uint_fast16_t)(kernel[ 1 ][ 2 ]) * (uint_fast16_t)(src->cell[ n ][ m+1 ])
+ (uint_fast16_t)(kernel[ 2 ][ 0 ]) * (uint_fast16_t)(src->cell[ n+1 ][ m-1 ])
+ (uint_fast16_t)(kernel[ 2 ][ 1 ]) * (uint_fast16_t)(src->cell[ n+1 ][ m ])
+ (uint_fast16_t)(kernel[ 2 ][ 2 ]) * (uint_fast16_t)(src->cell[ n+1 ][ m+1 ])
;
if (c >= cmin && c <= cmax) {
c = c - cmin + prng_u8() * uint_fast16_t)flicker;
}
dst->cell[n][m] = c >> 8;
}
}
}
You need two grids,
grid g, gtemp;Initialize the main one to all zeros, set the seed cell values using the
prng_u8() function, and apply the kernel twice,
apply(gtemp, g, 0, 128); apply(g, gtemp, 0, 128);The flicker term is how much randomness is added to each cell, and bias is whether it tends to increase (<128) or decrease (>128) the cell value.
Now you have the updated state in the main grid, and you can sample specific cells for your RGB LEDs, using the RGB look-up table
rgb[g.cell[row][column]][0..2] for red, green, and blue components for that LED, respectively. You can also do a number of
apply() rounds before updating the LEDs.
My favourite is using a regular triangular grid for the LEDs, with relatively large grids.
If you use a RAM-based
kernel, you can adjust it in real time, skewing it to a side, creating changing "draft". Bias it completely towards one side to get drifting plasma/fire.
The possibilities are very nearly limitless, and it is easy to get going and some nice results, while genuine-looking fire takes careful
tuning and experimentation.