Если вы используете в своей игре спрайты с прозрачностью (а обычно так и бывает, как минимум для UI), то вам, вероятно, стоит уделить внимание к полностью прозрачным пикселям текстур (или «текселам»).
Даже если значение альфа-канала равно 0, с пикселем всё равно связано значение цвета. Этот цвет ни на что не влияет, так ведь? В конце концов, пиксель полностью прозрачен, кому есть дело до его цвета…
Так вот, на самом деле этот цвет важен, если этого не понимать, то можно получить артефакты, которые заметны во многих играх. Чаще всего искажения очень малы и их не заметно, но иногда они действительно бросаются в глаза.
Пример искажений
Пора привести пример из реальной жизни! Вот XMB моей PS3 (главное меню), с демо-версиями нескольких игр.
Сначала выбрана Limbo, потом я просто нажимаю «вверх», чтобы переместиться к The Unfinished Sawn
(кстати, обе игры отличные).
Начало.
Нажимаю «вниз». Limbo спускается вниз.
Фон становится белым.
Артефакты.
Видите, что произошло с областью логотипа Limbo?
Фон сменился на белый фон The Unfinished Swan и в результате «абсолютно белый» логотип Limbo отрисован поверх фона, который тоже полностью белый. Эта область должна быть полностью белой, тогда откуда взялись эти странные серые пиксели?
Вероятнее всего, искажение возникло из-за того, что текстура Limbo использует для полностью прозрачных пикселей неправильные цвета RGB.
Фильтрация текстур
Артефакты на самом деле возникают из-за того, как видеопроцессор фильтрует текстуру при рендеринге спрайта на экране. Давайте рассмотрим его работу на простом примере.
Вот небольшая пиксельная текстура с красным крестом размером 12x12:
А вот его увеличенное изображение, шахматная клетка просто показывает, что это полностью прозрачная область со значением альфа-канала 0.
Можно использовать этот спрайт как значок для отображения в UI здоровья или как текстуру для игровой модели аптечки. (Хотя нет! На самом деле этого делать не стоит!)
Давайте создадим три версии этого спрайта, просто изменив значение цвета пикселей с нулевой альфа-прозрачностью.
Прозрачная область: зелёная. Как выглядит изображение:
Прозрачная область: синяя. Как выглядит изображение:
Прозрачная область: красная. Как выглядит изображение:
(Вы можете скачать файлы и проверить значения RGB прозрачных пикселей)
Эти три спрайта выглядят на экране совершенно одинаково, правда? Это логично: мы всего лишь изменили значение цвета прозрачных пикселей, которые всё равно будут невидимыми.
Но давайте посмотрим, что происходит, когда эти спрайты находятся в движении. Вот увеличенное изображение, чтобы лучше видеть экранные пиксели:
Мы видим тут искажения! Коричневый оттенок у первого спрайта и фиолетовый у второго. У третьего всё правильно, именно так он должен выглядеть.
Давайте рассмотрим синюю версию:
Как мы видим, проблема возникает, когда положение текстуры не соответствует попиксельно экранным пикселям. Это можно объяснить билинейной фильтрацией, которую видеопроцессор выполняет при рендеринге спрайта на экране: при сэмплировании текстуры видеопроцессор усредняет значения цвета ближайших соседних пикселей с запрошенными координатами в вертикальном и горизонтальном направлениях.
Рассмотрим случай, когда положение спрайта не совпадает ровно на половину пикселя:
Каждый экранный пиксель сэмплирует спрайтовую текстуру ровно между двумя текселами. Именно это происходит с пикселем, который видно на изображении: он выбирает спрайтовую текстуру посередине между сплошным красным текселом и прозрачным синим текселом. Средний цвет получится таким:
$$display$$0,5astbegin{bmatrix} color{#ff2c2c}1\ color{#00c300}0\ color{#2f9fff}0\ 1\ end{bmatrix}+0,5astbegin{bmatrix} color{#ff2c2c}0\ color{#00c300}0\ color{#2f9fff}1\ 0\ end{bmatrix}=begin{bmatrix} color{#ff2c2c}{0,5}\ color{#00c300}0\ color{#2f9fff}{0,5}\ 0,5\ end{bmatrix}$$display$$
А это частично прозрачный фиолетовый, примерно такой: █
Этот цвет, возвращённый сэмплером текстур, теперь будет примешан к альфа-каналу результата рендеринга (сплошному белому цвету).
Уравнение смешивания имеет вид:
$$display$$=0,5astbegin{bmatrix} color{#ff2c2c}{0,5}\ color{#00c300}0\ color{#2f9fff}{0,5}\ end{bmatrix}+(1 - 0,5)astbegin{bmatrix} color{#ff2c2c}1\ color{#00c300}1\ color{#2f9fff}1\ end{bmatrix}=begin{bmatrix} color{#ff2c2c}{0,75}\ color{#00c300}{0,5}\ color{#2f9fff}{0,75}\ end{bmatrix} $$display$$
Поэтому конечный цвет пикселя на экране будет примерно таким: █
Это нас не устраивает. Правильный результат (который мы получили, когда прозрачные пиксели были красными) будет таким:
$$display$$begin{bmatrix} color{#ff2c2c}{1}\ color{#00c300}0\ color{#2f9fff}{0}\ 0,5\ end{bmatrix}$$display$$
— это билинейно интерполированное значение, которое затем смешивается и получается
$$display$$begin{bmatrix} color{#ff2c2c}{1}\ color{#00c300}{0,5}\ color{#2f9fff}{0,5}\ end{bmatrix}$$display$$
Пиксель на экране выглядит так: █
Как же нам избежать этих неприятных артефактов?
Как избежать этой проблемы
Если вы художник: пусть всё просочится!
Если вы отвечаете за создание графических ресурсов, то обезопасьте свою работу и не доверяйте программистам и движку.
Велика вероятность, что на каком-то этапе конвейера цвета прозрачных пикселей «просочатся» на окружающую их графику. Мы уже видели, как это бывает при билинейной фильтрации текстур, но так может произойти и при генерировании MIP-текстур…
Можно бороться с таким просачиванием цветов… дополнительным просачиванием!
Исходный спрайт
Только RGB
Под этим я подразумеваю то, что перед экспортом графики на диск нужно сначала сделать так, чтобы все непрозрачные пиксели «просочились» в значения RGB соседних прозрачных пикселей (это также называется заливкой (flood-filling) или контурной заливкой (edge-padding)). В этом случае когда прозрачные пиксели в процессе выполнения игры просочатся к своим непрозрачным соседям, то они хотя бы протекут с правильным цветом.
На изображениях выше показан пример из реального мира: атлас спрайтов растительности, извлечённый из GTA V, с альфа-каналом и без него.
Заметьте границу вокруг пикселей с ненулевой прозрачностью: прозрачные пиксели заимствуют цвета своих ближайших видимых соседей. Rockstar не случайно проделала всю эту работу.
В этом процессе нам могут помочь инструменты: у Photoshop есть плагин Solidify,
для Gimp тоже существует плагин…
Также будьте внимательны при экспорте значений RGB прозрачных пикселей, например, при сохранении PNG: многие программы по умолчанию отбрасывают данные RGB прозрачных пикселей и заменяют их при экспорте сплошным цветом (белым или чёрным), чтобы улучшить сжатие.
Если вы программист: используйте Premultiplied Alpha!
Если вы программист, то уже знаете, что не стоит слепо доверять графическим ресурсам, созданным художниками. К счастью, у программистов есть больше возможностей для борьбы с этой проблемой.
Можно использовать инструмент для автоматизации просачивания цвета, о котором мы говорили ранее. Его следует использовать при импорте ресурсов. Но у нас есть гораздо лучшее и более надёжное решение: premultiplied alpha.
Я не буду подробно рассказывать о нём, потому что другие люди написали хорошие описания, например здесь и здесь.
Также крайне рекомендую посты Тома Форсайта (Tom Forsyth): 1 и 2 на эту тему.
Идея очень проста: вместо хранения текстуры как
$$display$$begin{bmatrix} color{#ff2c2c}{R}\ color{#00c300}G\ color{#2f9fff}{B}\ alpha\ end{bmatrix}$$display$$
нужно хранить её как
$$display$$begin{bmatrix} alphaast color{#ff2c2c}{R}\ alphaast color{#00c300}{G}\ alphaast color{#2f9fff}{B}\ alpha\ end{bmatrix}$$display$$
Компоненты RGB просто умножаются на значение альфа-прозрачности пикселя. Исходный цвет по-прежнему можно легко получить, разделив значение на альфа-прозрачность.
Так можно превратить спрайт:
Оригинал
Premultiplied Alpha
Также необходимо изменить уравнение смешивания, потому что наша текстура теперь содержит результат первого умножения, и её не нужно снова умножать на значение альфа-прозрачности:
В OpenGL это выражается во внесении следующих изменений в функцию смешивания:
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
⇒ glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)
Вернёмся к нашему спрайту с красным крестом, смещённому на полпикселя. В случае режима Premultiplied Alpha билинейный интерполятор будет искать среднее между
$$display$$begin{bmatrix} color{#ff2c2c}{1}\ color{#00c300}0\ color{#2f9fff}{0}\ 1\ end{bmatrix} и begin{bmatrix} color{#ff2c2c}{0}\ color{#00c300}0\ color{#2f9fff}{0}\ 0\ end{bmatrix}, вернув begin{bmatrix} color{#ff2c2c}{0,5}\ color{#00c300}0\ color{#2f9fff}{0}\ 0,5\ end{bmatrix}=color{#7f0000}█ при смешивании, а затем begin{bmatrix} color{#ff2c2c}{1}\ color{#00c300}0,5\ color{#2f9fff}{0,5}\ end{bmatrix}=color{#ff7f7f}█$$display$$
Это правильно и довольно неплохо решает все наши проблемы! Конечный результат получился в точности таким же, какой мы ожидаем при «традиционном смешивании», за исключением того, что мы избавились от всех артефактов. Вы заметите, что при работе с premultiplied alpha полностью прозрачный пиксель всегда имеет значение RGB чёрного цвета, поэтому нам не нужно волноваться о том, что же содержится на самом деле в прозрачных областях спрайта. Premultiplied alpha также позволяет избежать головной боли при генерировании цепочек MIP-текстур и наслоении нескольких просвечивающих спрайтов одного над другим.
Подводим итог
Итак, вернёмся к первоначальной теме: была ли проблема с логотипом Limbo действительно вызвана «мусором» в RGB прозрачных пикселей?
Есть только один способ узнать это, поэтому я извлёк файл PNG из пакета демо /PS3_GAME/ICON0.PNG
.
На первый взгляд, изображение выглядит отлично, но давайте удалим альфа-канал, чтобы визуализировать полные значения RGB:
Оригинал
Только RGB
Так и есть: вместо сплошного белого цвета в буквах «B» и «O» присутствуют неправильные значения RGB, которые «просачиваются» и вызывают виденный нами раньше графический баг.
Проблема с этими артефактами в том, что их сложно обнаружить. Я не замечал ничего странного с логотипом Limbo, пока он не рендерился на белом фоне. Не все знают об этой проблеме, поэтому ознакомление с темой будет полезно.
Если вы художник, то вы — первая линия защиты, уделяйте внимание цветам, которые находятся внутри прозрачных пикселей. Если вы программист, то задумайтесь об использовании premultiplied alpha.
Автор: PatientZero