У меня геймпад Flydigi Vader 5 Pro и к нему док-станция Charging Dock 2 Pro (в системе она видна как "Flydigi CD2"). На доке 162 RGB-светодиода, собранных в шестиугольную сетку в форме треугольника, и сами по себе они хорошие: яркие, ровные, с приличной цветопередачей. А вот эффекты из фирменной программы Flydigi Space Station выглядели дёшево: медленные анимации идут заметными ступеньками, градиенты между двумя цветами грязнеют в середине, радуга режется на полосы.

Стало интересно, виновато железо или софт. Спойлер: софт. В итоге получился FlyGlow, открытый редактор подсветки для этого дока. Ниже о том, как устроены фирменные эффекты, как док принимает кадры, что не так с цветом и как сделать всё плавно.


Как Space Station рисует эффекты

Space Station - приложение на Electron, а с железом общается фоновая служба на .NET. Эффекты считаются прямо в JavaScript интерфейса, а служба только передаёт готовые кадры в док. Если открыть код генераторов эффектов, видно всё сразу:

  • Каждый эффект - ровно 50 кадров на цикл. При медленной скорости один кадр держится 100 мс и дольше, поэтому плавный эффект превращается в слайд-шоу.

  • Цвета смешиваются линейно в sRGB. Середина перехода между двумя насыщенными цветами получается тёмной и серой: линейная интерполяция гамма-кодированных значений не соответствует ни физике, ни восприятию.

  • Радуга строится через HSL. HSL неравномерен для глаза: жёлтый и голубой выглядят широкими яркими полосами, синий проваливается.

  • Затухание считается по 8-битным значениям с округлением вниз. На тёмном конце шаг в одну единицу уже заметен глазом, и “дыхание” гаснет рывками.

Важный вывод: сам док эффекты не генерирует. Он просто проигрывает присланный набор кадров. Значит, если прислать кадры лучше, станет лучше и подсветка.

Что умеет док

Протокол дока первым разобрал проект OpenFlydigi (Linux-утилита для Apex 5 и CD2). Я перепроверил всё на своём экземпляре с прошивкой 0.0.3.9.

Параметр

Значение

USB

VID 37D7, PID 6001, HID-интерфейс, отчёты по 64 байта

Светодиоды

162 штуки, 16 рядов: 14, 15, 16, 15, 14, 13 ... 3

Кадров в цикла

до 255 (счётчик в один байт), Space Station отправляет максимум 200

Шаг кадра

период в единицах по 20 мс, то есть максимум 50 кадров в секунду

Яркость

1..100

Загрузка

команда 97 открывает передачу, команда 98 несёт пакеты по 50 байт, каждый с подтверждением

Пакет устроен просто: 5A A5 <команда> <длина> <данные> <контрольная сумма>, где контрольная сумма - восьмибитная сумма байтов от команды до конца данных. Конфиг подсветки - шесть байт заголовка (число кадров, период, яркость, режим, направление, размер палитры) и затем по 486 байт на кадр (162 × RGB).

Пара наблюдений на практике:

  • Один пакет с подтверждением занимает около 9 мс, то есть кадр загружается примерно 85 мс, а цикл из 200 кадров - около 17 секунд. Для живого управления в духе OpenRGB это не подходит, а вот для заранее посчитанной анимации - вполне.

  • Память кадров при загрузке не очищается: если заявить в заголовке кадров больше, чем реально отправлено, док начинает играть остатки предыдущей анимации вперемешку с мусором.

  • У Space Station есть ошибка на единицу при подсчёте пакетов: она объявляет
    длина / 50 + 1 пакетов, а отправляет ceil(длина / 50). Числа расходятся, когда длина кратна 50, то есть при количестве кадров, дающем остаток 4 от деления на 25. FlyGlow такие количества просто не использует.

Цвет: линейный свет и OKLab

Значения в sRGB - это не количество света, а гамма-кодированные числа. 50% в sRGB - это примерно 21% света. Поэтому FlyGlow держит всё в линейном свете: так складываются и смешиваются слои, как это делал бы свет.

Для градиентов линейного света мало: глаз воспринимает яркость нелинейно, и переход в линейном RGB тоже выглядит неровно. Для перцептивной интерполяции используется OKLab - цветовое пространство Бьёрна Оттоссона, в котором равные шаги выглядят равными. Смешивание двух цветов выглядит так:

Rgb mix(const Rgb& a, const Rgb& b, float t)
{
    const Lab la = toOklab(a), lb = toOklab(b);
    return fromOklab({la.L + (lb.L - la.L) * t,
                      la.a + (lb.a - la.a) * t,
                      la.b + (lb.b - la.b) * t});
}

