[Перевод] Как работает альфа-композитинг

image


Возможно, прозрачность не кажется какой-то интересной темой. Формат GIF, позволявший некоторым пикселям просвечивать сквозь фон, опубликован более 30 лет назад. Почти в каждом приложении для графического дизайна, выпущенном за последние два десятка лет, поддерживается создание полупрозрачного контента. Эти понятия давно перестали быть чем-то новым.

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


Если вам когда-нибудь доводилось смотреть сквозь розовые очки, то вы могли видеть нечто подобное тому, что показано на рисунке ниже. [В оригинале статьи многие изображения интерактивны.] Попробуйте перемещать очки, чтобы посмотреть, как они влияют на то, что сквозь них видно:

tuzomzqhbl9mcfzbxxcaneot3aq.png


Подобные очки работают следующим образом: они пропускают много красного цвета, приличное количество синего и совсем мало зелёного. Математику этих очков можно записать набором из трёх уравнений. Буква R обозначает результат операции, а буква D описывает точку, на которую мы смотрим. Индексы RGB обозначают красный, зелёный и синий компоненты:

RR = DR × 1.0
RG = DG × 0.7
RB = DB × 0.9


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

Симуляция поведения обычных солнцезащитных очков гораздо проще, они обычно просто ослабляют падающий свет на какую-нибудь величину:

qflq8kkgpnh__pk8lpv5rx6txya.png


Эти очки пропускают только 30% проходящего через них света. Их поведение можно описать следующими уравнениями:

RR = DR × 0.3
RG = DG × 0.3
RB = DB × 0.3


Все три компонента цвета уменьшены на одинаковое значение — поглощение падающего света одинаково. Мы можем сказать, что тёмные очки на 30% прозрачны (transparent) или что они на 70% непрозрачны (opaque). Непрозрачность (Opacity) объекта определяет, сколько цвета он блокирует. В компьютерной графике мы обычно имеем дело с упрощённой моделью, в которой для описания этого свойства нужно только одно значение. Непрозрачность может пространственно варьироваться. как, например, столб дыма, который становится выше всё прозрачнее.

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


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

ebca016ed191611e29f68dfca5aebe62.svg


Растеризация векторной фигуры в битовую карту

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

mpknpgk1lauj0ruzaobm6azr2wy.png


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

Можно сэмплировать векторную геометрию несколько раз на пиксель, чтобы получить большую градацию шагов и решить, что некоторые пиксели закрыты только частично. Одно из возможных решений: использовать четыре точки сэмплирования, позволяющие представить пять уровней покрытия: 0, 1⁄4, 2⁄4, 3⁄4 и 1:

hiuupwrv88cjcfjxv5_f-b1hege.png


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

image


Отрезок прямой делит квадрат на трапецию и прямоугольник

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

xjpdfwcvkdgujdwittnyzxan6ce.png


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

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

69ab931ef9b9eeb4f669f29652044c0d.png


В рендеринге текста активно применяется частичное покрытие

Имея непрозрачность объекта и покрытие им отдельных пикселей, можно соединить их в одно значение.


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

альфа = непрозрачность × покрытие


Объект с непрозрачностью 60%, покрывающий 30% площади пикселя, имеет в этом пикселе значение альфы 18%. Естественно, что когда объект прозрачен или совершенно не покрывает пиксель, то значение альфы в этом пикселе равно 0. После перемножения различия между непрозрачностью и покрытием пропадают, что в некотором смысле оправдывает то, что понятия «альфа» и «непрозрачность» используются как синонимы.

Альфу часто представляют в виде четвёртого канала битового изображения. Обычные значения красного, зелёного и синего дополняются значением альфы, образуя четвёрку значений RGBA.

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

qrxtpr2tyhpn1znd0brdi8ehhns.png


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

jm8abvbbl5mejxely1nbhefyv8u.png


