Первую «воду» я собрал так: каждый кадр каждому пикселю — случайная яркость синего. Пять строк кода, и на столе лента замерцала, как экран неисправного телевизора. Я менял delay, диапазоны random, яркость — мерцание никуда не уходило, потому что причина глубже: random() не имеет памяти. Ему всё равно, что он выдал в прошлом кадре и что делает соседний пиксель, — каждое значение живёт само по себе. Природные же поверхности — вода, пламя, дым — непрерывны: соседние пиксели похожи друг на друга, следующий кадр похож на предыдущий. За такой «плавной случайностью» и приходит шум Перлина — функция, которую Кен Перлин придумал в 1983 году для фильма «Трон», чтобы компьютерные текстуры перестали выглядеть пластиком; в 1997-м за неё дали технический «Оскар». В этом уроке — inoise8, восьмибитная целочисленная версия шума из FastLED: вода, плазма и правила настройки. Мораль той истории проста: для эффектов нужна не честная лотерея, а непрерывность. Сегодня я не подбираю ничего на глаз: шум честно считает колыхание за меня, а я только кручу ручки масштаба и скорости.
Чем шум отличается от random
Шум — это функция от координат, а не лотерея. Дадим ей номер пикселя — получим узор по ленте: соседние номера дают похожие значения, далёкие — независимые. Дадим вторую координату, время, — узор начнёт колыхаться, причём плавно: значение меняется непрерывно, а не прыгает. Проверить непрерывность легко руками: вызовите шум с шагом координаты 1 — соседние значения почти не отличаются; с шагом 100 — уже независимые. Отсюда и происходят эффекты: «вода» — яркость синих пикселей из шума при фиксированном оттенке, «плазма» — оттенок из шума, «облака» и «лава» — шум, прогнанный через палитру (до неё дойдём в следующем уроке). Кадр по-прежнему рисуется по таймеру millis() каждые 20–30 мс — меняется только способ получения числа на пиксель. Время, кстати, можно вести и без счётчика: x = millis() / 50 — тогда скорость задаёт делитель, и кадр ничего не должен накапливать.
Входов у inoise8 может быть до трёх, но рабочая схема для ленты одна: первая координата — позиция, i × scale + смещение, вторая — время. Результат — число от 0 до 255, которое в среднем держится около 128; запомните это, через абзац оно превратится в контраст. Внутри у inoise8 целочисленная математика: на Uno с его 16 МГц float-вычисления стоят десятки микросекунд каждое, а целочисленный шум считает ленту на 60 пикселей за пару миллисекунд — с запасом на 30 кадров в секунду. Второй плюс — память: результат кладётся в byte, координаты — в int, никаких таблиц на 256 значений: FastLED считает шум на лету для любого входа.
| Ручка | Типовое значение | Что даёт |
|---|---|---|
| Масштаб scale | 15–40 | Растяжение узора: меньше — шире «волны», больше — мелкая рябь |
| Шаг времени | x += 3 за кадр | Скорость колыхания: 1 — медленная вода, 10 — дрожь |
| Контраст | вычесть 30–60 | Шум дышит около 128 — вычитание отрезает мутную середину |
| Куда приложить | — | Яркость — вода, оттенок — плазма, индекс палитры — лава |
| Частота кадров | 20–40 мс | Кадр короче — эффект живее, но ток и нагрузка выше |
#include <FastLED.h> #define LED_PIN 6#define NUM_LEDS 60 CRGB leds[NUM_LEDS];unsigned int x = 0; // вторая координата шума — время void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(160);} void loop() { for (int i = 0; i < NUM_LEDS; i++) { byte n = inoise8(i * 25 + x, x * 3); // вычитание 40 с прижимом к нулю поднимает контраст: // шум дышит около 128, и без этого вся лента серая leds[i] = CHSV(160, 255, qsub8(n, 40)); } x = x + 3; // скорость колыхания FastLED.show(); delay(20);}qsub8(n, 40) здесь — главный трюк настройки. Если приложить шум к яркости напрямую, лента будет ходить между 0 и 255 редко: большинство значений кучкуется около 128, и вода выглядит уныло-серой. Вычитание 40 с прижимом к нулю отрезает нижнюю часть кучи: тёмные провалы наступают уже при n ниже 40, а пики остаются яркими. Подбирается это число на глаз: 20 — мягкая дымка, 60 — резкие вспышки по тёмной ленте. Мне удобно за минуту так: печатаю n в монитор порта, смотрю минимум и максимум за десять секунд, ставлю отсечку посередине мутной зоны. К слову о прижимах: это те же q-функции, что охраняли байт температуры в уроке про огонь — без qsub8 вычитание из маленького n перевернёт байт и даст случайные белые пиксели. Если вода вдруг выглядит „шагреневой“ — пики белые и рваные, — отсечку уменьшайте: вы уже срезали живое. Ту же отсечку в плазме я не ставлю принципиально: там нужна вся середина круга оттенков, и любое вычитание только обеднит эффект.
void loop() { for (int i = 0; i < NUM_LEDS; i++) { byte n = inoise8(i * 30 + x, x * 4); leds[i] = CHSV(n, 255, 255); // оттенок из шума } x = x + 4; FastLED.show(); delay(20);}Симулятор: шум из синусов
В симуляторе курса inoise8 нет — движок поддерживает базовый набор FastLED без библиотечной математики шума. Но идею плавной случайности там проверить можно честно: соберём суррогат из двух синусов с несоразмерными частотами — 0,45 и 0,13 радиана на пиксель — и двух разных скоростей. Единицы тут радианы, поэтому числа выглядят тускло: 0,45 радиана — это около 26 градусов дуги на пиксель. Из-за несоразмерности слоёв узор визуально не повторяется почти никогда, а сумма ходит от −2 до 2, которую map() растягивает в яркость 25–255. Это не настоящий Перлин: у синусов не будет «зубцов» и турбулентности. Но принцип — непрерывная функция вместо лотереи — тот же, и все ручки (масштаб, скорость, контраст) тренируются именно здесь. Полный код для живой платы — в блоках выше. Числа у синусов в радианах, как в школьной тригонометрии: полный период 2π ≈ 6,28, поэтому один «горб» узора при шаге 0,45 занимает около 14 пикселей — посчитайте по своей ленте.
Доказательство плавности — в мониторе порта: раз в полсекунды скетч печатает яркость центрального пикселя парой v=…, и кривая этих значений дышит, без прыжков. Если хотите увидеть контраст — откройте Serial Plotter и сравните: подставьте вместо синусов random(0, 255) — график превратится в пилу, и разница двух миров станет очевидной. Отдельный тест: оставьте только w1 — кривая станет чистой синусоидой, ровной как провод; верните второй слой — появится живая дрожь поверхности.
inoise8 принимает до трёх координат, и третья пригождается, когда лента перестаёт быть лентой. На матрице 8×8 вы отдаёте шуму x и y пикселя отдельно — и получаете облака, плывущие по плоскости, а не по строке; время идёт третьей координатой. На обычной ленте тот же приём даёт «псевдообъём»: дайте каждому пикселю фиктивную вторую координату — номер сегмента ленты — и узор станет богаче. Условие одно: чем больше координат, тем дороже расчёт. На Uno лента за сотню пикселей с двумерным шумом уже отзывается не на все 30 кадров в секунду — если частота кадров важна, замерьте её печатью millis() в монитор порта и снизьте масштаб узора.
Самое вкусное начинается на стыках инструментов. Шум вместо random в огне из прошлого урока — и вспышки перестают нервничать: поджигайте пиксели там, где шум пробил порог, и пламя задышит языками. Шум плюс палитра — лава и облака без единого подобранного вручную цвета: индексом цвета становится само значение шума. Шум плюс колесо оттенков HSV — плазма из кода выше. Правило получается экономное: один шум даёт число, а куда его приложить — к яркости, оттенку или индексу палитры — решаете вы. Один и тот же inoise8 в трёх эффектах выглядит как три разных алгоритма. Бонус, о котором редко пишут: шум детерминирован — при тех же входах он вернёт те же значения, поэтому эффект воспроизводится кадр в кадр, а случайность не «пересыпается» при каждом запуске. И правило работает в обратную сторону: если эффект выглядит слишком хаотично, это не повод добавлять сглаживание — это повод уменьшить шаг времени.
- Рябь и волна: в симуляторе поменяйте 0,45 на 0,9 и обратно. Найдите значение, при котором на шестнадцати пикселях видно ровно три «горба».
- Третий слой: добавьте float w3 = sin(i * 0.29 + t * 0.7) и учтите его в сумме с весом 0.5 — узор станет живее и менее предсказуемым.
- Плазма в симуляторе: оставьте v = 255, а в оттенок подайте map(w, -2, 2, 0, 255) — радужная плазма без единого random().
- График плавности: в Serial Plotter кривая v дышит плавно. Замените формулу на random(25, 255) — график станет пилой. Это лучший тест на «плавную случайность».
- Шум — функция от координат: соседние значения похожи; random() — лотерея без памяти
- inoise8(i * scale + x, x * 3): масштаб растягивает узор, шаг времени задаёт скорость колыхания
- Шум держится около 128 — контраст поднимают вычитанием с прижимом qsub8
- Вода — яркость из шума при фиксированном оттенке; плазма — оттенок из шума
- Суррогат в симуляторе — сумма двух несоразмерных синусов; на плате — inoise8 из кода выше
- 1. Чем шум Перлина принципиально отличается от random()?
- 2. В каком диапазоне держатся значения inoise8?
- 3. Что изменится на ленте, если в i * 25 поднять 25 до 60?
- 4. Зачем в эффекте воды стоит qsub8(n, 40)?
- 5. Для чего Кен Перлин придумал свой шум?
Частые вопросы по теме урока
- Чем шум Перлина принципиально отличается от random()?
- random() не помнит прошлых значений — отсюда мерцание. Шум непрерывен: близкие точки дают близкие числа, и вода колышется, а не дёргается. Именно этого не хватало моей первой «воде» из random(). Соседние по координатам и времени значения коррелируют — картинка непрерывна
- В каком диапазоне держатся значения inoise8?
- inoise8 — восьмибитная функция: результат 0–255, кучкуется около середины. Отсюда трюк с вычитанием 30–60 для контраста и прижимом к нулю. От 0 до 255, в среднем около 128
- Что изменится на ленте, если в i * 25 поднять 25 до 60?
- Масштаб — сколько ленты приходится на одну «волну» шума. Больше множитель — больше шума на пиксель — мельче рябь. Для широких волн множитель уменьшают, я чаще живу в диапазоне 15–40. Узор станет мельче — рябь
- Зачем в эффекте воды стоит qsub8(n, 40)?
- Шум кучкуется около 128, поэтому без обработки вода серая. Вычитание 40 с прижимом утапливает нижнюю часть диапазона в темноту и растягивает живые пики. Поднимает контраст: отрезает мутную середину шума с прижимом к нулю
- Для чего Кен Перлин придумал свой шум?
- Перлин писал процедурные текстуры для «Трона» и получил за эту математику технический «Оскар» в 1997-м. Ленты WS2812 просто отлично легли на готовую идею сорок лет спустя. Для графики фильма «Трон» в 1983 году