Pull to refresh
0
Playrix
Разработчик мобильных free-to-play-игр

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

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



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

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


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

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


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

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

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





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


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

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


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

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

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

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

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


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


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

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


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


    Она меняет свою форму по мере движения камеры между комнатами. Формула смешивания для очистки следующая:

    ColorSrc = GL_ZERO,
    ColorDst = GL_ONE_MINUS_SRC_ALPHA,
    AlphaSrc = GL_ZERO,
    AlphaDst = GL_ONE_MINUS_SRC_ALPHA

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

  2. Плавное исчезновение поля делается аналогично. Цена вопроса — один DrawCall.
  3. Тени только на определённых объектах:


    Нам требовалось, чтобы тень рисовалась только на поле с фишками и на некоторых элементах UI, но не на фоне. После отрисовки объектов, «принимающих» тени, рисуем спрайты теней, которые «затемняют цвета», не внося вклада в прозрачность. Таким образом, сразу два зайца убиты: фон без теней, а тень не меняет уровень прозрачности объекта. Блендинг тени такой:

    ColorSrc = GL_SRC_ALPHA,
    ColorDst = GL_ONE_MINUS_SRC_ALPHA,
    AlphaSrc = GL_ZERO,
    AlphaDst = GL_ONE
  4. Засветку, свет и эффект бликов можно имитировать таким же образом:


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


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

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



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




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


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

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


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

AlphaSrc = GL_ONE_MINUS_DST_ALPHA
AlphaDst = GL_ONE

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

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


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


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

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)



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

  2. Отрисовываем то, что под фишками, и подложку, тут и начинается «обратное» смешивание:


  3. Рисуем нужную нам в этом проходе часть UI:

  4. Дальше следует проход, в котором мы рисуем тени, свет, блики для поля:

  5. Рисуем верхние части фона:

  6. Проход основной картинки фона:

  7. Дальше идёт постпроцесс, но в этой статье мы его затрагивать не будем.




Заключение


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

Articles

Change theme settings

Information

Website
plrx.me
Registered
Employees
1,001–5,000 employees
Location
Ирландия