[Перевод] Эффект дизеринга в трёхмерной игре

image


Создатель Papers, Please Лукас Поуп работает над новым трёхмерным проектом Return of the Obra Dinn, в котором пытается с помощью эффекта дизеринга воссоздать в игре ощущение старинной книги.

Для начала краткое объяснение: Obra Dinn выполняет внутренний рендеринг всего в 8-битной палитре в градациях серого, а затем на этапе постобработки преобразует конечные выходные данные в 1-битные значения. Преобразование из 8-битного в 1-битный цвет выполняется сравнением каждого пикселя исходного изображения с соответствующей точкой в тайловом паттерне дизеринга. Если значение пикселя изображения больше значения точки паттерна дизеринга, то выходному биту присваивается значение 1, в противном случае оно равно 0. Выходные данные упрощаются до 1-битных значений, а глаз зрителя объединяет пиксели, аппроксимируя из них больше битов.

43019478f0935551952e4d3479a77295.png


Преобразование исходного изображения по шаблону дизеринга

Двумя компонентами этого процесса являются исходное изображение и паттерн дизеринга. В различных случаях Obra Dinn использует два отличающихся паттерна: матрица Байера 8×8 для более плавного диапазона оттенков и поле синего шума 128×128 для менее упорядоченного вывода.

7f57da6449b9c5a847b54dacb38ab770.png


Байер/синий шум

d0b125a0d643e689b875957746e4c1d2.png


Результат внутри движка без каркасных линий. Байер на сфере, синий шум на всём остальном.

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

Двигаем сферу
e699302ee6b47b8520d5523970445796.gif


Сегодня дизеринг в основном используется для статичных исходных изображений или при высоком разрешении выходных данных. Первое, что думаешь, глядя на этот плавающий эффект дизеринга, это не «да, именно так работает дизеринг», а «что это за дёргающийся эффект и как мне его отключить».

Образец A. Для более приятного изображения контраст уменьшен.
fa165b96a570d2ea3f8b9762ac166d00.gif


Попробуйте сфокусироваться на каком-нибудь объекте, пока он двигается, и вы поймёте, в чём основная проблема Obra Dinn в полноэкранном режиме. Существуют способы исправить это, и чаще всего они сводятся к «этот стиль не работает, замени его». Я довольно далеко зашёл на этом пути, экспериментируя с различными стилями, но потом вернулся назад и задал себе вопрос — возможно, не стоит давать этим гадским пикселям мешать мне.

Стабилизируем дизеринг


Чтобы дать глазам возможность наилучшим образом всё рекомбинировать, дизеринг оптимальнее всего использовать с точками паттерна дизеринга, имеющими корреляцию 1:1 с выходными пикселями. Но если будет присутствовать корреляция «только» с выходными данными, то при применении постэффектов сцены не будет никакой связи между отрендеренной геометрией и пороговым паттерном. В каждом кадре у движущихся элементов сцены будет новое пороговое значение. Вместо этого я хочу, чтобы паттерн дизеринга был «приклеен» к геометрии и казался стабильным при движении вместе с остальной сценой.

Здесь возникает проблема наложения. Существует конфликт между «идеальным» наложением паттерна дизеринга (1:1 с экраном) и идеальным наложением на сцену (x:1 с геометрией), так что нужно быть готовым идти на компромиссы. БОльшая часть моей работы посвящена наложению входного паттерна дизеринга на различные пространства, которое обеспечивает наилучшее совпадение паттерна с геометрией сцены. Здесь всё выполняется на этапе до задания порогов.

Пространство текселов


Первой моей попыткой было наложение паттерна дизеринга на пространство текселов. Это аналогично дизерингу текстур объектов во время рендеринга сцены вместо выполнения постобработки 8-битного выходного изображения. Я не ожидал, что это сработает, но всё равно хотел посмотреть, как будет выглядеть идеально совпадающее со сценой наложение.

102a5097d36f733c80835508d7cc6c8b.png


Паттерн дизеринга в пространстве текселов

Ну, в целом ожидания себя оправдали. Наложение на все объекты выполнено по-разному, поэтому масштабы из паттернов не совпадают. Их можно унифицировать. Но настоящая проблема заключается в искажениях. Любой ресемплинг из одного пространства в другое приведёт к искажениям, а для паттернов дизеринга не так просто выполнить mip-текстурирование или фильтрацию, как для традиционных текстур. Однако доведём это до конца:

Применение к подвижной сцене
5a2608f8bbf97c80bd7d451fdc8b11c2.gif


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

