Опасайтесь прозрачных пикселей

http://www.adriancourreges.com/blog/2017/05/09/beware-of-transparent-pixels/
  • Перевод
image

Если вы используете в своей игре спрайты с прозрачностью (а обычно так и бывает, как минимум для UI), то вам, вероятно, стоит уделить внимание к полностью прозрачным пикселям текстур (или «текселам»).

Даже если значение альфа-канала равно 0, с пикселем всё равно связано значение цвета. Этот цвет ни на что не влияет, так ведь? В конце концов, пиксель полностью прозрачен, кому есть дело до его цвета…

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

Пример искажений


Пора привести пример из реальной жизни! Вот XMB моей PS3 (главное меню), с демо-версиями нескольких игр.

Сначала выбрана Limbo, потом я просто нажимаю «вверх», чтобы переместиться к The Unfinished Sawn
(кстати, обе игры отличные).

image
Начало.

image
Нажимаю «вниз». Limbo спускается вниз.

image
Фон становится белым.

image
Артефакты.

Видите, что произошло с областью логотипа Limbo?

Фон сменился на белый фон The Unfinished Swan и в результате «абсолютно белый» логотип Limbo отрисован поверх фона, который тоже полностью белый. Эта область должна быть полностью белой, тогда откуда взялись эти странные серые пиксели?

Вероятнее всего, искажение возникло из-за того, что текстура Limbo использует для полностью прозрачных пикселей неправильные цвета RGB.

Фильтрация текстур


Артефакты на самом деле возникают из-за того, как видеопроцессор фильтрует текстуру при рендеринге спрайта на экране. Давайте рассмотрим его работу на простом примере.

Вот небольшая пиксельная текстура с красным крестом размером 12x12:



А вот его увеличенное изображение, шахматная клетка просто показывает, что это полностью прозрачная область со значением альфа-канала 0.

Можно использовать этот спрайт как значок для отображения в UI здоровья или как текстуру для игровой модели аптечки. (Хотя нет! На самом деле этого делать не стоит!)

Давайте создадим три версии этого спрайта, просто изменив значение цвета пикселей с нулевой альфа-прозрачностью.

image
Прозрачная область: зелёная. Как выглядит изображение:

image
Прозрачная область: синяя. Как выглядит изображение:

image
Прозрачная область: красная. Как выглядит изображение:

(Вы можете скачать файлы и проверить значения RGB прозрачных пикселей)

Эти три спрайта выглядят на экране совершенно одинаково, правда? Это логично: мы всего лишь изменили значение цвета прозрачных пикселей, которые всё равно будут невидимыми.

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

image

Мы видим тут искажения! Коричневый оттенок у первого спрайта и фиолетовый у второго. У третьего всё правильно, именно так он должен выглядеть.

Давайте рассмотрим синюю версию:

image

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

Рассмотрим случай, когда положение спрайта не совпадает ровно на половину пикселя:

image

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

