Отложенный Alpha blending

В этой статье я хочу поговорить о методах смешивания растеризуемой геометрии. Классические модели смешивания полупрозрачных объектов — Alpha, Additive, Multiplicative — объединяет один и тот же принцип отрисовки: последовательно рисуем один примитив за другим, смешивая получаемые на выходе фрагментного шейдера пиксели с тем, что находится в текущем буфере. Каждый новый примитив обновляет область буфера, в которую рисуется; в случае с альфа-смешиванием объекты, которые находятся выше, заслоняют ранее отрисованные. Но что если хочется что-то сделать с группой объектов, рисуемых поверх сцены, — например, обрезать их по маске или подсветить? Тут сразу в голову приходят два решения: или внести изменения в их материал (т.е. изменить шейдер, расширить набор текстур), к примеру, добавив проекцию еще одной текстуры, которая будет отвечать за маску прозрачности. Однако если у нас много разношерстных объектов, менять каждый уникальный материал неудобно и чревато ошибками. Второй вариант — нарисовать все интересующие нас объекты в отдельный полноэкранный таргет и рисовать уже его на финальную сцену. Тут мы можем сделать с его содержимым все, что захотим, но это требует выделения лишней памяти и, что самое неприятное, — переключения рендер таргетов. Это не самая «дешевая» операция на мобильных устройствах, которую будет необходимо выполнить дважды. А если захочется вот так работать с несколькими слоями?

d50e929b993fd07bbae87c16bef3e65c.png
Есть и другой, более простой и элегантный, способ решить эти проблемы. Мы можем рисовать сцену в обратном порядке!

Небольшое отступление, чтобы напомнить, как работает классический способ отрисовки
При отрисовке на экран чего-либо через Alpha Blending мы смешиваем пиксель, который хотим нарисовать, и пиксель того, что там уже было нарисовано до нас. При этом у нас есть 4 канала RGBA, где RGB — цвет и A (Alpha) — прозрачность (другие форматы нас в данный момент не интересуют). Функция смешивания работает отдельно для трёх цветовых каналов и отдельно для канала прозрачности. Для цвета она выглядит так:
c57cd7ea905d4fc58a9e65c1e604b0e2.png

Здесь ColorSrc — RGB цвет, который мы хотим нарисовать (наша изначальная текстура, результат работы пиксельного шейдера), ColorDst — цвет пикселя в буфере, куда мы рисуем, Color_Result — то, что получилось при смешивании, и то, что будет записано в буфер. Что же такое Variable1 и Variable2, на которые мы умножаем рисуемый цвет и цвет в буфере? Это настраиваемые переменные, динамическая часть формулы (знак плюс тоже можно изменить, но сейчас нам это не нужно). Их значением может быть то, что нам уже известно на данный момент: цвета и прозрачность двух пикселей, заранее заданные константы… 

Классическое смешивание полупрозрачных объектов выглядит так:

aa6d2d0b1a1f02685551030ddf1db356.png

Где AlphaSrc — альфа-канал рисуемого пикселя (прозрачность), OneMinusAlphaSrc, как нетрудно догадаться, значит 1.0 — AlphaSrc. Формула аналогична такой записи: С1 *, а + С2 * (1 — а). В итоге у нас получается линейная интерполяция между двумя цветами через alpha (прозрачность). То есть если альфа = 1, мы перезапишем значение в буфере, а если альфа = 0, старое значение останется неизменным. Подробнее ознакомиться с таблицей возможных значений в OpenGL можно тут.

Эта таблица справедлива и для OpenGL ES 2.0 включительно — за последние пару десятков лет тут принципиально ничего не изменилось.


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

ec587ff0c9f1111e43aa75d7352ee72e.png


В чём же фокус?  


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

Как это работает?


Выше был описан метод смешивания через канал прозрачности изображения, которое рисуем. Теперь мы это повернём наоборот: будем использовать прозрачность уже нарисованных пикселей (а точнее, смешивание рисуемой прозрачности с уже отрисованной). То есть вместо AlphaSrc мы будем использовать AlphaSaturate, а вместо OneMinusAlphaSrc — One. Получается, если в буфере уже лежит что-то с прозрачностью = 1, то вклад будет нулевой, и цвет такого пикселя не изменится. Если там была нулевая прозрачность — сложим оба цвета вместе (для этого нам нужно будет очищать буфер кадра нулями или чёрным цветом с нулевой прозрачностью). При таком сложении результирующий цвет будет равен рисуемому. Итоговая формула выглядит так:
 

14b5cc53798ec10be6a67901a4740fb3.png


(прим. AlphaSaturate = min (AlphaSrc, 1 — AlphaDst))

Значения прозрачности требуется складывать: она должна накапливаться слой за слоем, то есть у нас будет One и One в переменных смешивания для альфа-канала. Почему мы не модифицируем ColorDst и очищаем буфер нулями? Это нужно для Additive-смешивания, AdditiveBlending при этом будет отличаться только тем, что в AlphaSrc переменной у него будет находиться Zero. Он не должен модифицировать прозрачность, только цвет.