Очевидно, что чем больше бит, тем лучше, и чаще всего для альфы используется битовая глубина 8 для соответствия точности компонентов цвета, из-за чего многие RGBA-буферы занимают по 32 бита на пиксель. Стоит также заметить, что в отличие от компонентов цвета, которые часто кодируются при помощи нелинейного преобразования, альфа хранится линейно — закодированное значение 0.5 соответствует значению альфы 0.5.

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

tc_wkf6dtsblbjvxx1nslytlu2u.gif


Заметьте, что он ведёт себя не так, как цветное стекло. Стекло просто блокирует часть фонового освещения с различной яркостью. Если посмотреть на абсолютно чёрный объект через розовое стекло, то его чернота сохранится, потому что чёрный объект не испускает и не отражает никакого света. Однако полупрозрачный розовый объект добавляет своего собственного света. Если разместить его поверх чёрного объекта, то результат окажется розоватым. Хорошим аналогом такого поведения является взвешенный в воздухе мелкий материал, например дымка, дым, туман или какой-нибудь цветной порошок.

Визуализировать альфа-канал немного труднее — идеально прозрачный объект невидим по определению, поэтому чтобы различать объекты, нам нужно воспользоваться двумя трюками. Шахматный фон показывает, какие части изображения прозрачные; этот паттерн используется во многих графических приложениях:

6b4ff5064b6cd9262e4d381125b406c4.svg


Шахматный паттерн показывает прозрачные части

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

c9ac12c335263552b56dda8f3bd35262.svg


Отображение значений RGB и A на разных поверхностях

Чем ярче оттенок серого, тем выше значение альфы, то есть чистый чёрный соответствует 0% альфы, а чистый белый — 100% альфы. Небольшие квадраты показывают, что компоненты RGB и A изображения разделены на две части.

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


Очень немногие эффекты 2D-рендеринга можно реализовать одной операцией, и для создания готового результата мы используем процесс композитинга, сочетающего различные изображения. Например, простую кнопку «Cancel» можно создать композитингом пяти отдельных элементов:

cd52d180f6aba101c8fd2bf95bd721fe.svg


Элементы композитинга кнопки «Cancel»

Композитинг часто выполняется в несколько этапов, на каждом из которых комбинируются два изображения. Используемое в композитинге изображение переднего плана обычно называют source. Используемое в композитинге фоновое изображение, на которое накладывается source, обычно называют destination.

Мы начнём с композитинга на непрозрачный фон, потому что это очень распространённый случай. Всё, что вы видите на экране в конечном итоге накладывается композитингом на непрозрачный destination.

Когда значение альфы у source равно 100%, то source непрозрачен и должен полностью закрывать destination. Если значение альфы равно 0%, то source полностью прозрачен и он никак не влияет на destination. Значение альфы 25% позволяет объекту испустить 25% своего света и пропускает 75% света от фона, и так далее:

985719921b6dc06496caba46db08a5c5.svg


Композитинг фиолетовых source с разными значениями альфы на жёлтый destination

Вы можете уже понять, к чему всё идёт — простой случай альфа-композитинга на непрозрачный фон — это просто линейная интерполяция между цветами destination и source. На показанном ниже графике ползунок управляет значением альфы source, а красный, зелёный и синий графики отображают значения RGB-компонентов. Результат R — это просто смешение между source S и destination D:

fdbw7xdswtd_k6crz13wprtqbtm.gif


Происходящее здесь можно описать показанными ниже уравнениями. Как и раньше, индекс обозначает компонент, то есть SA — это значение альфы в source, а DG — значение зелёного в destination:

RR = SR × SA + DR × (1 − SA)

RG = SG × SA + DG × (1 − SA)

RB = SB × SA + DB × (1 − SA)


Уравнения для красного, зелёного и синего компонентов имеют одинаковый вид, поэтому можно просто использовать индекс RGB и соединить их в одну строку:

RRGB = SRGB × SA + DRGB × (1 − SA)