Деформация при движении


Если я хотел, чтобы паттерн дизеринга отслеживал движение геометрии под ним, то почему бы просто не деформировать паттерн на основании изменения позиции каждого отрендеренного пикселся в сцене? Действительно, почему бы не попробовать. Это немного похоже на motion blur, при котором каждый пиксель отслеживает своё движение относительно предыдущего кадра. В этом случае я обновляю текстуру дизеринга, чтобы её паттерн двигался вместе со сценой. Если пиксель сцены не присутствовал на предыдущем кадре, то в нём паттерн дизеринга перезагружается. Реализацию этой техники очень облегчила статичность игры — мне нужно было беспокоиться о движении камеры, а не отдельных объектов.

Деформация паттерна дизеринга для сохранения покадровой согласованности со сценой
b6d6ff6a4784673b118073a54fcc0481.gif


Это была довольно «быстрая и грязная» попытка, но стали очевидны некоторые факты. Во-первых, это в чём-то работает. Во-вторых, паттерну дизеринга нужно учитывать соседей — он не может быть просто отдельными пикселями. Если рассматривать каждый пиксель отдельно, как делается в этом способе, то очевидно, что мы получим разрывы и искажения в паттерне. В этой тестовой сцене я сдвинул камеру, чтобы показать это на примере сундука. Посмотрев на сам искажённый паттерн дизеринга, легче это заметить.

Задание порога сплошным серым цветом с деформируемым паттерном дизеринга
07d829b053df01a9e46de6e5f64fa32f.gif


Эти разрывы возникают из-за разной глубины пикселей и выбранных порогов. Я подумывал о сложной системе исправления проблемы на основе отслеживания областей, усреднения их глубины и смещения всех точек паттерна дизеринга в каждой области на одинаковое значение. Разрывы вдоль границ областей можно скрыть резкой сменой освещения или каркасной линией. Это не получилось бы реализовать из-за того, что игра использовала для генерации каркасов моделей цветные области. Когда я приступил к реализации всего этого, то сначала упустил в уравнении глубину, что дало мне гораздо более простую альтернативу:

Наложение на экран со смещением


При составлении уравнений для деформируемого дизеринга из них выпало очень простое преобразование:

DitherOffset = ScreenSize * CameraRotation / CameraFov


Сдвиг наложенного на экран паттерна дизеринга на основании поворота камеры
8acbfdafc9f5972bf914f02f2c494d05.gif


В сущности, это выражает то, что я хотел: сдвиг наложенного на экран паттерна дизеринга ровно на один экран при повороте камеры на одну область обзора. Благодаря этому сохраняется наложение 1:1 с экраном, но при этом также учитывается упрощённое преобразование видимой геометрии сцены. На самом деле это соответствует только движению в центре экрана, но, к моему счастью, выглядит достаточно хорошо.

Смещение паттерна дизеринга для отслеживания поворота ровно на один экран fov камеры
b5c248e406cfd82fdfc120ed45129c98.gif


Заметьте: похоже, что подвергнувшиеся дизерингу пиксели стула в основном движутся с геометрией. То же самое относится и к сфере. Более перпендикулярные к полю зрения плоскости отображаются не очень хорошо — пол по-прежнему выглядит хаотичным.

Хотя подход и не идеален, простой сдвиг наложенного на экран дизеринга сохраняет общий паттерн и движение сцены, чтобы глазу удобнее было отслеживать вместе. Я был этим очень доволен. Занимаясь подчисткой кода и коммитами, выпустив один-два поста в devlog, я всё равно не мог избавиться от мысли об идеально прилепленном дизеринге:

Пространство мира — кубическое наложение


Предыдущие эксперименты показали, что любая корреляция между паттерном дизеринга и геометрией сцены должна игнорировать информацию глубины, получаемую от сцены. На практике это означает, что дизеринг можно прицеплять к геометрии во время поворота камеры, но не её перемещения. Это не так уж плохо для Obra Dinn, учитывая медленный темп игры и наблюдательную роль игрока. Обычно в игре он ходит по кораблю, останавливается и смотрит на объекты. При ходьбе на экране происходит так много изменений, что плавающий дизеринг не особо очевиден.

С учётом этого, моей следующей попыткой стало наложение паттерна дизеринга на геометрию ненапрямую, с помощью предварительного рендеринга паттерна на стороны куба, центрированного вокруг камеры. Куб перемещается с камерой, но остаётся ориентированным относительно мира. Получается смесь: немного экрана, немного сцены.

