Режимы блендинга Photoshop в Unity
Последние пару недель я пытался воспроизвести режимы блендинга Photoshop в Unity. Это непростая задача. Несмотря на достижения современных аппаратных средств для графики, блок блендинга по-прежнему не может быть программируемым и, вероятно, так останется еще некоторое время. Некоторые расширения OpenGL ES реализуют данную функциональность, но большинство аппаратных средств и API-интерфейсов этого не делают. Итак, какие у нас варианты?
1) Копия вторичного буфера
Общий подход заключается в том, чтобы скопировать весь вторичный буфер перед блендингом. Так делает Unity. После этого тривиально реализовать любой нужный блендинг в шейдерном коде. Очевидная проблема заключается в том, что до операции блендинга нужно сделать полную копию вторичного буфера. Конечно, существуют оптимизации, такие как копирование только нужных данных для какой-то меньшей текстуры. Но все усложняется, когда у вас много объектов, использующих режимы блендинга. Еще вы можете сделать только одну копию вторичного буфера и повторно использовать ее, но тогда вы не можете складывать результат блендинга разных объектов друг с другом. В Unity это делается через GrabPass. Это подход, используемый модулем Blend Modes.
2) Улучшение блока блендинга
Современные графические процессоры имеют небольшой блок в конце графического конвейера, называемый Слияние выхода (Output Merger). Это аппаратное обеспечение, ответственное за получение вывода пиксельного шейдера и смешение его со вторичным буфером. Оно не программируется, так как это вызвало бы много сложностей (вы можете почитать об этом здесь) и современные GPU такую возможность не предоставляют.
Формулы режима блендинга были получены здесь и здесь. Используйте их в качестве эталона для сравнения с тем, что я даю. Есть много других источников. Только часто забывают упомянуть, что Photoshop не самом деле использует модифицированные формулы и иное количество зажимов, особенно при работе с альфой. Gimp делает то же самое. Это мой опыт воссоздания режимов блендинга Photoshop исключительно для комбинации блока блендинга и шейдеров. Первые несколько режимов просты, но по мере продвижения, нам придется прибегать к все более хитрым трюкам.
Две оговорки прежде чем мы начнем. Во-первых, режимы блендинга Photoshop выполняются в пространстве sRGB, а это означает, что если вы сделаете это в линейном пространстве, они будут выглядеть неправильно. Как правило, это не проблема. Но из-за количества трюков, которые мы будем делать с этими режимами, многие из значений должны выходить за пределы диапазона 0 – 1. Значит, нам нужен буфер HDR для выполнения вычислений. Unity может сделать это, установив HDR в настройках камеры, а также установив Gamma для цветового пространства в Player Settings. Это явно нежелательно, если вы выполняете вычисления освещения в линейном пространстве. В пользовательском движке вы, вероятно, сможете установить настройки по-иному (чтобы обеспечить линейное освещение).
Если вы хотите испытать код совместно с чтением, скачивайте его тут.
А) Затемнение (Darken)
Формула | min(SrcColor, DstColor) |
Результат шейдера |
color.rgb = lerp(float3(1, 1, 1), color.rgb, color.a); |
Блок блендинга | Min(SrcColor · One, DstColor · One) |
По мере приближения альфа к 0 нам надо устремлять минимальное значение к DstColor, заставляя SrcColor быть максимально возможным цветом float3(1, 1, 1)
B) Умножение (Multiply)
Формула | SrcColor · DstColor |
Результат шейдера |
color.rgb = color.rgb * color.a; |
Блок блендинга | SrcColor · DstColor + DstColor · OneMinusSrcAlpha |
C) Затемнение основы (Color Burn)
Формула | 1 – (1 – DstColor) / SrcColor |
Результат шейдера |
color.rgb = 1.0 - (1.0 / max(0.001, color.rgb * color.a + 1.0 - color.a)); // max чтобы не получилась бесконечность |
Блок блендинга | SrcColor · One + DstColor · OneMinusSrcColor |
D) Линейный затемнитель (Linear Burn)
Формула | SrcColor + DstColor – 1 |
Результат шейдера |
color.rgb = (color.rgb - 1.0) * color.a; |
Блок блендинга | SrcColor · One + DstColor · One |
E) Замена светлым (Lighten)
Формула | Max(SrcColor, DstColor) |
Результат шейдера |
color.rgb = lerp(float3(0, 0, 0), color.rgb, color.a); |
Блок блендинга | Max(SrcColor · One, DstColor · One) |
F) Экран (Screen)
Формула | 1 – (1 – DstColor) · (1 – SrcColor) = Src + Dst – Src · Dst |
Результат шейдера |
color.rgb = color.rgb * color.a; |
Блок блендинга | SrcColor · One + DstColor · OneMinusSrcColor |
G) Осветление основы (Color Dodge)
Формула | DstColor / (1 – SrcColor) |
Результат шейдера |
color.rgb = 1.0 / max(0.01, (1.0 - color.rgb * color.a)); |
Блок блендинга | SrcColor · DstColor + DstColor · Zero |
Вы можете увидеть расхождения между версиями Photoshop и Unity в альфа-блендинге, особенно по краям.
H) Линейный осветлитель (Linear Dodge)
Формула | SrcColor + DstColor |
Результат шейдера |
color.rgb = color.rgb; |
Блок блендинга | SrcColor · SrcAlpha + DstColor · One |
Здесь тоже продемонстрирована “утечка” цвета по краям. Честно говоря, мне больше нравится правая версия, потому что она выглядит более живой. То же относится к Color Dodge. Но это ограничивает отображение 1-к-1 для Photoshop/Gimp.
Все предыдущие режимы блендинга имеют простые формулы, и так или иначе могут быть реализованы с помощью нескольких команд и правильного режима наложения. Однако некоторые режимы блендинга ведут себя в зависимости от условий или имеют сложные выражения (сложные относительно блока блендинга), и нуждаются в переосмыслении. В большинстве случаев необходим двухпроходный подход (с использованием синтаксиса Pass в вашем шейдере). Двухпроходные шейдеры в Unity имеют ограничение в том, что для данного материала не гарантируется, что два прохода будут выполнены прямо один за другим.
Такие режимы опираются на предыдущий проход, поэтому вы получите странные артефакты. Если у вас есть два дублирующих спрайта (как в 2D-игре, например, в нашем случае), сортировка будет неопределенной. Обходной путь для этого заключается в перемещении свойства Order in Layer, чтобы сделать правильную сортировку.
I) Перекрытие (Overlay)
Формула | 1 – (1 – 2 · (DstColor – 0.5)) · (1 – SrcColor), if DstColor > 0.5
2 · DstColor · SrcColor, if DstColor <= 0.5 |
Проход шейдера 1 |
color.rgb *= color.a; float3 A = (4.0 * color.rgb - 1.0) / (2.0 - 4.0 * color.rgb); float3 B = (1.0 - color.a) / ((2.0 - 4.0 * color.rgb) * max(0.001, color.a)); color.rgb = A + B; |
Проход блендинга 1 | SrcColor · DstColor + DstColor · DstColor |
Проход шейдера 2 |
color.rgb = (2.0 - 4.0 * color.rgb * color.a) * max(0.001, color.a); |
Проход блендинга 2 | SrcColor · DstColor + DstColor · Zero |
Требуется пояснить, как я разобрался с Overlay. Мы берем первоначальную формулу и делаем ее аппроксимацию через линейный блендинг:
Упрощаем насколько возможно и получаем вот это
Получить DstColor · DstColor мне удалось только путем изоляции члена и вычисления его в два прохода, при этом вычитая с обеих сторон один и тот же фактор:
Но данная формула не учитывает альфа. По-прежнему необходима линейная интерполяция этой большой формулы для альфа, где альфа = 0 должно возвращать Dst. Тогда
Если включить последний член в первоначальную формулу, ее по-прежнему можно решить в два прохода. Надо проявить внимание и зажать значение альфа с max(0.001, a), потому что есть вероятность деления на ноль. Окончательная формула
J) Мягкий свет (Soft Light)
Формула | 1 – (1 – DstColor) · (1 – (SrcColor – 0.5)), if SrcColor > 0.5
DstColor · (SrcColor + 0.5), if SrcColor <= 0.5 |
Проход шейдера 1 |
float3 A = 2.0 * color.rgb * color.a / (1.0 - 2.0 * color.rgb * color.a); float3 B = (1.0 - color.a) / ((1.0 - 2.0 * color.rgb * color.a) * max(0.001, color.a)); color.rgb = A + B; |
Проход блендинга 1 | SrcColor · DstColor + SrcColor · DstColor |
Проход шейдера 2 |
color.rgb = (1.0 - 2.0 * color.rgb * color.a) * max(0.001, color.a); |
Проход блендинга 2 | SrcColor · DstColor + SrcColor * Zero |
К Soft Light мы применяем практически те же рассуждения, что к Overlay. Это в результате приводит нас к формуле Pegtop. Оба они отличаются от версии Photoshop тем, что у них нет разрывов. В текущей также есть более темная бахрома при альфа-блендинге.
K) Жесткий свет (Hard Light)
Формула | 1 – (1 – DstColor) · (1 – 2 · (SrcColor – 0.5)), if SrcColor> 0.5
DstColor · (2 · SrcColor), if SrcColor <= 0.5 |
Проход шейдера 1 |
float3 A = (2.0 * color.rgb * color.rgb - color.rgb) * color.a; float3 B = max(0.001, (4.0 * color.rgb - 4.0 * color.rgb * color.rgb) * color.a + 1.0 - color.a); color.rgb = A / B; |
Проход блендинга 1 | SrcColor · One + DstColor · One |
Проход шейдера 2 |
color.rgb = max(0.001, (4.0 * color.rgb - 4.0 * color.rgb * color.rgb) * color.a + 1.0 - color.a); |
Проход блендинга 2 | SrcColor · DstColor + SrcColor * Zero |
Hard Light имеет очень тонкий хак, который позволяет ему работать и делать блендинг с альфой. В первом проходе мы делим на магическое число, только чтобы умножить на него же во втором проходе! Потому что когда альфа = 0, должен получиться DstColor и все окрасится в черный.
L) Яркий свет (Vivid Light)
Формула | 1 – (1 – DstColor) / (2 · (SrcColor – 0.5)), if SrcColor > 0.5
DstColor / (1 – 2 · SrcColor), if SrcColor <= 0.5 |
Проход шейдера 1 |
color.rgb *= color.a; color.rgb = color.rgb >= 0.5 ? 1.0 / max(0.0001, 2.0 - 2.0 * color.rgb) : 1.0); |
Проход блендинга 1 | SrcColor · DstColor + SrcColor · Zero |
Проход шейдера 2 |
color.rgb = color.rgb < 0.5 ? (color.a - color.a / max(0.0001, 2.0 * color.rgb)) : 0.0; |
Проход блендинга 2 | SrcColor · One+ SrcColor · OneMinusSrcColor |
M) Линейный свет (Linear Light)
Формула | DstColor + 2 · (SrcColor – 0.5), if SrcColor > 0.5
DstColor + 2 · SrcColor – 1, if SrcColor <= 0.5 |
Результат шейдера |
color.rgb = (2 * color.rgb - 1.0) * color.a; |
Блок блендинга | SrcColor · One + DstColor · One |
Источник: http://www.elopezr.com/photoshop-blend-modes-in-unity/