$$display$$0,5\ast\begin{bmatrix} \color{#ff2c2c}1\\ \color{#00c300}0\\ \color{#2f9fff}0\\ 1\\ \end{bmatrix}+0,5\ast\begin{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$$

А это частично прозрачный фиолетовый, примерно такой:

Этот цвет, возвращённый сэмплером текстур, теперь будет примешан к альфа-каналу результата рендеринга (сплошному белому цвету).

Уравнение смешивания имеет вид:

$\alpha_{фона} \ast RGB_{спрайта} + (1 - \alpha_{спрайта}) \ast RGB_{фона}$

$$display$$=0,5\ast\begin{bmatrix} \color{#ff2c2c}{0,5}\\ \color{#00c300}0\\ \color{#2f9fff}{0,5}\\ \end{bmatrix}+(1 - 0,5)\ast\begin{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-текстур…

Можно бороться с таким просачиванием цветов… дополнительным просачиванием!

image
Исходный спрайт

image
Только 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} \alpha\ast \color{#ff2c2c}{R}\\ \alpha\ast \color{#00c300}{G}\\ \alpha\ast \color{#2f9fff}{B}\\ \alpha\\ \end{bmatrix}$$display$$

Компоненты RGB просто умножаются на значение альфа-прозрачности пикселя. Исходный цвет по-прежнему можно легко получить, разделив значение на альфа-прозрачность.

Так можно превратить спрайт:

image
Оригинал

image
Premultiplied Alpha

Также необходимо изменить уравнение смешивания, потому что наша текстура теперь содержит результат первого умножения, и её не нужно снова умножать на значение альфа-прозрачности:

$RGB_{спрайта} + (1 - \alpha_{спрайта}) \ast RGB_{фона}$

В 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:

image
Оригинал

image
Только RGB

Так и есть: вместо сплошного белого цвета в буквах «B» и «O» присутствуют неправильные значения RGB, которые «просачиваются» и вызывают виденный нами раньше графический баг.

Проблема с этими артефактами в том, что их сложно обнаружить. Я не замечал ничего странного с логотипом Limbo, пока он не рендерился на белом фоне. Не все знают об этой проблеме, поэтому ознакомление с темой будет полезно.

Если вы художник, то вы — первая линия защиты, уделяйте внимание цветам, которые находятся внутри прозрачных пикселей. Если вы программист, то задумайтесь об использовании premultiplied alpha.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 83
  • –6
    Напоролся на именно этот баг (особенность?) в cairo (графическая библиотека, которая используется, например, в Firefox). Вот, например, svg-шная картинка:
    image
    Там на чёрном фоне переход полностью прозрачного цвета в белый. Казалось бы, должен быть градиент между чёрным и белым. А вот фиг! Прозрачный задан как rgba(255,0,0,1), и в результате посередине явственно виден красный. Я уже сделал патч и описание, хотел отправить разрабам cairo… А потом посмотрел ту же картинку в Хроме. и увидел то же самое. И в последнем Эксплорере. Так что это уже не баг, а фича, и чинить это, к сожалению, никто не будет.
    • +8
      У вас в вашей свг такой градиент (посмотрите код ее). Естественно перед будет с красного прозрачного в белый непрозрачный на черном фоне. Поведение на 100% закономерное. Если хотите чтобы не было красного — замените 255 на 0.
      <linearGradient xmlns="http://www.w3.org/2000/svg" id="MyGradient">
              <stop offset="0%" stop-color="rgba(255,0,0,0)"/>
              <stop offset="100%" stop-color="white"/>
      </linearGradient>
      
      • –8
        Неважно, какие там RGB, если прозрачность 100%. При переходе с полностью прозрачного на белый (или любой другой) цвет должен быть только этот конечный цвет, и никакой другой. Об этом, собственно, и статья, и в этом я совершенно согласен с её автором.
        • +11
          цвет должен быть только этот конечный цвет, и никакой другой

          Кто вам такое сказал? А если я хочу выйти из прозрачного красного и в прозрачный белый? Я вот так часто в Юнити в частичках делаю. Там даже настраивается альфа-канал — отдельно, а все остальное — отдельно.
          Вы мыслите поверхностно (вот у меня есть задача и пусть именно под эту задачу все подстраиваются). А задачи бывают совершенно разные и текущее решение — позволяет решать их все, а не только некоторое подмножество.

          Об этом, собственно, и статья

          Статья, конечно, близка, но не об этом.
          • –9
            А если я хочу выйти из прозрачного красного и в прозрачный белый? Я вот так часто в Юнити в частичках делаю. Там даже настраивается альфа-канал — отдельно, а все остальное — отдельно.

            Прозрачный красный — это как? Полностью прозрачный он один, у него нет цвета. Если же имеется в виду «из прозрачного через полупрозрачный красный и в белый», то запросто:
                    <stop offset="0%" stop-color="rgba(0,0,0,0)"/>
                    <stop offset="50%" stop-color="rgba(255,0,0,0.5)"/>
                    <stop offset="100%" stop-color="white"/>
            
            • +11
              Стало интересно, накодил сравнение: https://jsfiddle.net/cjysxbc2/.

              Прозрачный красный — это абстрактное понятие. Вас же не смущает мнимая единица, которая в квадрате внезапно превращается в -1? Почему же вас смущает прозрачный красный, который при изменении прозрачности внезапно становится красным, а не белым / чёрным?

              К слову, какой цвет вы предлагаете на роль «настоящего прозрачного»? Чёрный прозрачный или белый прозрачный? Или, может быть, серый прозрачный?
              • +9
                Прозрачный красный — это как?

                это красный + альфаканал. Ваш кэп.

            • +5
              А как по вашему должен высчитываться цвет по середине, только на основании второго цвета? Или вы предлагаете отдельно считать случай, когда первый цвет полностью прозрачен и когда он не полностью прозрачен? Интерполяция же идет по всем четырем каналам одинаково, так что «баг» в вашем случае, не баг, а вполне ожидаемое поведение.
              • –3
                В статье есть чёткий и понятный ответ, к которому я когда-то самостоятельно пришёл (см. пункт «Если вы программист: используйте Premultiplied Alpha!»).
                • +1
                  используйте Premultiplied Alpha

                  Это вообще бредовый совет. Вот у меня есть красный цвет. Я ходу сделать красный с полупрозрачность 25%. Так вот — по формуле, данной в статье у меня вместо «rgba(255,255,255,0.25)» будет «rgba(51,51,51,0.2)». То есть теперь этот цвет будет мало того, что прозрачным, так еще и чернить и он будет некорректно орисовываться поверх белых поверхностей! Premultimlied alpha можно использовать только если рисовать более светлое поверх более темного, а не всегда. Посмотрите сами на его картинку-пример — эта картинка испорчена!

                  До:
                  image

                  После:
                  image

                  А поверх белого фона это будет еще более заметно.
                  • +2
                    То есть теперь этот цвет будет мало того, что прозрачным, так еще и чернить и он будет некорректно орисовываться поверх белых поверхностей!

                    Это неправда. Premultiplied Alpha нужна для вычислений, а не для вывода на экран. Для вывода на альфу нужно обратно разделить. Когда вы после вычислений получите rgba(51,51,51,0.2) и разделите на альфу, вы получите обратно rgba(255,255,255,0.2).

                    • +1
                      Это должен быть выбор в момент наложения, а не единственный способ в момент сохранения. Какой смысл терять 80% информации при записи в файл?
                      • 0

                        Зачем такой выбор, какой в нем смысл? О потере какой информации идет речь и при чем тут запись в файл, если мы говорим об обработке и выводе на экран?


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

                        • 0
                          Процитирую вам из топика:
                          Идея очень проста: вместо хранения текстуры как [RGBα] нужно хранить её как [α∗Rα∗Gα∗Bα]


                          О выводе не говорится. Говорится о хранении. Если хранить картинки в таком виде, то в большом количестве целевых устройств будет именно черный цвет. Именно этот эффект мы видим на картинке «после».
                          • +2
                            Для premultiplied обычно меняют режим блендинга.
                            Обычный блендинг это:
                            dectColor * (1 — srcAlpha) + srcColor * srcAlpha
                            То есть чтобы смешать 2 картинки видеокарта потом все равно делает это умножение на альфу (srcColor * srcAlpha). Поэтому режим смешивания для premultiplied делают таким, чтобы этого умножения не было. Если использовать OpenGL, то вместо glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) просто делают glBlendFunc(GL_SRC_ONE, GL_ONE_MINUS_SRC_ALPHA).
                            Так что чернить оно не будет.
                            Но есть другой неприятный момент, смещение цвета при семплинге. Я об этом написал чуть ниже в комментарии. Он имеет место быть для картинок, которые сильно растягиваются, например текстуры GUI. На выходе можем получить не совсем то, что задумывал дизайнер.
                            • +2

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

                        • 0
                          Premultiply нужно осторожно делать, ибо он меняет изображение, и может все испортить. Покажу на примере:

                          Возмем 2 RGBA пикселя:
                          {1, 0, 0, 0.1} {0, 0, 1, 0.9}
                          Допустим мы семплим ровно между пикселей, это будет что-то типа lerp(c1, c2, 0.5), и пиксель после семплинга будет:
                          {0.5, 0, 0.5, 0.5}

                          А теперь делаем Premultiply для этих пикселей:
                          {0.1, 0, 0, 0.1} {0, 0, 0.9, 0.9}
                          Аналогично семплим, и получаем другой результат:
                          {0.05, 0, 0.45, 0.5}
                          Уже как бы видно, что результат другой, если разделим на 0.5 то получим:
                          {0.1, 0, 0.9, 0.5}, согласитесь, это значительно отличается от {0.5, 0, 0.5, 0.5}
                          • 0

                            Соглашусь, значительно отличается. Не понимаю, что тут испорчено, это верный результат.

                            • 0
                              Если у вас текстура градиента скажем 2*2 пикселя, которую вы растягиваете на 200 * 200, то результат может быть сильно не такой, какой задумывал художник.
                              Предлагаете потом художнику доказывать, что он не прав, и у нас в рендере результат верный?
                              • 0

                                Понял о чем вы. Растягивать текстуру 2*2 — это грубо говоря наложить градиент. И дизайнеру может хотеться, чтобы градиент шел из красного непрозрачного в зеленый прозрачный. При умножении такого трудно добиться. Но это не значит, что такое поведение без умножения — один из вариантов нормы. Нет, правильный скейлинг и субпиксельный рендеринг может быть только с умножением. А то, что вам иногда нужно другое поведение — это изобразительный прием. Изобразительные приемы бывают разные и вы можете реализовать их через шейдеры. К скейлингу это не будет иметь отношения.

                            • 0

                              Другими словами, Premultiply нужно делать там, где художник не проверял результат интерполяции между соседними пикселями?

                          • +2
                            Посмотрите сами на его картинку-пример — эта картинка испорчена!

                            Картинка тут конечно зря приведена, она только запутывает

                      • +5
                        Неважно, какие там RGB, если прозрачность 100%.
                        А если прозрачность 90%? А если 99%? А если у нас формат float'ы использует и прозрачность 99.999%?

                        В какой момент и почему вдруг становится «неважно какие там RGB»?
                        • –2
                          Повторюсь: в статье есть чёткий и понятный ответ (см. пункт «Если вы программист: используйте Premultiplied Alpha!»).
                          • 0
                            причина понятна, а вот вывод сомнительный
                            эту проблему должен решать программист — а именно обойти\заблокировать фильтрацию, а не художник
                        • +4
                          Напоминает недавний спор про гомеопатические препараты. Вещества в нем нет, но на самом деле как бы есть, но очень мало…
                          Хочется просто сказать: «Ты хочешь 0, но принципиально пишешь 255, а потом винишь компьютер, что он не угадал твоё желание — не надо так!»
                    • 0
                      Не подскажете как перевести ARGB->RGB. Вроде бы и примеры в сети есть. Но если одно из значений ARGB 0 — получаются обрытный результат. Допускаю, что у меня дело в кривизне рук и желании 'решить' побыстрому.
                      • +2
                        Не подскажете как перевести ARGB->RGB.

                        Никак. Оно не «переводится». Вы можете наложить ARGB изображение на что-то (сплошной цвет или другое сплошное изображение), после чего альфа везде будет равна единице и тогда вы её сможете отбросить.

                      • –1

                        цвет канала OR (альфа-канал XOR цвет подложки).
                        OR — побитовое ИЛИ
                        XOR — побитовое исключающее ИЛИ
                        В коде будет что-то типа:


                        background = 0xFF # Белая подложка
                        new_red = red | (alpha ^ background)
                    • +5
                      Я по этому поводу даже когда-то тулзу написал. Строю по прозрачным областям DistanceField и заливаю ближайшим цветом из непрозрачной области.
                      Вот например была такая текстура:
                      Заголовок спойлера


                      А вот она без альфаканала. Вверху до, внизу — обработанная моей тулзой:
                      Заголовок спойлера


                      Вот тут исходный код тулзы:
                      https://github.com/MrShoor/AvalancheProject/tree/master/Tools/PNGEdger
                      А вот я загрузил собранную версию (если кому-то понадобится): http://www.gamedev.ru/files/?id=125762
                      • 0
                        По идее же можно заполнить таким образом только полностью прозрачные пиксели, которые соседствуют с не полностью прозрачными. Тогда такие текстуры будут гораздо лучше сжиматься :)
                        • 0
                          Ну тулза вроде это и делает. Насчет сжиматься не уверен (если имеется ввиду размер файла), но мипы по ним строить будет можно без опаски.
                          • 0
                            У вас в примере все прозрачные пиксели заполнены разными цветами. Но по идее достаточно сгенерировать подобные цвета только для тонкой линии вокруг не полностью прозрачных пикселей, а остальное залить одним цветом (прозрачным белым или чёрным, например).
                            • 0

                              Это плохая идея. При разных операциях при формировании конечного пикселя может учитываться разное количество исходных. При ресайзе будут учитываться 2×scale×W исходных пикселей, где scale — коэффициент уменьшения, а W — окно фильтра. При Гауссовом размытии до трех сигма в каждую сторону.

                      • 0

                        Если пиксели строго прозрачные или строго непрозрачные (как в примере с плюсом), можно особо не заморачиваться: достаточно сделать всё прозрачное "прозрачным чёрным" и выбрать подходящую glBlendFunc. (В некоторых графических редакторах "прозрачный чёрный" сам по себе появляется безо всяких усилий со стороны редактирующего).
                        В случае с полупрозрачными пикселями на краях так не прокатит, придётся делать как в статье.

                        • 0
                          А можно поподробнее, т.е. не совсем понял бывает прозрачный черный и прозрачный белый(не черный)? Просто не очень хорошо разбираюсь в цветовых пространствах.
                          • 0

                            Прозрачный черный: 0x00000000 (или 0xff000000, в зависимости от значения, вкладываемого в альфа-канал)
                            Прозрачный белый: 0x00ffffff (или 0xffffffff, в зависимости от значения, вкладываемого в альфа-канал)


                            Это особенность не столько цветового пространства, сколько формата представления цвета.

                        • 0
                          Вот автор пишет формулу для смешивания(линейной интерполяции) RGB цветов,
                          но ведь вроде для корректного смешивания надо возвести в квадрат, смешать, и извлечь корень?
                          https://www.youtube.com/watch?v=LKnqECcg6Gw
                          http://stsievert.com/blog/2015/04/23/image-sqrt/
                          • 0
                            Это нужно только для «гамма-скорректированных» изображений.
                            В играх текстуры вполне могут быть сразу в линейном sRGB. Не делать же гамма-коррекцию для каждой операции, когда разумнее загрузить в линейном виде, сделать фильтрацию, и уже на выводе финального изображения откалибровать для монитора.
                            Современные игры учитывают гамму, претензии скорее нужно направлять к софту: графическим редакторам, браузерам.
                            https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch24.html
                            • 0
                              Не делать же гамма-коррекцию для каждой операции, когда разумнее загрузить в линейном виде, сделать фильтрацию, и уже на выводе финального изображения откалибровать для монитора.
                              Делать. Точность в sRGB будет выше. Иначе зачем бы заморачиваться с sRGB вообще. Сегодня такое конвертирование крайне дешевое, потому что делается аппаратно на видеокарте отдельными блоками.
                              • +2
                                Меня превратно поняли. Мне проще в псевдокоде объяснится:
                                Вместо:
                                Загрузить две картинки
                                
                                Возвести значения цветовых каналов изображений в квадрат
                                  Наложить изображения
                                Найти корень
                                
                                Опять возвести в квадрат
                                  Сделать тонирование
                                Опять найти корень
                                
                                Вывести финальное изображение
                                

                                Так:
                                Загрузить изображения
                                Возвести значения в корень
                                
                                Наложить изображения
                                Тонировать
                                
                                Найти корень
                                Вывести результат
                                

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

                            В википедии есть статья на эту тему
                            https://en.m.wikipedia.org/wiki/Alpha_compositing

                            • –3
                              Удивляет что в редакторах правильно все отображает (масштабирование, то, се) а в движках вылезает.
                              • 0
                                Спасибо, а мы с этими артефактами бодались-бодались, а дело вот в чем!
                                • 0
                                  Наконец-то кто-то это написал нормально. Мы именно так и решали это на нескольких проектах, но так руки и не доходили это нормально написать, теперь есть хорошая дока на которую всех сбрасывать можно. Из дополнений только то, что в WebGl например сжатие текстур поддерживается только DXT3 и DXT5, а они не поддерживают премультиплаед альфу, а экономия видеопамяти в 4 раза стоит того. В итоге приходится делать экструзию(то что в статье предлагается делать через soldify-b) граничных пикселей на этапе экспорта ресуров. В нашем случае пришлось реализовать это самим руками, не так сложно как кажется.
                                  • 0

                                    Добрый день!
                                    Почему то сразу вспомнил сайт UFO. Метод "вставки чёрных кадров" ( https://www.testufo.com/#test=blackframes ) — это то же, что описано в статье?

                                    • –5
                                      Получается, это баг видеокарт. И вы предлагаете его костылить подготовкой изображения. При софтовом рендеринге его легко можно избежать.
                                      • +1
                                        Это принцип работы видеокарт и мониторов (в связке), и предлагается его понимать и уметь правильно с ним работать
                                        • –2
                                          Из статьи я понял что при отображении изображений, если доверять рендеринг чёрному ящику с невнятными алгоритмами отрисовки субпикселей, могут возникнуть баги. Путей избежать баги два. Первый — написать правильное отображение субпикселей, которое умеет использовать маску, или в случае чёрного ящика с багом, обходить его подготовкой изображений (в вашем случае).
                                          Не могу согласиться с вами и назвать баг принципом работы. Это недоработка, которая тянется из игнорирования альфа-канала при субпиксельном рендеринге.
                                          • +1
                                            Из статьи вы должны были понять, что прозрачный пиксель — тоже пиксель, который не рисуется, но используется в вычислених при сглаживании. Сглаживание — не баг видеокарты/рендеринга, а один из аспектов работы.
                                            • –3
                                              Что мешает использовать маску прозрачности, вычисляя прозрачные пиксели?
                                              • +1
                                                Тот факт, что эпоха GIF'ов с одним прозрачным цветом в палитре давно ушла в прошлое и сегодня прозрачный цвет — это цвет с уровнем прозрачности 100% (или, что то же самое, уровнем непрозрасности (opacity) — 0%). И вот если вводить правила, пристойно работающие для прозрачности 90%, 99%, 99.9%, то на 100% они иногда будут давать неожиданные, для неподготовленного человека, результаты.
                                                • –3
                                                  В каком месте я утверждаю, что пиксели должны иметь дискретную прозрачность? Я предлагаю брать ближние пиксели для вычислений из фона, а не из скрытого альфа-слоем основного слоя.
                                        • 0
                                          Получается, это баг видеокарт.

                                          Что вы называете видеокартой? Видеокарта — это набор железа. Еще есть драйвера, графические API, ваш код шейдеров. Все это заставляет видеокарту работать так или иначе. Видеокарта делает то, что ей скажут. Статья о том, как заставить работать правильно.

                                          • –5
                                            Видеокарта — это железо. Понять не могу, как так можно запрограммировать баг, описанный в статье.
                                            Ведь понятно, что усреднять пиксели надо принимая во внимание все слои. В статье же описывается, как не принимать во внимание слой фона, на котором рисуется картинка со сглаживанием и субпиксельным отображением. Но в чём проблема принимать во внимание все слои? Отпадёт необходимость городить кучу описанных костылей.
                                            image
                                            В указанном в статье примере субпиксели какого-то лешего учитывают соседние пиксели только основного слоя, которые должны быть прозрачны под альфа-слоем. Хотя соседние пиксели надо брать из сочетания фона и основного слоя (в примере фон белый, учитывая что прозрачность полная, то совершенно не важно что там в основном слое).

                                            • 0

                                              При чем тут вообще субпиксели? И как вы собираетесь брать сочетание фона и основного слоя — если именно это сочетание и строится неправильно?

                                              • –3
                                                Переходные пиксели по-вашему как называются?
                                                Ошибка в построении вызвана тем, что граничащие пиксели берутся из слоя, который не видно вместо того чтобы браться из фона. Основной слой становится прозрачным через маску альфа-слоя.
                                                Не понимаю, как вы не понимаете, что баг легко поправить, просто учитывая фон, а не городить огород.
                                                • 0

                                                  Предложите формулу, раз это так легко сделать.

                                                  • 0
                                                    Формула та же, ничего не меняется. Просто учитывается фон.

                                                    Берём красный крест 255,0,0 с синим ключевым цветом 0,0,255 (что не важно, учитывая альфа канал, который превращает его в цвет фона)
                                                    Подсчёт пикселей на 50% перекрытии на белом фоне 255,255,255:
                                                    255,0,0 + 255,255,255 => 255,128,128, то есть розовый. Никакого синего или фиолетового.
                                                    • 0

                                                      Это для однородного фона так все просто. А если на фоне уже что-то нарисовано?

                                                      • 0
                                                        А какая разница? Синий цвет, который скрыт маской не участвует в вычислениях. Откройте любой редактор, поддерживающий слои и субпиксельные сдвиги, тот же photoshop. Почему там не наблюдается подобных эффектов?
                                                        • 0

                                                          Разница в том, что прежде чем "просто учитывать цвет фона в формуле", надо понять какой из пикселей фона нужно брать. Вы это забыли сказать.

                                                          • 0
                                                            Точно так же. При перекрытии 30% на 70%, беру 30% левого пикселя фона, 70% правого и смешиваю. Результат считаю пикселем фона. Неоткуда там взяться синему «ключевому» цвету.
                                                            • 0

                                                              Что такое "левый пиксель фона" и "правый пиксель фона"?

                                                              • 0
                                                                image

                                                                На этом изображении указанный пиксель перекрывает 2 пикселя фона. Один неизбежно становится левым, второй правым.
                                                                • 0

                                                                  Подождите, но вы что и на чем рисуете? Мне почему-то на этой картинке виден один пиксель фона, перекрытый двумя пикселями изображения...

                                                                  • 0
                                                                    На картинке уже результат. На изображении, которое мы передвигаем пиксель изображения один и граничит с двумя пикселями фона.
                                                                    • 0

                                                                      То есть вы предлагаете фактически сначала отрисовать фон на изображении — а потом изображение уже без альфа-канала — на фоне?


                                                                      При таком способе отрисовки любое изображение будет немного размывать фон — а это неправильно.

                                                                      • 0
                                                                        То есть правильно смотреть на ключевой синий цвет?
                                                                        • 0

                                                                          Правильно делать как написано в этом посте. Смотреть на синий ключевой цвет вам никто и не предлагает.

                                                                          • 0
                                                                            Как мы видим, проблема возникает, когда положение текстуры не соответствует попиксельно экранным пикселям. Это можно объяснить билинейной фильтрацией, которую видеопроцессор выполняет при рендеринге спрайта на экране: при сэмплировании текстуры видеопроцессор усредняет значения цвета ближайших соседних пикселей с запрошенными координатами в вертикальном и горизонтальном направлениях.


                                                                            В статье предполагается, что сэмплирование происходит перед наложением на фон. Поэтому фон считается неопределённым и возникает артифакт. Я же предлагаю объединить эти процессы. При семплировании брать ближайшие пиксели уже с фона. Откройте photoshop или gimp. Там нет таких проблем. Прозрачный фон считается прозрачным.
                                                                            • 0

                                                                              Нет, в статье предлагается два способа решения проблемы, один из которых — костыль, а второй нормальный.


                                                                              Вы предлагаете более сложный способ.

                                                                              • 0

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

                                                        • +3
                                                          Формула та же, ничего не меняется. Просто учитывается фон.
                                                          Нифига ж себе «просто». А ничего что понятие «фона» в общем случае неопределено?

                                                          Рассмотрите простой пример: возьмите два ваших креста и расположите их друг над другом. Сдвиньте один из них на 1/3 пикселя, а второй — на 2/3. И? Что тут будет с фоном?

                                                          Подсчёт пикселей на 50% перекрытии на белом фоне 255,255,255:
                                                          Если бы в природе был только белый фон и не было бы никакого другого — то никому нафиг бы не сдалась вся эта конструкция. Но что делать, если у вас кроме обьекта и белого фона есть ещё что-нибудь? Другие обьекты, в частности, полупрозрачные.

                                                          Вы уж, пожалуйста, изобретите что-нибудь работающее не только для одного частного случая, а?
                                                          • 0
                                                            Расположите объекты наложения слоями и подсчитывайте по-очереди, считая фоном результат подсчёта. Фон не определён в общем случае. Но во время построения изображения фоном будет самый нижний слой.
                                                            • 0
                                                              Расположите объекты наложения слоями и подсчитывайте по-очереди, считая фоном результат подсчёта.
                                                              А если обьекты того, отказываются «слоями» располагаться? Мы тут про игру, в общем-то говорим.

                                                              Но во время построения изображения фоном будет самый нижний слой.
                                                              Это если он существует. А если у вас обьекты расположены так, что нельзя сказать какой из них «ниже»?
                                                              • 0
                                                                Я уже уловил, за что меня минусуют. Я не уловил контекста статьи. И того что всем непременно надо сэмплировать текстуры до их рендеринга, иначе я несу ересь по мнению большинства.
                                                                И всё же что вы говорите — это те самые условия, которых я предлагаю избегать.

                                                                Ещё во времена экранного режима 0x13h, когда вся видеопамять умещалась в 64к не было необходимости делать всё как у всех, было интересно экспериментировать, можно было с интересом написать свой хитрый движок рендеринга и даже свою поделку (игрой назвать сложно) на этом движке. И вот тогда мои слова про то, что подготовку текстур можно делать непосредственно во время рендеринга сцены, вместе с сэмплированием и фильтрациями были бы уместны.

                                                                Но теперь многие функции положены на видеокарты, что стало стандартом и проще не изобретать велосипед, а идти по проверенному пути, в котором одни грабли обходятся заранее подготовленными костылями и все так делают, о чём собственно статья. Да, в ней всё верно написано. Но с учётом того частного случая, который стал повсеместно употребляться. Статья как раз о том, как готовить текстуры, шлифуя костыль до блеска.
                                            • 0
                                              С этим эффектом столкнулись люди давным-давно-предавно, ещё во времена, когда текстуры были маленькие — и тогда баги были хорошо заметны. Суть: альфа-канал растягивается отдельно, rgb-слои отдельно, и следовательно, из-под альфы по границе обязательно вылазит фон спрайта; а если не дай боже фон был залит каким-то ключевым цветом (ярко-зеленым например), текстуры будут выглядеть просто чудовищно.

                                              Кстати, а ведь маленькие текстуры до сих пор используются. Где? Да везде! Mip-mapping. )) А когда поверх текстуры еще и DXT-сжатие прошлось, баги множатся и расцветают во всей красе.

                                              Вот как мы боролись с этим в Half-Life; по сути, точно такое же заполнение, как рекомендуют здесь в комментариях, но с упрощенным алгоритмом, без distance field.
                                              • –2
                                                Суть: альфа-канал растягивается отдельно, rgb-слои отдельно, и следовательно, из-под альфы по границе обязательно вылазит фон спрайта;

                                                собственно такой должна быть статья плюс картинки, а то воды налили
                                              • 0
                                                Надо будет учесть инфу, спасибо. Начинающим особенно пригодится.

                                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.