Processing: Эффект огня 🔥

Классический эффект огня, который появился на заре демосцены.

Это классический эффект огня, который появился на заре демосцены

Сам алгоритм очень простой, по сути это клеточный автомат. Есть поле состоящее из ячеек, двумерный массив. Ячейка может принимать значение от 0 до 255. Чем выше значение, тем больше энергии она содержит.

Каждый кадр в самой нижней строке генерируется последовательность случайных значений. Это источник пламени.

Затем вычисляется энергия каждой ячейки массива: нужно сложить сумму энергий трёх ячеек под вычисляемой и одной ячейки на две строки ниже, а затем разделить это значение на четыре, или немного больше. Например на 4,0625. Чем выше будет это значение, тем быстрее будет затухать пламя. Если разделить на 5, то огонь вообще будет тлеть.

Алгоритм вычисления энергии ячейки

После всех вычислений остаётся вывести массив на экран, присвоив цвет в зависимости от величины энергии ячейки. Это может быть градиент от чёрного через красный и жёлтый к белому.

Для демонстрации алгоритма я выбрал Processing. Он просто-таки создан для таких целей. Код компактный и легко читается. Фактически, современный графический Бэйсик. Скачать Processing можно по ссылке https://processing.org/download.

Скетч реализации на Java для Processing представлен ниже.

int[][] cells;

void setup() {
  size (640, 128);
  colorMode(HSB, 255, 255, 255);
  background(0);
  cells = new int[width][height];
}

void draw() {
  fireGenerate();
  fireUpdate();

  loadPixels();
  for (int y=0; y < height; y++) {
    for (int x=0; x < width; x++) {
      pixels[x+y*width] = color(cells[x][y]/3, 255, min(255, cells[x][y]*2));
    }
  }
  updatePixels();
}

void fireGenerate() {
  for (int x=0; x < width; x++) {
    cells[x][height-1] = (int) random (255);
  }
}

void fireUpdate() {
  for (int y=0; y < height; y++) {

    int row1 = (y+1)%height;
    int row2 = (y+2)%height;

    for (int x=0; x < width; x++) {

      int left = (x-1+width)%width;
      int center = (x)%width;
      int right = (x+1)%width;

      int row1_left = cells[left][row1];
      int row1_center = cells[center][row1];
      int row1_right = cells[right][row1];
      int row2_center = cells[center][row2];

      cells[x][y] = (int)((row1_left+row1_center+row1_right+row2_center) / 4.0625);
    }
  }
}

Метод fireUpdate() можно записать короче, но тогда трудно разобрать, что там происходит. Поэтому я его развернул. Как пример, посмотрите ниже сокращённую запись.

void fireUpdate() {
  for (int y=0; y < height; y++) {
    for (int x=0; x < width; x++) {
      cells[x][y] = (int)((cells[(x-1+width)%width][(y+1)%height]+cells[(x)%width][(y+1)%height]+cells[(x+1)%width][(y+1)%height]+cells[(x)%width][(y+2)%height]) / 4.0625);
    }
  }
}

Как результат, получаем вот такой эффект огня! 🔥

Эффект огня

А вот что будет, если поменять делитель на 4,015625. 🔥

Эффект огня с делителем на 4,015625

Если в генераторе fireGenerate() поднять нижний диапазон случайных значений от 75 то огонь будет ярче. Также можно добавить искры, инициируя случайные ячейки по всему полю, а не только в самом низу.

void fireGenerate() {
  for (int x=0; x < width; x++) {
    cells[x][height-1] = 75 + (int) random (255-75);
  }
  for (int i=0; i < 5; i++) {
    cells[(int)random(0,width-1)][(int)random(0,height-1)] = (int) random (255);
  }
}

И вот результат! 🔥

Эффект огня с искрами

В общем, есть простор для творчества. В дальнейшем я планирую продолжить заметки по ретро-эффектам.