Синусная волна на ленте хороша, пока вы не засечёте её период по часам. Моё «северное сияние» на балконе первый месяц ходило строем: одиннадцать секунд туда, одиннадцать обратно — и глаз начал предсказывать каждый вздох. Случайные числа дали противоположную крайность — помехи на экране. Третье государство между ними — шум Перлина: непрерывная случайность, у которой соседние значения похожи, а период не засекается вовсе. Контракт функции и настройку воды с плазмой разобрали в уроке про inoise8; здесь — модель в голове: что такое решётка высот, куда подавать позицию и время, и как честно понять, что вам хватило бы синуса. Хороший шум стоит на трёх столбах: непрерывность по ленте, непрерывность по времени и отсутствие заметного периода — проверим каждый на числах.
Договор такой: inoise8(x, y) принимает до трёх целых координат и возвращает байт от нуля до двухсот пятидесяти пяти, в среднем около ста двадцати восьми. Внутри — решётка, в узлах которой «высоты» перемешаны случайно, а между узлами плавно интерполируются. Подали номер пикселя — получили рельеф по ленте: соседние номера дают похожие значения, далёкие — независимые. Подали вторую координату, время, — рельеф поплыл, причём плавно, без дёрганья. И никаких таблиц: FastLED считает высоту на лету для любого входа, память тратится только на буфер ленты.
Всё ремесло живёт в масштабе входов. Позицию подают умноженной: i * 30 — каждый пиксель отступает в поле на тридцать единиц, и горбы широкие; i * 80 — рябь. Время подают счётчиком: q плюс единица-тройка за кадр, — либо готовым делением millis() / 30, где делитель и есть скорость. Масштаб и время — две независимые оси, и путать их не стоит: первая строит рельеф, вторая его везёт. И главное правило, на котором хоронят шум новички: координаты обязаны меняться каждый кадр, иначе лента показывает одну и ту же фотографию, хотя loop() крутится изо всех сил.
Решётка, масштаб, средний уровень
«Около ста двадцати восьми» — не мелочь, а ключ к применению. Приложите шум к яркости напрямую: лента будет серо-мерцательной, потому что большинство значений кучкуется у середины, и до нуля с потолком добираются редкие пики. Для воды и сияния середину отрезают вычитанием с прижимом к нулю — qsub8(n, 40) и подбор отсечки считали в уроке про шум. Либо отдайте шум не яркости, а оттенку или индексу палитры: механика палитр в уроке про CRGBPalette16, там шум рисует лаву и облака без единой ручки контраста.
Время удобно вести счётчиком-байтом: он переполняется каждые двести пятьдесят шесть шагов, но решётка шума сама периодична с тем же периодом — на стыке нет шва, картинка склеивается бесшовно. Лень считать — подавайте millis() / 30: делитель тридцать даёт ленивый дрейф, десять — живое колыхание, пятьдесят — почти стоячую воду. Два слоя с разными масштабами — inoise8(i * 30, millis() / 20) плюс inoise8(i * 80, millis() / 50) — дают зыбь и рябь разом; складывайте их средним, как складывали волны в океане из трёх темпов, — переполнения нет, а узор перестаёт повторяться на память. Масштабы слоёв берите разными и некратными: тридцать против восьмидесяти, а не тридцать против шестидесяти, иначе слои сольются в один и вы не заметите, что платите за два вызова.
#include <FastLED.h>#define LED_PIN 6#define NUM_LEDS 60 CRGB leds[NUM_LEDS];unsigned int q = 0; // координата времени в поле шума void setup() { FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(180);} void loop() { for (int i = 0; i < NUM_LEDS; i++) { byte n = inoise8(i * 30, q); // позиция x30, время - счётчик leds[i] = CHSV(96 + n / 3, 200, 255); // оттенок гуляет по зелено-синему } q += 2; // скорость дрейфа рельефа FastLED.show(); delay(25);}Числа скетча подобраны под глаза. Шаг позиции тридцать: на шестидесяти пикселях поле проходит два с лишним периода рельефа, и горбы читаются поодиночке, не сливаясь. Скорость q += 2 при кадре двадцать пять миллисекунд — полное путешествие узора занимает секунды, а не кадры: шум живёт на медленных скоростях, и торопить его смысла нет — при плюс десяти за кадр рельеф дрожит, как неисправный телевизор. Один кадр сияния на Uno я мерил грубо: пересчёт шестидесяти пикселей укладывается в пару миллисекунд — запас перед кадром десятикратный. Если сияние кажется мелким, уменьшите множитель позиции до пятнадцати; если рябит — поднимите до шестидесяти, это одна цифра в одном месте.
Оттенок 96 + n / 3 гуляет по зелёно-синему углу колеса: низкие высоты — травяной, пики — ледяной. Средний уровень шума держит середину спектра, и сияние никогда не уходит ни в чистый зелёный, ни в чистый синий — глаз читает это как «природа», хотя формула — одна строка. Тёплый вариант того же приёма для свечи собирается тем же вызовом: мерцание свечи ведёт дрейф уровня именно шумом, добавляя к нему редкие рывки сквозняка. Ток у сияния умеренный: насыщенность двести зажигает два канала из трёх, и шестьдесят пикселей просят примерно вдвое меньше, чем белая заливка, — потолок яркости сто восемьдесят оставляет блоку запас.
Как я подбираю применение шума за пять минут. Запускаю скетч с печатью n в монитор порта и смотрю десять секунд: минимум, максимум, где гуляет середина. Потом решаю, куда идёт шум: к яркости — тогда режу низ вычитанием с прижимом, к оттенку — тогда не режу ничего, весь диапазон становится краской. Третий вариант — индекс палитры, и он самый благодарный: шум честно гуляет по всей палитре от края до края, а контраст настраивается уже самой палитрой, а не формулой в кадре. За те же пять минут сцена получает и характер, и контраст — и обе ручки остаются независимыми.
Отдельно про целые числа на AVR. int на Uno шестнадцатибитный: произведение i * 30 при шестидесяти пикселях влезает свободно, но «улучшение» вида (millis() % 100000) * масштаб уже уезжает за 32767 — держите сомножители маленькими и делите пораньше. unsigned int для q хватает с запасом: больше шестидесяти пяти тысяч шагов при плюс два за кадр — получас непрерывной работы, а стык на переполнении решётка переживает незаметно. Печать n в порт из кадра я оставляю только на отладку: двадцать строк в Serial за кадр тормозят loop заметнее, чем весь пересчёт ленты.
| Ручка | Рабочее значение | Что даёт и когда ломается |
|---|---|---|
| Позиция в поле | i * 30 | Ширина горбов: 15-30 — валы, 60-80 — рябь, 1-5 — независимое мерцание |
| Время счётчиком | q += 1-3 за кадр | Скорость дрейфа: 1 — медленная вода, 5+ — дрожь экрана |
| Время без счётчика | millis() / 30 | Делитель и есть скорость: 10 — колыхание, 50 — стоячая вода |
| Средний уровень | около 128 | Для яркости отрезайте середину (вычесть 30-60 с прижимом) или отдайте шум палитре |
Когда хватает sin8, а когда нужен именно шум
Разница видна с первой секунды. Одиночная волна читается метрономом: горбы равные, паузы равные, период засекается по секундам на стене. Сумма трёх — рельеф: где-то две волны сошлись и вспух пик, где-то разошлись в яму, и повторения глаз не находит. В симуляторе сумма синусов стоит ноль, потому что sin здесь дешёвый; на Uno три float-вызова на пиксель — уже нагрузка на бюджет кадра, а inoise8 строит то же поле целыми числами и считает шестьдесят пикселей за пару миллисекунд. Честности ради: сумма трёх синусов тоже периодична — просто её общий период так длинен, что глаз его не засечёт; шум непериодичен по построению, и на длинных сценах это различие проявляется. Переключите поле обратно на одиночную волну и посмотрите ещё раз: на короткой ленте три горба выглядят почти прилично — обман начинается, когда сцена живёт дольше минуты.
Итог моего выбора такой. Синус оставляю, когда колебание одно, период известен и предсказуемость — часть замысла: дыхание, пульс, маяк, качели — каркас один и тот же, что в уроке про дыхание. Шум беру, когда ломается хоть одно из трёх: период не должен засекаться, рельеф нужен одновременно по ленте и по времени, слоёв больше одного. Тест на часах простой: засекли повтор — а хотели природы, — переходите на шум. Бывают и гибриды, и я к ним пришёл после сияния на балконе: медленное дыхание уровня оставил синусом — его период никто не считает, — а рябь поверх дал шумом. Два вызова, и сцена перестала выглядеть ни машиной, ни помехой. Если же метроном устраивает, тащить решётку ради решётки смысла нет: sin8 табличный и стоит копейки, а на его каркасе уже стоят волны из урока про sin8 и beat88.
- 1. Что возвращает inoise8(x, y)?
- 2. Картинка из шума замерла: кадр не отличается от кадра. Первое, что проверить?
- 3. Позицию подаёте как i * 30 и меняете на i * 80. Что станет с узором?
- 4. Вторая координата — millis() / 30. За что отвечает делитель?
- 5. Когда я остаюсь на sin8 и не тащу шум?
- inoise8(x, y) — байт 0-255, в среднем около 128; внутри — решётка высот с плавной интерполяцией
- Позиция — i * 30 (ширина рельефа), время — счётчик q или millis() / 30 (делитель и есть скорость)
- Координаты обязаны меняться каждый кадр: статичный вызов даёт мёртвую фотографию
- Решётка периодична каждые 256 единиц — байтовый счётчик времени переполняется без шва
- Синус — одно колебание с известным периодом; шум — непериодический рельеф и слои с разными масштабами
Частые вопросы по теме урока
- Что возвращает inoise8(x, y)?
- inoise8 возвращает беззнаковый байт 0-255, значения кучкуются около середины. Цвет из него собираете вы: CHSV, палитра или яркость. Байт от 0 до 255, в среднем около 128
- Картинка из шума замерла: кадр не отличается от кадра. Первое, что проверить?
- Шум — функция от координат: статичный вызов возвращает одно и то же. Двигайте счётчик q или подавайте millis() / делитель. Меняется ли координата времени: без движения q вызов даёт те же числа каждый кадр
- Позицию подаёте как i * 30 и меняете на i * 80. Что станет с узором?
- Чем больше множитель, тем дальше пиксели отстоят друг от друга в поле и тем независимее их высоты: 60-80 — рябь, 15-30 — валы. Узор мельчает в рябь
- Вторая координата — millis() / 30. За что отвечает делитель?
- millis() растёт на единицу координаты каждые 30 мс: делитель 10 — живое колыхание, 50 — почти стоячая вода. За скорость дрейфа: меньше делитель — быстрее движение
- Когда я остаюсь на sin8 и не тащу шум?
- Синус дёшев и предсказуем — и это его сила, пока предсказуемость нужна. Шум берут за непериодичность и рельеф по двум осям сразу. Когда нужно одно колебание с известным периодом — дыхание, пульс, маяк