e91e32f1075d56acfeb848c54871a58b.png


Паттерн дизеринга наложен на куб, центрированный относительно камеры

753920afa3e43877b7cce860b557070d.png


Вид из камеры, смотрящей в угол. Масштаб наложения для наглядности увеличен.

Наложение на куб работает неплохо, когда смотришь на стороны, но не так хорошо, когда камера направлена на угол. Паттерн дизеринга по-прежнему идеально зафиксирован в 3D-пространстве при повороте камеры. Даже при грубых проверках результат выглядит многообещающим.

Задание порога сцене с помощью наложенного на куб паттерна дизеринга
dfda025b1e35029d921f965c165ca30b.gif


Дело, наконец, сдвинулось с места. Благодаря тому, что это постобработка, такой подход более общий, чем наложение в пространстве текселов, что хорошо. Проблема теперь сводится к конкретному кубическому наложению. При идеальном наложении один тексел на кубе всегда соответствует одному пикселю на экране, вне зависимости от поворота камеры. Для куба это невозможно…

Пространство мира — сферическое наложение


…, но благодаря сфере я подобрался достаточно близко.

269d2dcb245b33c3da0873d15fe92371.png


Наложение паттерна дизеринга на внутренность сферы

Поиск этого конкретного сферического наложения потребовал определённого времени. Не существует способов идеального замощения сферы квадратной текстурой. Можно было бы переопределить матрицы дизеринга через сетку шестиугольников или чего-то подобного, что хорошо замощает сферу. Возможно, получилось бы, но я не пробовал. Вместо этого я «взломал» замощение сферы, добившись тщательной настройкой того, чтобы «кольцевое» наложение исходного паттерна дизеринга давало хорошие результаты.

Применённый к сцене эффект
c55529e354a3e26501e3f451af260930.gif


Лучше, чем с кубом, но по-прежнему много искажений. Размер сферически наложенной точки очень похож на размер экранного пикселя — отличается ровно настолько, чтобы создавать муар. Я чувствовал, что близок к решению, и очень просто исправить такие искажения с помощью суперсемплирования: применить порог дизеринга при более высоком разрешении, а затем снизить его.

df191fb1500cf9b7197b5ab3157bcb55.png


Сферически наложенный паттерн дизеринга при увеличении 2x и со сниженным до 1x разрешением

Задание порога при 2x, с последующим снижением разрешения до 1x
ae67a0b9f9b17b819f601b56511c645c.gif


Это пока самый лучший из полученных мной результатов. Тут есть несколько компромиссов:

  1. Точки паттерна дизеринга становятся больше в размерах и менее эффективными по краям экрана
  2. Паттерн не выравнен по направлениям «верх-низ-лево-право» для большинства поворотов камеры
  3. Выходные данные больше не являются 1-битными из-за конечного снижения разрешения


Но преимущества очень велики:

  1. Дизеринг отлично прикрепляется ко всем поворотам камеры. В игре это ощущается немного странно.
  2. Дискомфорт от плавающего дизеринга совершенно пропал, даже в полноэкранном режиме.
  3. Сохраняется пикселизированный стиль игры


Можно полностью избавиться от недостатка 3, снова ограничив выходные данные 1-битными значениями с помощью простого порога в 50%. Результат по-прежнему лучше, чем без суперсемплинга (ниже представлены три примера для сравнения).

Сравнение трёх подходов
d3e80349c226387f712627d1fc2e1bef.gif


В игре с палитрой по умолчанию
a5f81f5a8192905cf98e9297e71ce529.gif


Подводим итог


Кажется немного странным потратить 100 часов на то, отсутствия чего даже не заметят. Никто точно не подумает «блин, да этот дизеринг адски стабилен, это какая-то магия». Но я не хотел, чтобы у людей возникали проблемы, которые должны были бы возникнуть, так что их стоило устранить.

Наложение в экранном пространстве со смещением работает лучше всего при масштабе 1x, а сферическое наложение — при 2x. Вся сцена сейчас рендерится в разрешении 800×450 (поднял разрешение с 640×360), что повышает разборчивость, при этом не требуется жертвовать стилем low-res. В готовой игре будет два режима отображения:

ЦИФРОВОЙ — дизеринг в пространстве экрана со смещением, 1-битный вывод.

АНАЛОГОВЫЙ — полноэкранный наложенный на сферу дизеринг, сглаженный вывод.

© Habrahabr.ru