Для наглядности, схема обратной отрисовки выглядит так:  

Сперва мы очистим буфер кадра. Затем выставим приведнёную выше функцию смешивания и начнём отрисовку с самых верхних объектов (в классическом подходе они бы рисовались последними), спускаясь к самым нижним. Последним будет нарисовано фоновое изображение.

d21bad2c56508107c644860dff8b9aff.png


Как это можно использовать?


Я опишу несколько задач, решаемых этим методом, на примере нашего проекта:

  1. Отсечение объектов по маске с прозрачностью. Плавное отсечение игровой комнаты:
    d8297dce08eac582b70d49b2f28acbdf.gif

    После отрисовки игрового поля достаточно очистить прозрачность в тех местах изображения, которые мы хотим спрятать. Это делается с помощью формулы смешивания, при которой отрисовываемый объект-маска перезаписывает цвет и прозрачность обратно пропорционально своей собственной прозрачности, а степень очистки можно плавно регулировать. В данном случае для отсечения используется такая геометрия:
    2b709daa3b9e142938838d4386aaf7cb.png

    Она меняет свою форму по мере движения камеры между комнатами. Формула смешивания для очистки следующая:
    ColorSrc = GL_ZERO,
    ColorDst = GL_ONE_MINUS_SRC_ALPHA,
    AlphaSrc = GL_ZERO,
    AlphaDst = GL_ONE_MINUS_SRC_ALPHA

    Можно использовать любую геометрию с любыми текстурами, начиная очистку с того слоя, с которого потребуется:
    0b453899413a5208fe9e90c8c9813f3f.gif
  2. Плавное исчезновение поля делается аналогично. Цена вопроса — один DrawCall.
  3. Тени только на определённых объектах:
    65396cccdc9f687f4156a2115520e50b.gif

    Нам требовалось, чтобы тень рисовалась только на поле с фишками и на некоторых элементах UI, но не на фоне. После отрисовки объектов, «принимающих» тени, рисуем спрайты теней, которые «затемняют цвета», не внося вклада в прозрачность. Таким образом, сразу два зайца убиты: фон без теней, а тень не меняет уровень прозрачности объекта. Блендинг тени такой:
    ColorSrc = GL_SRC_ALPHA,
    ColorDst = GL_ONE_MINUS_SRC_ALPHA,
    AlphaSrc = GL_ZERO,
    AlphaDst = GL_ONE
  4. Засветку, свет и эффект бликов можно имитировать таким же образом:
    d28c3e59add17227f8b03de8d41d2540.gif

    Ещё пример
    ffc82af61e49886eb73c1fd5ff20cbbb.gif
  5. Подсветка для «бустеров»:
    87a908622cdf167a5eaf68969fdf6503.gif

    Туториальная подсветка
    08b68e852abda76a41ba0b6559ff6509.gif

    При этом никаких дополнительных рендер таргетов или специальной геометрии с вырезами не требуется. Можно сделать подсветку из любых спрайтов, в том числе с пересечениями друг между другом. Цена вопроса — 2 DrawCalls.



И в качестве эксперимента имитация Ambient occlusion
00ec3e611e8a55efe7b2a8494235c62c.gif



Не всё так просто


Есть и обратная сторона, а точнее ограничения: не все блендинги можно повторить для такой техники. Alpha-смешивание и additive — точно можно, а вот собственные специальные блендинги придётся или адаптировать или не использовать. Но есть выход: можно разделить этапы отрисовки сцены. Часть выполнить обратным методом, часть — обычным, у нас так и сделано для спецэффектов поверх поля и постпроцесса.

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

c6e4d0fd8db8d8477efe064b04683fe7.png


Это можно побороть, модифицировав аддитивный блендинг в части смешения альфа-канала:

AlphaSrc = GL_ONE_MINUS_DST_ALPHA
AlphaDst = GL_ONE


Но не для всех видов смешивания это подойдёт, и надёжнее будет модифицировать саму текстуру. Что имеется в виду:

Если есть текстуры вида:  

f34946611f5ef341f383f9620291ca75.png
2996784325943157b8494d98a2181cc4.jpg


То из них нужно сделать такие:

7d22d36020fdc1f5cb4f38267ae19a2f.png
be2ecb0b79fbc39b74d698d2337e60d8.png


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

RGB_old = Texel_in.rgb
A_old = Texel_in.a
A_middle = 1.0 / ((RGB_old) / 3.0) * A_old // linear color space
RGB_new = RGB_old * A_middle;
A_shift = minimum( 1.0 / RGB_new.r, 1.0)
A_shift = minimum( 1.0 / RGB_new.g, A_shift)
A_shift = minimum( 1.0 / RGB_new.b, A_shift)
RGB_result = RGB_new * A_shift; 
A_result = (RGB_result) / 3.0)
Texel_out = Vector4(RGB_result, A_result)



Здесь я по шагам разберу отрисовку сцены нашего проекта


Заключение


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

© Habrahabr.ru