Более того, поскольку destination непрозрачен и уже блокирует весь фоновый свет, мы знаем, что значение альфы у результата всегда равно 1:

RA = 1


Композитинг на непрозрачный фон выполняется просто, но он довольно ограничен в возможностях. Во многих случаях требуется более надёжное решение.
На изображении ниже показан двухэтапный процесс композитинга трёх разных слоёв, помеченных как A, B и C. Символ ⇨ будет обозначать «накладывается композитингом на»:

6a9fb8ca58b71a7327785e10b54ef8da.svg


Результат двухэтапного композитинга трёх слоёв

Сначала мы накладываем композитингом B на C, а затем накладываем на них A, чтобы получить готовое изображение. В следующем примере мы выполним всё немного иначе. Сначала мы соединим композитингом два верхних слоя, а затем наложим результат на последний destination:

a99a10e409562f903fa7e66b4a363706.svg


Результат двухэтапного композитинга трёх слоёв в другом порядке

Вы наверно задаётесь вопросом, возникает ли такая ситуация на практике, но на самом деле она очень распространена. Многие нетривиальные операции композитинга и эффекты рендеринга, например, маскирование и размытие, требуют проходить через промежуточный буфер, содержащий только частичные результаты композитинга. Эта концепция имеет разные названия: внеэкранные проходы (offscreen passes), слои прозрачности (transparency layers) или побочные буферы (side buffers), но обычно в их основе лежит одинаковая идея.

Для нас важнее то, что почти любое изображение с прозрачностью можно воспринимать как частичный результат какого-нибудь рендеринга, который позже будет наложен композитингом на последний destination:

8622cc804d280c508244ccfc60c0e461.svg


Частичный композитинг кнопки в буфер

Нам нужно понять, как заменить композитинг полупрозрачных изображений A и B одним изображением (A⇨B), имеющим тот же цвет и непрозрачность. Давайте начнём с вычисления значения альфы конечного буфера.


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

Допустим, некое количество света проходит через первый объект, а затем через второй объект. Если прозрачность первого объекта равна 80%, то он пропустит 80% падающего света. Аналогично этому, второй объект с прозрачностью 60% пропустит 60% проходящего через него света, что даёт нам 60% × 80% = 48% от исходного света. Можете поэкспериментировать с прозрачностью в оригинале статьи; не забывайте, что ползунки управляют прозрачностью, а не непрозрачностью объектов на пути света:

avf5lvv7kve1zkgnztkd_i_hylq.png


Естественно когда или первый, или второй объект непрозрачны, никакой свет через них не проходит, даже другой полностью прозрачен.

Если объект D имеет прозрачность DT, а объект S имеет прозрачность ST, то окончательная общая прозрачность RT этих двух объектов равна их произведению:

RT = DT × ST


Однако прозрачность — это просто единица минус альфа, поэтому подстановка даёт нам следующее:

1 − RA = (1 − DA) × (1 − SA)


Это выражение можно развернуть в такое:

1 − RA = 1 − DA − SA + DA × SA


И упростить так:

RA = DA + SA − DA × SA


Его можно сократить до одного из двух аналогичных видов:

RA = SA + DA × (1 − SA)

RA = DA + SA × (1 − DA)


Вскоре мы увидим, что чаще используется второй. Интересно также заметить, что получившееся значение альфы не зависит от относительного порядка объектов — непрозрачность получившихся пикселей одинакова, даже если поменять source и destination местами. Это очень логично. Свет проходящий через два объекта, должен затухать одинаково, с какой стороны бы о ни светил: спереди или сзади.
Вычислить альфу оказалось не так сложно, поэтому давайте попробуем разобраться в вычислениях RGB-компонентов. Изображение source имеет цвет SRGB, но его непрозрачность SA заставляет учитывать в готовом результате только произведение этих двух значений:

SRGB×SA


Изображение destination имеет цвет DRGB, непрозрачность заставляет его испускать свет DRGB×DA, однако часть света блокируется непрозрачностью изображения S, поэтому всё влияние destination равно:

