Упрощённый огонь из урока про эффекты я собрал за десять минут и через час выключил. Пламя выглядело как неисправная ёлка: у дна дёргались случайные красно-оранжевые точки, остальная лента тлела угрюмыми угольками, а о живом костре речи не шло. Час я потратил на подбор яркостей, пока не признал: проблема не в ленте и не в моих руках, а в подходе. Я раскрашивал пиксели напрямую, когда настоящий огонь раскрашивает не пиксели, а тепло. В этом уроке разберём Fire2012 — самый известный и самый переписываемый алгоритм огня для FastLED из официальных примеров: массив температур, случайное остывание, подъём жара и вспышки у дна. В симуляторе ниже он крутится на шестнадцати пикселях, в коде — версия для настоящей ленты. И про масштаб сразу: на шестнадцати пикселях огонь выглядит свечой, на шестидесяти — костром, а матрица 16×16 уже даёт пламя с задником. Алгоритм один и тот же, меняется только длина массива.
Модель тепла: красим не пиксели, а градусы
Главная мысль алгоритма: цвет вообще не выбирается. Для каждого пикселя хранится число — температура от 0 (холодно) до 255 (раскалено), — а цвет вычисляется из неё одной функцией: низкая температура даёт чёрный и тёмно-красный, средняя — красный и оранжевый, высокая — жёлтый и почти белый. Массив heat[] живёт отдельным буфером рядом с leds[], как второй экран: вы пересчитываете его каждый кадр, а затем одним циклом переносите в цвет. Такой двухслойный каркас стоит запомнить — он же собирает дым, воду и любую «физику» на ленте. Кадры по-прежнему рисуются по таймеру millis(): мы собирали этот каркас в уроке про millis() вместо delay(), здесь ничего нового не появится.
Шаг первый — остывание. Каждый кадр из температуры каждого пикселя вычитается случайное число. Случайное — не для красоты: при одинаковом остывании фронт пламени ползёт ровной «лесенкой», а живой огонь выедает жар пятнами. Классический пример берёт COOLING = 55 и считает вычитание как random8(0, (55 × 10) / N + 2): для ленты на 60 пикселей это 0–11 за кадр, для моих шестнадцати — 0–36. Формула звучит странно, пока не прочитаешь её вслух: чем короче лента, тем сильнее остывает каждый пиксель за кадр — иначе на коротком куске жар дойдёт до конца и застрянет там навсегда. На шестнадцати пикселях я иногда поднимаю COOLING до 70 — пламя становится ниже и деловитее. Ещё деталь: на плате вычитание пишут в байт через qsub8, а в симуляторе ниже я заменю его на if с проверкой — целочисленный массив честно принимает минус, и это нагляднее для разбора.
| Параметр | По умолчанию | Что происходит при изменении |
|---|---|---|
| COOLING | 55 | Ниже 45 — стена огня на всю ленту, выше 70 — низкий каминный огонёк |
| SPARKING | 84 (шанс из 255) | Ниже 50 — тлеющий уголь, выше 150 — сплошные вспышки |
| Кадр | 20 мс (50 кадров/с) | 40 мс — пламя в замедленной съёмке, для больших лент |
| Зона поджига | нижние 4 пикселя | Больше — огонь «горит» всей нижней половиной |
Шаг второй — подъём: у огня есть направление. Каждая ячейка берёт треть от суммы жара двух ячеек ниже: heat[i] = (heat[i − 1] + heat[i − 2] + heat[i − 2]) / 3. Нижняя соседка входит в формулу с весом ×2 — это гасит рваные фронты, и пламя тянется вверх со скоростью примерно пиксель за кадр. Вес ×2 у heat[i − 2] — не опечатка: (a + 2b) / 3 — старое целочисленное сглаживание, которое существовало до всяких цифровых фильтров; попробуйте убрать второй член, и фронт станет рваным. Проходить цикл надо от вершины к дну, в обратную сторону: если идти снизу вверх, вы запишете в верхние ячейки уже обновлённый жар, и волна полетит вдвое быстрее расчётной. Шаг третий — поджиг: с шансом SPARKING = 84 из 255, то есть примерно каждый третий кадр, в одном из четырёх нижних пикселей температура прыгает на 160–255. Именно вспышки дают характер — без них пламя догорает и превращается в тёплую лампочку.
В полном коде встретятся две незнакомые функции с приставкой q. qsub8(a, b) — вычитание с прижимом к нулю, qadd8(a, b) — сложение с потолком 255. Они нужны, потому что температура хранится в байте: обычное вычитание из нуля перевернёт байт в 255, и холодный пиксель вспыхнет белым посреди тёмной ленты. Поиск таких «случайных белых пикселей» — классический вечер с отладочной печатью, и q-функции его предотвращают бесплатно.
Полный скетч для настоящей ленты
#include <FastLED.h> #define LED_PIN 6#define NUM_LEDS 60#define COOLING 55#define SPARKING 84 CRGB leds[NUM_LEDS];byte heat[NUM_LEDS]; void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(180);} void loop() { // шаг 1: у каждого пикселя свой случайный минус for (int i = 0; i < NUM_LEDS; i++) { heat[i] = qsub8(heat[i], random8(0, ((COOLING * 10) / NUM_LEDS) + 2)); } // шаг 2: жар ползёт вверх, нижняя соседка с весом x2 for (int i = NUM_LEDS - 1; i >= 2; i--) { heat[i] = (heat[i - 1] + heat[i - 2] + heat[i - 2]) / 3; } // шаг 3: вспышка в нижних четырёх пикселях if (random8() < SPARKING) { int y = random8(0, 4); heat[y] = qadd8(heat[y], random8(160, 255)); } // шаг 4: температура в цвет for (int i = 0; i < NUM_LEDS; i++) { leds[i] = HeatColor(heat[i]); } FastLED.show(); delay(20);}Цвет из температуры здесь делает HeatColor(heat[i]) — встроенная функция FastLED, которая натягивает на диапазон 0–255 градиент «чёрный — тёмно-красный — красный — оранжевый — жёлтый — белый». Как она устроена внутри — тема третьего урока этого пакета: это та самая раскладка HeatColors_p, только без ручек настройки. Диапазоны у неё и у жара совпадают один в один: температура 30 — тлеющий тёмно-красный, 130 — красный, 200 — оранжевый, 255 — бело-жёлтый кончик; никаких map() между слоями не нужно. В симуляторе движка нет ни HeatColor, ни random8, поэтому там тот же алгоритм собран на примитивах: случайность берётся из обычного random(), прижимы написаны через if, а цвет считается через CHSV — оттенок 0–31, красно-оранжевый сектор колеса оттенков, и яркость, равная температуре. Пламя в симуляторе выглядит чуть проще, но алгоритм внутри — ровно тот, что в коде выше.
Полезно заранее знать, что отличает Fire2012 от упрощённой версии при взгляде глазами зрителя. Смотрите на пламя как на процесс: жар рождается в нижних пикселях, проходит вверх около пикселя за кадр — при кадре в 20 мс это примерно 50 пикселей в секунду, на метровой ленте в 60 пикселей жар поднимается за секунду с четвертью, — и тает по дороге из-за остывания. Языки получаются сами: вспышки разной силы догоняют друг друга на подъёме, а формула трети стачивает разрывы между ними. В упрощённом огне языков не было — жар там не двигался, а просто гас на месте. Если ваш огонь равномерно рдеет снизу доверху без намёка на языки, поднимите COOLING: жар остывает слишком лениво и застревает, вместо того чтобы выгорать пятнами.
Настройка огня под сцену — это три ручки, и крутить их стоит по очереди, по одной. COOLING — высота пламени: 40–50 даёт стену огня на всю ленту, 70–90 — каминный огонёк, а дальше 100 я не заходил — пламя вырождается в красные искры у дна. SPARKING — характер: ниже 50 огонь тлеет ровно и скучно, 84 — классический костёр, 150 и больше — вспышки летят постоянно. Задержка кадра управляет темпом жизни: 20 мс — живой огонь, 40 мс — пламя замедленной съёмки, на больших лентах так хочется разглядывать языки. Мои рабочие пресеты: «камин» — COOLING 70, SPARKING 60, кадр 25 мс, пламя невысокое и редкие вспышки; «пожар» — COOLING 45, SPARKING 150, всё летает. И про питание: огонь почти никогда не заливает ленту белым, поэтому реальный ток заметно ниже худшего случая в 60 мА на пиксель, о котором писал урок про питание ленты — но блок питания всё равно считайте по максимальной яркости, которую допускаете.
- Высота пламени: в симуляторе поменяйте 36 на 20 и на 60. Заметьте, до какого пикселя доходит жар (яркость заметно больше нуля) — зависимость обратная и её видно сразу.
- Синий огонь: CHSV(heat[i] / 8, 255, heat[i]) замените на CHSV(160, 255, heat[i]) — «газовая плита». Затем опустите насыщенность с 255 до 180 — пламя станет голубоватым и холодным на вид.
- Двойной поджиг: продублируйте строку вспышки — в кадре будет вспыхивать два пикселя сразу. Пламя станет агрессивнее; найдите порог, при котором это ещё костёр, а не взрыв.
- Обратное пламя: в цикле цвета пишите leds[15 - i] = CHSV(...). Пламя перевернётся без единого изменения в алгоритме — наглядная проверка, что направление задаётся выводом, а не расчётом.
- Огонь — это температура: отдельный массив heat[], цвет вычисляется из жара, а не выбирается
- Четыре шага кадра: случайное остывание, подъём вверх, вспышка у дна, температура в цвет
- COOLING 55 и SPARKING 84 — стартовые значения; высоту и характер пламени крутят именно они
- qsub8 и qadd8 — арифметика с прижимом: байт не перевернётся от 0 в 255 и не даст белых пикселей
- Дно огня — пиксель 0; лента висит наоборот — разверните вывод или физически переверните ленту
- 1. Что хранит массив heat[] в алгоритме Fire2012?
- 2. Почему остывание на шаге 1 случайное, а не фиксированное?
- 3. Зачем шаг подъёма проходить от вершины к дну, а не наоборот?
- 4. Что вернёт qadd8(heat[y], 200), если heat[y] = 100?
- 5. COOLING подняли с 55 до 100. Что стало с пламенем?
Частые вопросы по теме урока
- Что хранит массив heat[] в алгоритме Fire2012?
- heat[] — второй, «физический» слой: температура каждой ячейки. Цвет вычисляется из неё отдельным шагом, поэтому огонь получается связным процессом, а не набором случайных точек. Массив живёт параллельно leds[] и пересчитывается каждый кадр. Температуру пикселя от 0 до 255
- Почему остывание на шаге 1 случайное, а не фиксированное?
- Живой огонь выедает жар пятнами. Одинаковый минус каждому пикселю даёт механическую лестницу вместо языков. Формула (55 × 10) / N + 2 сама подгоняет диапазон под длину ленты. При фиксированном вычитании фронт пламени ползёт ровной «лесенкой»
- Зачем шаг подъёма проходить от вершины к дну, а не наоборот?
- Подъём читает нижних соседей. Если идти снизу вверх, вы перезаписываете ячейки, которые ещё понадобятся соседям выше, — скорость волны перестаёт соответствовать формуле и пламя становится нервным. Иначе в верхние ячейки попадёт уже обновлённый жар, и волна ускорится вдвое
- Что вернёт qadd8(heat[y], 200), если heat[y] = 100?
- qadd8 складывает и прижимает результат к 255: байт не способен уместить 300, и без прижима он перевернулся бы в 44 — пиксель внезапно погас бы посреди вспышки. 255 — сложение с потолком
- COOLING подняли с 55 до 100. Что стало с пламенем?
- Больше COOLING — сильнее вычитание каждый кадр: жар не успевает подняться высоко. Пламя садится к дну и вырождается в искры; выше 100 я не заходил, дальше правят уже поджиг и кадр. Ниже: красные искры у дна