Свеча из ленты — самый частый запрос после костра: три пикселя, тёплый цвет, и вроде бы всё готово. Я собрал её по первому попавшемуся рецепту — менять яркость случайным числом двадцать раз в секунду — и получил не свечу, а неисправную гирлянду: пиксель нервно дёргался с полным диапазоном от тьмы до максимума, и через минуту на него было больно смотреть. Настоящая свеча рядом с этой версией выглядела как эталон стабильности.
Причина в том, как видит глаз. Он прощает медленные изменения любого размера, но высокочастотный хаос — нет: скачок яркости каждые 50 мс читается не как пламя, а как помеха в проводе. Пламя свечи на самом деле меняется медленно — его несёт воздух, а воздух инертен, — плюс изредка подрагивает от сквозняка. То есть живое мерцание складывается из двух вещей: плавного дрейфа вокруг среднего уровня и редких коротких рывков. Уберите первое — получится мигалка, уберите второе — ночник с периодом, который засекается по часам.
Здесь разбираем именно эту сборку: базовый уровень, два несинхронных медленных колебания, редкий рывок с затуханием, тёплый оттенок и объём из трёх пикселей. Это не огонь из Fire2012 — там поле жара поднимается по ленте, а свеча стоит на месте и живёт яркостью. И не звёздное мерцание из урока про искры — звёзды вспыхивают на тёмном фоне редкими точками, а свеча горит непрерывно и меняется оттенком настроения, не выключаясь.
Слои пламени: медленный дрейф плюс редкий рывок
Модель собирается из трёх слоёв. Первый — базовый уровень, около шестидесяти процентов яркости: свеча никогда не гаснет и не вспыхивает в полную силу. Второй — дрейф: два медленных колебания с разными периодами, скажем 1,7 секунды и 0,7 секунды, сложенные вместе. Периоды несинхронны, поэтому сумма не повторяется заметно глазу — в отличие от одиночной синусоиды из урока про дыхание, у которой период засекается за десять секунд. На настоящей плате роль дрейфа обычно отдают шуму Перлина: inoise8 от бегущей координаты времени даёт именно такой плавный непериодический гуляющий уровень — механика шума разобрана в отдельном уроке.
Третий слой — рывки. Раз в несколько секунд, с вероятностью, привязанной к силе «сквозняка», яркость получает единичный скачок на 25–65 пунктов, и дальше этот скачок затухает сам: каждый кадр остаток умножается на девять десятых. Получается характерный жест пламени — короткий подскок и мягкий спад, а не дребезг. Всё вместе держится в кадре 30 мс: быстрее не нужно, медленнее — и рывки сливаются с дрейфом.
Цвет — половина эффекта. Воск живёт в тёплом углу колеса оттенков из урока про HSV: оттенок 18–26, насыщенность 200–230. И оттенок пусть слегка гуляет — два-четыре шага медленным колебанием: чистая горизонтальная прямая оттенка выдаёт матрицу так же, как прямая яркости. Если пламя кажется ядовитым — вы ушли в оранжевый 30+, если серым — задрали насыщенность ниже 180.
#include <FastLED.h>#define LED_PIN 6#define NUM_LEDS 16 CRGB leds[NUM_LEDS];unsigned long last = 0;int jerk = 0; // остаток рывка, затухает каждый кадр void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(190);} void loop() { if (millis() - last < 30) return; // кадр 30 мс last = millis(); // дрейф: шум от медленно бегущей координаты времени // делитель 50 — ручка темпа: меньше = суетливее, больше = спокойнее int drift = inoise8(millis() / 50, 0); // 0..255 примерно int v = 150 + (drift - 128) / 2; // вокруг 150 плюс-минус 64 // рывок от сквозняка: редко, коротко, с мягким спадом if (random8() < 8) { jerk = random8(25, 66); } jerk = (jerk * 9) / 10; v = constrain(v + jerk, 40, 255); // оттенок тоже гуляет на пару шагов int hue = 22 + (drift - 128) / 48; // 19..25 // пламя объёмно: фитиль, середина, кончик leds[0] = CHSV(hue, 210, v); leds[1] = CHSV(hue + 6, 230, v * 3 / 4); leds[2] = CHSV(hue + 10, 240, v / 2); for (int i = 3; i < NUM_LEDS; i++) { leds[i] = CRGB(0, 0, 0); } FastLED.show();}Главная грабля этого скетча — темп шума. inoise8 принимает координату, и если скормить ей сырой millis(), пламя будет дрожать как осиновый лист: за секунду координата пробегает тысячу единиц, а шум на таком шаге меняется десятки раз. Делитель — обязательный слой: millis() / 50 даёт двадцать шагов в секунду по медленной оси, и уровень успевает «дышать». Вторая грабля — пределов: сумма базового уровня, дрейфа и рывка легко вылезает за 255, а результат обёртки байта вы увидите как внезапную тьму вместо вспышки. constrain в середине скетча — не украшение, а страховка.
Почему ровно три пикселя: пламя объёмно, и его нижняя точка — фитиль — всегда темнее и теплее, кончик — светлее. Развал яркости v, три четверти, половина с лёгким сдвигом оттенка рисует этот объём даже на ленте, повёрнутой боком. Больше пяти пикселей на одну свечу не берите: начинается «факел», и глаз требует уже движения вверх — а это другая задача, поле жара из Fire2012.
Ток — грань, на которой свеча обычно и живёт. Три пикселя в полную белую яркость берут порядка шестидесяти миллиампер, а свечной диапазон 30-70 из 255 держит ток около пятнадцати-двадцати — на батарейном ночнике это разница между заменой питания раз в месяц и раз в неделю. Отсюда же верхняя граница базового уровня: выше восьмидесяти процентов свеча физически правдивей, но греет блок питания и выбеливает глаз, привыкший к темноте. Если лента длинная, а свеча нужна одна, ведите её на первых трёх пикселях и гасите остальные на ноль — буфер это позволяет, и каждый негорящий пиксель экономит свои миллиамперы.
Переключите кнопку и просто посмотрите минуту. Дешёвый режим выдаёт себя мгновенно: у случайной стрельбы нет ни истории, ни инерции — яркость не помнит, где была кадр назад. Живая версия движется: между двумя рывками уровень успевает проплыть по дрейфу, и глаз спокойно называет это пламенем. Именно контраст между режимами — лучший аргумент против рецептов из первых ссылок поиска.
Ручкой сквозняка слушайте частоту рывков: на нуле свеча почти спокойна — только дрейф, на сотне — живёт в постоянном лёгком волнении. Порог вероятности draft / 8 + 2 подобран так, чтобы даже при нулевой ручке редкий рывок случался: совсем ровное пламя — тоже фальшь, у настоящей свечи воздух никогда не стоит.
| Подход | Характер движения | Вердикт для свечи |
|---|---|---|
| Случайная яркость каждый кадр | Скачки 0–255 без истории, 20 раз в секунду | Нервная стрельба, читается как помеха |
| Одиночная синусоида | Плавно, но строго периодично | Ночник, период засекается по часам |
| Два медленных слоя плюс рывки с затуханием | Непериодичный дрейф, редкие подскоки | Свеча: глаз верит |
| Fire2012 с полем жара | Жар поднимается по ленте, языки | Костёр: на 3 пикселях неуместен |
Свеча как ночник: куда её встроить
Одинокая свеча из трёх пикселей редко живёт сама по себе — обычно это режим в ночнике или bedside-лампе. Каркас трёх режимов с сохранением состояния после перезагрузки уже разобран в уроке про ночник на EEPROM: свеча там встаёт младшим режимом с самым низким бюджетом яркости — базовые 150 из 255 при трёх горящих пикселях съедают меньше, чем один пиксель на полной яркости, и ночник можно оставлять на всю ночь.
Если свечей нужно много — каминная полка, витрина, десять штук по комнате — та же модель раскладывается на массив: каждая свеча получает собственную фазу дрейфа и собственный таймер рывков. Главное правило, которое я вынес из этого: рассинхронизируйте фазы, а не частоты. Одинаковые частоты с разными фазами рано или поздно совпадут и синхронно мигнут — а разные фазы живут независимо вечно.
Практика рассинхрона на массиве: каждой свече достаётся свой сдвиг координаты шума — плюс сорок-пятьдесят единиц на номер — и свой период рывков, собранный из случайного при старте с сохранённым зерном. Без сдвига десять свечей на одной inoise8 дышат синхронно, как хор, и глаз мгновенно раскусывает подделку; со сдвигом даже одинаково собранные тройки пикселей живут каждая своей жизнью. Случайный период рывков добивает иллюзию: интервалы 2-6 секунд, выбранные random на каждом узле, не дают глазу поймать общий такт. Это всё те же пять строк на свечу — просто выполненные восемь раз с разными числами.
- 1. Почему случайная яркость каждый кадр не выглядит пламенем?
- 2. Что делает редкий рывок (jerk) в живой модели свечи?
- 3. Чем свеча принципиально отличается от Fire2012?
- 4. Что случится, если скормить inoise8 сырой millis() без делителя?
- 5. Какие оттенок и насыщенность дают believable тёплый воск?
- Случайная яркость каждый кадр — это помеха, а не пламя: у скачков нет истории и инерции
- Формула свечи: база около 60% яркости плюс два несинхронных медленных колебания плюс редкие рывки с затуханием
- Рывок — жест, а не шум: одиночный подскок на 25–65 пунктов и спад умножением на 9/10 каждый кадр
- На плате дрейф делает inoise8 от масштабированного времени: millis() / 50, иначе пламя дрожит
- Три пикселя: фитиль темнее и теплее, кончик светлее, оттенок 18–26 при насыщенности 200–230
Частые вопросы по теме урока
- Почему случайная яркость каждый кадр не выглядит пламенем?
- Настоящее пламя меняется медленно — воздух инертен. Скачок яркости двадцать раз в секунду не оставляет глазу времени сшить кадры в движение, и остаётся нервный дребезг. Глаз прощает медленные изменения, но высокочастотный хаос читает как помеху
- Что делает редкий рывок (jerk) в живой модели свечи?
- Рывок — это короткий жест пламени: единичный скачок на 25–65 пунктов, а дальше остаток каждый кадр умножается на девять десятых — экспоненциальный мягкий спад. Даёт одиночный подскок яркости, который затем мягко затухает
- Чем свеча принципиально отличается от Fire2012?
- Fire2012 живёт перемещением жара снизу вверх — это языки костра. У свечи нет подъёма: одна точка меняет яркость и оттенок, поэтому ей хватает слоя-модели без массива температур. У огня поле жара поднимается по ленте, у свечи источник стоит на месте
- Что случится, если скормить inoise8 сырой millis() без делителя?
- Координата пробегает тысячу единиц за секунду, и шум меняется десятки раз за это время. Делитель вроде millis() / 50 замедляет бег по оси шума до скорости, на которой пламя дышит. Пламя будет дрожать в разы быстрее настоящего — превратится в дрожь
- Какие оттенок и насыщенность дают believable тёплый воск?
- Воск живёт в узком тёплом углу колеса: 18–26 по оттенку при насыщенности 200–230. Уход за 30 даёт пластиковый оранж, падение насыщенности ниже 180 — серость. Оттенок около 22, насыщенность около 210 — тёплый жёлто-оранжевый