DRGB×DA×(1 − SA)


Общий вклад света от S и D равен их сумме:

SRGB×SA + DRGB×DA×(1 − SA)


Аналогично, вклад объединённых слоёв равен их цвету, умноженному на их непрозрачность:

RRGB×RA


Мы хотим, чтобы эти два значения совпадали:

RRGB×RA = SRGB×SA + DRGB×DA×(1 − SA)


Что даёт нам окончательные уравнения:

RA = SA + DA × (1 − SA)

RRGB = (SRGB×SA + DRGB×DA×(1 − SA)) / RA


Посмотрите, насколько сложным получилось второе уравнение! Заметьте, что для получения значений RGB результата нам нужно выполнить деление на значение альфы. Однако для последующего этапа композитинта снова потребуется умножение на значение альфы, потому что результат текущей операции станет новым source или destination следующей операции. Это просто некрасиво.

Давайте на секунду вернёмся к почти финальному виду RRGB:

RRGB×RA = SRGB×SA + DRGB×DA×(1 − SA)


Source, destination и результат умножаются на их альфа-компоненты. Это даёт нам понять, что цвету и альфе пикселя «нравится» быть вместе, поэтому нужно сделать шаг назад и переосмыслить способ хранения информации о цвете.
Вспомните, что мы говорили о непрозрачности — если объект частично непрозрачен, то его вклад в результат тоже будет частичным. Концепция Premultiplied alpha («предварительное умножение на альфу») реализует эту идею. Значения RGB-компонентов, как понятно из названия, предварительно умножаются на альфа-компонент. Начнём с цвета без предварительного умножения:

(1.00, 0.80, 0.30, 0.40)


Предварительное умножение на альфу даёт нам следующее:

(0.40, 0.32, 0.12, 0.40)


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

e20ba7338feb27cdf22f3c25d3afa697.svg


Информация RGB и A в изображении без предварительного умножения

Заметьте, что области, где альфа равна 0, могут иметь произвольные значения RGB, как видно по зелёным и голубым глитчам на изображении. В случае предварительного умножения на альфу информация о цвете также хранит значения непрозрачности пикселя:

6135d9ede6ea6789bbcd979e48a2cce0.svg


Информация RGB и A в предварительно умноженном изображении

Premultiplied alpha иногда называют associated alpha, а не-premultiplied alpha иногда называют straight или unassociated alpha.

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

(0.0, 0.0, 0.0, 0.0)


В случае premultiplied alpha существует только один полностью прозрачный цвет, и это очаровательно.

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

Фильтрация


Гауссово размытие — это популярный способ создания интересного расфокусированного фона или снижения высокой частоты фоновой части содержимого некоторых элементов UI. Как мы увидим, предварительное умножение альфы критически важно для создания правильно выглядящего размытия.

Изображение, которое мы будем анализировать, создано заполнением фона непрозрачным на 1% синим цветом, поверх которого нарисован непрозрачный красный круг. Сначала давайте рассмотрим пример без предварительного умножения. Я отделил каналы RGB от канала альфы, чтобы было понятно, что происходит. Стрелка обозначает операцию размытия:

1ce0eb7f9342c4d90ca7c18bb827afba.png


Размытие содержимого без предварительного умножения

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

Когда цвета предварительно умножены на альфу, результат получается правильным:

2c69f8aa9cba7a689eb2094d89661576.png


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

Из-за предварительного умножения синий цвет изображения уменьшается до 1% от его исходной силы, поэтому его влияние на цвета размытого круга чрезвычайно мало.

Интерполяция


Рендеринг изображения, пиксели которого идеально сопоставляются с destination — это простая задача, потому что между сэмплами нам нужно выполнять тривиальное сопоставление «один к одному». Проблема возникает, когда не существует простого сопоставления, например, из-за поворота, масштабирования или переноса. На рисунке ниже видно, что пиксели повёрнутого изображения, обозначенные красным контуром, больше не соответствуют destination:

a7c55cd9d320d2fc5b8b4dd80622b54c.svg


Относительная ориентация изображения и пиксели destination до и после поворота

Существует множество способов выбора цвета из изображения, который должен быть записан в пиксель destination, и самый простой из них — это так называемая «интерполяция по ближайшему соседу» (nearest-neighbor interpolation), при которой в качестве конечного пикселя просто выбирается ближайший сэмпл в текстуре.

В показанной ниже демонстрации красным контуром показана позиция изображения в destination. Справа показаны позиции сэмплов с точки зрения изображения. Перетаскивая ползунок (в оригинале статьи), можно поворачивать четырёхугольник и наблюдать за тем, как сэмплы выбирают цвета из битовой карты. Я выделил один пиксель в source и destination, чтобы их связь была нагляднее:

zd0vifjvna1jjf_apdomph8bmd0.png


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

s0fyqirq3p9cpyfb5vl4gxzsz0q.png


Так получается лучше, но края вокруг прямоугольников выглядят неправильно, содержимое пикселей без умножения сливается, потому что альфа «применяется» после интерполяции. Рекомендуемое иногда решение слияния цвета верного содержимого, которое показано в потрясающей статье Адриана Корреже [перевод на Хабре], далеко от идеала — ни один цвет в зазоре между красным и синим прямоугольниками не будет выглядеть правильно.

Давайте посмотрим, как всё будет выглядеть при изображении с premultiplied alpha и композитинге с усовершенствованной формулой, которую мы вскоре выведем:

toaxzyjkgdouyossh7kzgkgudyi.png


Просто идеально — мы избавились от всех слияний цветов и нигде не видно зубцов.

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


Вернёмся к композитингу. Мы остановились на почти выведенном уравнении:

RRGB×RA = SRGB×SA + DRGB×DA×(1 − SA)


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

RRGB = SRGB + DRGB×(1 − SA)


Давайте рассмотрим уравнение альфы:

RA = SA + DA × (1 − SA)


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

R = S + D × (1 − SA)


Посмотрите, насколько premultiplied alpha всё упростила. Когда мы анализируем компоненты уравнения, все они находятся на своём месте. Операция маскирует часть фонового света и прибавляет новый свет:

R = S + D × (1 − SA)


Такую операцию смешения называют source-over, sover или просто normal, и она, без всяких сомнений, является самым распространённым режимом композитинга. Почти всё, что вы видите на моём веб-сайте, смешано в этом режиме.

Ассоциативность


Важным свойством source-over, выполняемым над предварительно умноженными на альфу цветами, является ассоциативность этой операции. Благодаря нему в сложном уравнении смешения мы можем расставлять скобки совершенно произвольно. Все показанные ниже композиции эквивалентны:

R = (((A⇨B)⇨C)⇨D)⇨E

R = (A⇨B)⇨(C⇨(D⇨E))

R = A⇨(B⇨(C⇨(D⇨E)))


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

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


В июле 1984 года Томас Портер и Том Дафф опубликовали оригинальную статью «Compositing Digital Images». Авторы не только впервые ввели понятие premultiplied alpha и вывели уравнение композитинга source-over, но и представили целое семейство операций альфа-композитинга, многие из которых малоизвестны, хоть и очень полезны. Новые функции также называют операторами, потому что аналогично сложению или умножению они выполняют действия с входными значениями для создания выходного значения.

Over


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

vxnbn3qhwfuiobes7rws0ardxtq.gif


R = S + D × (1 − SA)


R = S × (1 − DA) + D


Если переключиться в destination-over, то вы сразу поймёте, что он просто «переворачивает» source-over — destination и source меняются в уравнении местами и результат эквивалентен тому, что мы будем считать destination изображением source. Хотя он кажется излишним, оператор destination-over чрезвычайно полезен, потому что он позволяет выполнять композитинг объектов, находящихся под уже существующим содержимым.