Ещё три приёма:

  • Затухание по воспринимаемой яркости. Глаз оценивает яркость примерно как кубический корень от количества света, на этом построена ось L в OKLab. Поэтому для ровного на глаз затухания яркость задаётся в “воспринимаемых” единицах и переводится в линейный свет возведением в куб. “Дыхание” перестаёт резко гаснуть в конце.

  • Радуга из насыщенных основных цветов, интерполированных в OKLab, где опорные точки расставлены пропорционально расстоянию между цветами в OKLab. Оттенок идёт с равномерной скоростью, без широких жёлтых и голубых полос.

  • Гамма вывода настраивается. Светодиод с ШИМ-управлением выдаёт свет линейно от значения, поэтому по умолчанию в док уходят линейные значения: соотношения яркостей совпадают с превью. Для любителей картинки “как в Space Station” есть ползунок.

Восемь бит и дизеринг

Линейные значения хороши всем, кроме тёмного конца: в восьми битах самый тусклый уровень - это уже 1/255, и плавное затухание в темноте снова идёт ступеньками. Помогает временной дизеринг: ошибка округления каждого светодиода переносится на следующий кадр. Анимация зациклена, поэтому прогон делается дважды, и сохраняются результаты второго прохода: ошибка, приходящая в первый кадр, тогда ровно та, что набирается к концу цикла.

Бесшовные циклы

Док крутит анимацию по кругу, поэтому шов между последним и первым кадром заметен сразу. Каждый эффект в FlyGlow - чистая функция от фазы цикла в диапазоне [0, 1): никакого внутреннего состояния, никаких часов. Скорость задаётся целым числом повторов, а не произвольной частотой.

Для "органических" эффектов вроде огня, плазмы и сияния нужен зацикливаемый шум. Трюк классический: четырёхмерный шум, у которого время бежит по окружности в двух дополнительных измерениях.

double loopNoise(double x, double y, double phase, uint32_t seed, double radius)
{
    const double a = 2.0 * std::numbers::pi * phase;
    return valueNoise4(x, y, radius * std::cos(a), radius * std::sin(a), seed);
}

При phase = 0 и phase = 1 точка на окружности одна и та же, поэтому шов исчезает математически, а не подгонкой. Автотест прогоняет все 21 эффект: нет ли NaN, совпадает ли последний кадр с первым, нет ли резких скачков между соседними кадрами.

Жить рядом со Space Station

Первой версией FlyGlow отправлял кадры через фоновую службу Space Station: её интерфейс общается со службой по именованному каналу \\.\pipe\fcs.sock, сообщения - protobuf с текстовым заголовком. Работало, но выяснилось, что служба обслуживает одного клиента за раз: пока открыто окно Space Station, канал занят им.

Решение оказалось проще: HID-интерфейс дока на Windows можно открыть совместно (FILE_SHARE_READ | FILE_SHARE_WRITE), даже когда его держит служба. Ответы устройства приходят во все открытые дескрипторы, поэтому свои подтверждения приходится отличать по номеру команды, а чужие игнорировать. Теперь FlyGlow пишет в док напрямую, а Space Station спокойно работает рядом и занимается геймпадом. Канал службы остался запасным путём.

Редактор

Остальное - уже интерфейс, C++20 и Qt 6 Widgets:

  • Слои с режимами наложения “Обычный”, “Сложение”, “Экран”, “Умножение”, “Замена светлым”, непрозрачностью и областью, нарисованной кистью.

    Своя анимация. Кисть всегда привязана к ближайшему светодиоду, а размер считается кольцами шестиугольной сетки, так что размер 1 - ровно один диод. Ключевые кадры на таймлайне, между ними интерполяция в OKLab. Левая кнопка рисует, правая стирает.

  • Импорт GIF и видео. Видео декодируется через Media Foundation, без внешних зависимостей. Каждый светодиод усредняет свою шестиугольную ячейку кадра по 19 точкам в линейном свете, а не берёт один пиксель, поэтому мелкие детали не мерцают.

  • Отправка - кнопка с заливкой-прогрессом, около 85 мс на кадр.

  • Интерфейс на английском и русском, переключается на лету без перезапуска.

Что дальше

  • Подсветка геймпадов. У Apex 5 и Vader 5 Pro свой конфиг подсветки, но там всего около 10 кадров на цикл, так что выиграть получится в качестве цветов, а не в плавности.

  • У дока есть команда записи одного кадра, которую Space Station не использует. Если она окажется быстрой, появится живой режим: реакция на музыку или на картинку на экране, а там и интеграция с OpenRGB. Код открыт под MIT, установщик и portable-версия лежат в релизах. Если у вас такой же док с другой прошивкой, отчёт о том, как всё работает, очень поможет.

Комментарии (0)