Fake Racing — трёхмерный дизеринг в гоночной игре
Давно хотел применить эффект дизеринга (dithering) к трёхмерной сцене, превратив картинку в однобитную. Наконец, задумка реализована, и теперь мне есть что рассказать и показать.
О чём речь
Дизеринг — искусственное подмешивание шума, позволяющее создать иллюзию высокодетализированного изображения при фактически низкой глубине цвета. Наглядный пример: слева — оригинал, справа — 1-битное изображение, созданное при помощи алгоритма Флойда-Штейнберга.
Если с двумерными изображениями технология и алгоритмы давно известны, то при обработке анимированных трёхмерных сцен всё не так однозначно. Проблема в том, что паттерн должен быть привязан к объектам сцены и перемещаться вместе с ними, а во всех известных алгоритмах паттерн либо создаётся динамически (что непригодно даже для двумерной анимации), либо привязан к плоскости экрана.
Obra Dinn
Одна из самых известных игр с эффектом 3D-дизеринга это, конечно, Return of the Obra Dinn — детективная игра, созданная Лукасом Поупом. На «Хабре» есть перевод статьи о реализации дизеринга в Obra Dinn с анимированными гифками, демонстрирующими суть проблемы и пути решения.
Лукас решил поставленную задачу следующим способом: он спроецировал паттерн на сферу с центром в точке обзора, получился эдакий «скайбокс» из паттерна. При вращении камеры вокруг статичной точки поворот сферы оставался привязанным к сцене, и от паразитного мелькания удалось избавиться.
Но, как справедливо указано в статье, данное решение подходит только для камеры с фиксированной позицией (будто установленной на штативе). Это не проблема для медленной детективной игры, где игрок часто останавливается и рассматривает окружение, но для подвижных игр и тем более для гонок решение не годится, поэтому я решил написать собственную реализацию.
Рендерим пиксели
Поскольку спроецированный размер текстуры меняется при приближении или отдалении, встал вопрос, как сохранить плотность пикселей для сохранения нужного оттенка. Изначально я хотел сгенерировать текстуру со сложным распределением пикселей и маской, «зажигающей» пиксели в правильном порядке, но оказалось, что это лишнее — с задачей прекрасно справляется обычный механизм мипмаппинга, нужно только сгенерировать текстуры для мип-уровней самостоятельно. Для этого был написан скрипт на Python.
Из-за меняющегося масштаба, точки, очевидно, не могут быть отдельными пикселями. В текстуре они сохраняются в виде квадрата 2х2 пикселя (экспериментировал с градиентными кружками разных размеров, результат неудовлетворительный), а получившаяся «клякса» превращается ровно в один пиксель с помощью несложного шейдера.
Результат.
Сразу видна проблема — чёткая граница мип-уровней, пиксели на которой еще и мелькают. Игры с ручным выбором мип-уровня по маске ни к чему хорошему не привели, поэтому я снова доверил всё автоматике — 4 выборки из текстуры с небольшим изменением масштаба, в итоге мип-уровень выбирается сам.
Вы могли заметить, что на последней гифке плотность пикселей возросла. Этого удалось достичь, размножив паттерн на 6 каналов двух RGB-текстур. Это нужно и для рисования полутонов. В зависимости от яркости, выбирается соответствующее количество каналов, в результате текстура становится более или менее плотной.
К сожалению, граница перехода от светлых пикселей на тёмном фоне к тёмным пикселям на светлом фоне получилась достаточно резкой.
У меня были сомнения, достаточно ли такого набора полутонов. Но все сомнения развеялись, как только я применил технологию к реальной картинке.
Позже я даже снизил число каналов до четырёх (одна RGBA-текстура) без заметного ухудшения качества картинки.
Что дальше
Проверяем на реальной сцене.
Чего-то не хватает. Наверное, выделения края.
Позже я немного поэкспериментировал с оттенками. Да, изображение перестало быть однобитным, но едва заметное различие цвета разных пикселей сделало картинку чуть-чуть сочнее. Главное, не переборщить с насыщенностью цвета.
Ложка дёгтя
В наши дни самым наглядным форматом представления своего продукта является видео. И вот тут беда пришла откуда не ждали: современные видеокодеки совершенно не рассчитаны на кодирование пикселизированной графики. В результате после загрузки на YouTube мы получаем вот это.
Пришлось добавить альтернативный режим, в котором «кляксы» паттерна остаются жирными и сглаженными. Из-за этого потерялся шарм и аутентичность, так что включать данный режим есть смысл только для стриминга и записи видео с экрана.
Бороться приходится и с сайтами, автоматически конвертирующими изображения в JPEG. Все скриншоты для этой статьи пришлось вдвое увеличивать без интерполяции, чтобы последующая перекодировка в JPEG не сильно портила картинку.
VR
Став счастливым обладателем VR-шлема, я решил добавить экспериментальную поддержку виара. Как и ожидалось, дизеринг-стиль с виаром не очень сочетается, так что в нём тоже пришлось включить сглаженный альтернативный рендерер из-за паразитных мельканий. Думаю, можно найти более изящное решение решение или изменить стилистику, но это будет уже другая история.
Поиграть и посмотреть живьём
Демоверсия игры Fake Racing доступна в Steam в рамках февральского фестиваля:
В демке пока только базовый геймплей, кое-что интересное уже запланировано и в процессе реализации. Релиз должен состояться в апреле. Думаю, напишу еще один пост с геймплейными фишками, если удастся всё сделать так, как задумал.
До скорых встреч!