Out


Операторы source-out и destination-out отлично подходят для пробивания отверстий в source или destination:

c1pnoudiz66kdmddiaqkmdxcyuq.gif


R = S × (1 − DA)


R = D × (1 − SA)


Из этих двух операторов более удобным является Destination-out, потому что использует альфа-канал для пробивания отверстий в форме destination.

In


Операторы source-in и destination-in по сути являются операторами маскирования:

fxjejc-my5ewd7otz8_r2ylueas.gif


R = S × DA


R = D × SA


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

Atop


Операторы source-atop и destination-atop позволяют накладывать новое содержимое на уже существующее, одновременно маскируя его по контуру destination:

jsf-2on9uxoowdzpk73ugiadypw.gif


R = S × DA + D × (1 − SA)


R = S × (1 − DA) + D × SA


Xor


Оператор исключающего ИЛИ (xor) сохраняет или source, или destination, а их совпадающие области исчезают:

bmi6uoidqbrytp4e4gbmy6afvck.png


R = S × (1 − DA) + D × (1 − SA)


Source, Destination, Clear


Последние три классических режима композитинга довольно скучны. Source, также называемый copy, просто берёт цвет source. Аналогично, destination игнорирует цвет source и просто возвращает destination. Оператор clear просто всё очищает:

fwlimouoegmu9ff1r1tqno-uags.gif


R = S


R = D


R = 0


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

qf-lctetuaygx6y7nwxhl95breu.gif


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

Операторы


Если присмотреться к операторам Портера-Даффа, то можно заметить, что все они имеют одинаковый вид. Source всегда умножается на некий коэффициент FS и прибавляется к destination, умноженному на коэффициент FD:

R = S×FS + D×FD


FS может принимать значения 0, 1, DA и 1 − DA, а FD может быть равным 0, 1, SA или 1 − SA. Не имеет смысла умножать source или destination на их собственные альфы, потому что они уже предварительно умножены, и мы просто получим причудливый, но не особо полезный эффект квадратичной альфы. Все операторы можно представить в виде таблицы:
Обратите внимание на симметрию операторов по диагонали. Четыре центральных элемента в таблице отсутствуют и так получилось потому, что они отличаются от остальных.
В своей статье Портер и Дафф представили ещё один оператор, при котором и FS, и FD равны 1. Он известен под названиями plus, lighter и plus-lighter:

R = S + D


Эта операция по сути прибавляет освещение source к destination:

a2fa9b31c91913bc0445b91ea8217670.png


Аддитивное освещение, реализованное при помощи оператора plus

Зелёный и красный правильно образуют жёлтый, а зелёный и синий образуют голубой (cyan). Чёрный цвет — это отсутствие операции, он никак не изменяет цветовые значения, потому что прибавление к числу нуля ничего не меняет.

Трём оставшимся операторам не дали названий, потому что они не особенно полезны. Они являются просто комбинацией маскирования и смешения.

Также стоит заметить, что premultiplied alpha позволяет нам использовать оператор source-over непредусмотренным образом. Давайте снова взглянем на уравнение:

R = S + D × (1 − SA)


Если нам удастся сделать значение альфы в source равным нулю, то при наличии ненулевых значений в RGB-каналах мы можем добиться аддитивного освещения без использования оператора plus:

40042bfbf25d98ec04a5609fcf828f39.png


Аддитивное освещение, реализованное при помощи оператора source-over

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

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


Давайте взглянем на этот простой рисунок пилюли, составленный всего из шести примитивов:

6e7b44c84ff94dfbe3e0f2943cd44b89.png


Рисуем пилюлю при помощи простых фигур

Если бы нас попросили отрендерить пилюлю с непрозрачностью 50%, то у нас могло бы возникнуть искушение просто разделить пополам непрозрачность каждой операции отрисовки, но это окажется ошибочным решением:

© Habrahabr.ru