Речь пойдёт о “Weighted blended order-independent transparency” (далее WBOIT) — приёме, описанном в JCGT в 2013 г. (ссылка).
Когда на экране появляется несколько прозрачных объектов, цвет пикселя зависит от того, какой из них находится ближе к наблюдателю. Вот общеизвестная формула смешивания цветов для такого случая:
Для неё важен порядок расположения фрагментов: цвет ближнего фрагмента и его непрозрачность (opacity) обозначены как Cnear и ?, а результирующий цвет всех фрагментов, что расположились за ним — как Cfar. Непрозрачность — это свойство, принимающее значения от 0 до 1, где 0 означает, что объект настолько прозрачен, что его не видно, а 1 — что он настолько непрозрачен, что за ним ничего не видно.
Чтобы использовать эту формулу, нужно сперва отсортировать фрагменты по глубине. Представьте, сколько головной боли с этим связано! В общем случае, сортировку нужно делать в каждом кадре. Если вы сортируете объекты, то некоторые объекты сложной формы придётся разрезать на куски и сортировать по глубине отрезанные части (в частности, для пересекающихся поверхностей это совершенно точно нужно будет делать). Если вы сортируете фрагменты, то сортировка будет происходить в шейдерах. Такой подход называется «Order-independent transparency» (OIT), и в нём используется связный список, хранящийся в памяти видеокарты. Предсказать, сколько памяти придётся выделить под этот список — практически нереально. А если памяти не хватит, на экране появятся артефакты.
Повезло тем, кто может контролировать, сколько полупрозрачных объектов размещается на сцене, и где они относительно друг друга находятся. Но если вы делаете САПР, то прозрачных объектов у вас будет столько, сколько захочет пользователь, и располагаться они будут как попало.
Теперь вы понимаете желание некоторых людей упростить себе жизнь и придумать формулу смешивания цветов, не требующую сортировки. Такая формула есть в статье, на которую я сослался вначале. Там даже есть несколько формул, но самая лучшая по мнению авторов (и по моему мнению тоже) — вот эта:
На скриншоте — группы полупрозрачных треугольников, расположенных в четыре слоя по глубине. Слева они отрендерены с использованием техники WBOIT. Справа — картинка, полученная с использованием формулы (1), классический блендинг цветов с учётом порядка расположения фрагментов. Далее буду называть это CODB (Classic order-dependent blending).
Перед тем, как начать рендеринг прозрачных объектов, мы должны отрендерить все непрозрачные. После этого прозрачные объекты рендерятся с тестом глубины, но без записи в буфер глубины (это делается так:
C0 в формуле (2) — это цвет непрозрачного фрагмента, поверх которого рисуются прозрачные фрагменты, которых у нас n штук, обозначенных индексами с 1 по n. Ci — это цвет i-го прозрачного фрагмента, ?i — его непрозрачность.
Если присмотреться, то формула (2) немножко похожа на формулу (1). Если представить, что — это Cnear, C0 — это Cfar, а — это ?, то это и будет 1-я формула, один в один. И правда, — это взвешенное среднее цветов прозрачных фрагментов (по такой же формуле в механике определяется центр масс), оно сойдёт за цвет ближнего фрагмента Cnear. C0 — это цвет непрозрачного фрагмента, расположенного за всеми фрагментами, для которых мы посчитали это взвешенное среднее, и он сойдёт за Cfar. То есть мы заменили все прозрачные фрагменты одним “усреднённым” фрагментом и применили стандартную формулу смешивания цветов — формулу (1). Что же это за хитрая формула для ?, которую нам предлагают авторы оригинальной статьи?
Это скалярная функция в n-мерном пространстве, так что вспомним дифференциальный анализ функций нескольких переменных. Учитывая что все ?i принадлежат диапазону от 0 до 1, частная производная по любой из переменных всегда будет неотрицательной константой. Значит, opacity «усреднённого» фрагмента возрастает при увеличении opacity любого из прозрачных фрагментов, а это именно то, что нам надо. К тому же, она возрастает линейно.
Если непрозрачность какого-то фрагмента равна 0, то его вообще не видно, он не оказывает влияния на результирующий цвет.
Если непрозрачность хотя бы одного фрагмента равна 1, то ? равно 1. То есть непрозрачный фрагмент становится невидим, что в общем-то хорошо. Только вот прозрачные фрагменты, расположенные за фрагментом с opacity=1, всё еще просвечивают через него и влияют на результирующий цвет:
Тут сверху лежит синий треугольник, под ним зелёный, а под зелёным — белый и циановый, и всё это на чёрном фоне. У синего непрозрачность = 1, у всех остальных — 0,5. Картинка справа — это то, что должно быть. Как видите, WBOIT смотрится отвратительно. Единственное место, где проступает нормальный синий цвет — это кромка зелёного треугольника, обведённая непрозрачной белой линией. Как я только что сказал, непрозрачный фрагмент невидим, если opacity прозрачного фрагмента равна 1.
Ещё лучше это видно вот здесь:
Оранжевый треугольник имеет непрозрачность 1, зелёный с отключённой прозрачностью просто рисуется вместе с непрозрачными объектами. Выглядит это так, как будто ЗЕЛЁНЫЙ треугольник просвечивает ОРАНЖЕВЫМ цветом через оранжевый треугольник.
Чтобы картинка выглядела достойно, проще всего не назначать объектам высокую непрозрачность. В своём рабочем проекте я не позволяю задавать opacity больше 0,5. Это 3D CAD, в котором объекты рисуются схематично, и особого реализма не требуется, так что там допустимо такое ограничение.
С низкими значениями непрозрачности картинки слева и справа смотрятся почти одинаково:
А с высокими они заметно различаются:
Вот так выглядит прозрачный многогранник:
У многогранника оранжевые боковые и зелёные горизонтальные грани. К сожалению, с первого взгляда это не поймёшь, т.е. картинка не выглядит убедительно. Там, где спереди оказывается оранжевая стенка, нужно больше оранжевого, и там, где зелёная — больше зелёного. Будет гораздо лучше рисовать грани одним цветом:
Чтобы хоть как-то возместить отсутствие сортировки по глубине, авторы статьи придумали несколько вариантов добавления глубины в формулу (2). Это делает реализацию сложнее, а результат — менее предсказуемым и зависящим от особенностей конкретной трёхмерной сцены. Я не стал углубляться в эту тему, так что кому интересно — предлагаю ознакомиться со статьёй.
Утверждается, что WBOIT иногда способен на то, чего не может классическая прозрачность с сортировкой. Например, вы рисуете дым как систему частиц, используя только две частицы — с тёмным и светлым дымом. Когда одна частица проходит через другую, классическое смешивание цветов с сортировкой даёт некрасивый результат — цвет дыма из светлого резко становится тёмным. В статье говорится, что WBOIT с учётом глубины позволяет добиться плавного перехода и выглядит правдоподобнее. То же можно сказать про моделирования меха и волос в виде тонких трубок.
Теперь о том, как реализовать формулу (2) на OpenGL. Код примера лежит на Гитхабе (ссылка), и большая часть картинок в статье — оттуда. Можете собрать и поиграться с моими треугольничками. Используется фреймворк Qt.
Тем, кто только приступает к изучению рендеринга прозрачных объектов, рекомендую вот эти две статьи:
> Learn OpenGL. Урок 4.3 — Смешивание цветов
> Алгоритм Order-Independent Transparency c использованием связных списков на Direct3D 11 и OpenGL 4
Вторая, правда, не так важна для понимания этого материала, но первая обязательна к прочтению.
Чтобы вычислить формулу (2), нам понадобятся 2 дополнительных фреймбуфера, 3 multisample текстуры и рендербуфер, в который мы будем записывать глубину. В первую текстуру — colorTextureNT (NT означает non-transparent) — мы будем рендерить непрозрачные объекты. Она имеет тип GL_RGB16F. Вторая текстура (colorTexture) будет иметь тип GL_RGBA16F; в первые 3 компонента этой текстуры мы будем записывать вот этот кусок формулы (2): , в четвёртый — . Ещё одна текстура типа GL_R16F (alphaTexture) будет содержать .
Сначала надо создать эти объекты получить от OpenGL их идентификаторы:
Как я уже сказал, тут используется фреймворк Qt, и все вызовы OpenGL проходят через объект типа QOpenGLFunctions_4_5_Core, который у меня везде обозначается как f.
Теперь следует выделить память:
И настроить фреймбуфферы:
На втором проходе рендеринга вывод из фрагментного шейдера пойдёт сразу в две текстуры, и это надо явно указать с помощью glDrawBuffers.
Большая часть этого кода выполняется один раз, при старте программы. Код, выделяющий память под текстуры и рендербуфферы, вызывается при каждом изменении размера окна. Далее пойдёт код рендеринга, который вызывается каждый раз при перерисовке окна.
Только что мы нарисовали все непрозрачные объекты на текстуре colorTextureNT, а глубины записали в рендербуффер. Перед тем, как использовать тот же самый рендербуффер на следующем этапе рисования, надо убедиться, что туда уже записаны все глубины непрозрачных объектов. Для этого используется GL_FRAMEBUFFER_BARRIER_BIT. После рендеринга прозрачных объектов мы вызовем функцию ApplyTextures(), которая запустит финальную стадию рендеринга, на которой фрагментный шейдер будет считывать данные из текстур colorTextureNT, colorTexture и alphaTexture, чтобы применить формулу (2). Текстуры к тому моменту должны быть полностью записаны, поэтому перед вызовом ApplyTextures() мы применяем GL_TEXTURE_FETCH_BARRIER_BIT.
defaultFBO — это фреймбуфер, через который мы выводим изображение на экран. В большинстве случаев это 0, но в Qt это QOpenGLWidget::defaultFramebufferObject().
При каждом вызове фрагментного шейдера у нас будет информация о цвете и непрозрачности текущего фрагмента. Но на выходе в текстуре colorTexture мы хотим получить сумму (а в текстуре alphaTexture — произведение) неких функций от этих величин. Для этого используется блендинг. Причём, коль скоро для первой текстуры мы вычисляем сумму, а для второй — произведение, настройки блендинга (glBlendFunc и glBlendEquation) для каждого аттачмента надо задавать отдельно.
Вот содержимое функции PrepareToTransparentRendering():
И содержимое функции CleanupAfterTransparentRendering():
В моём фрагментном шейдере непрозрачность обозначается буквой w. Произведение цвета на w и саму w мы выводим в один выходной параметр, а 1 – w — в другой. Для каждого выходного параметра задаётся layout qualifier в виде «location = X», где X — индекс элемента в массиве аттачментов, который мы в 3-м листинге передали функции glDrawBuffers (конкретно, выходной параметр с location = 0 отправляется в текстуру, привязанную к GL_COLOR_ATTACHMENT0, а параметр с location = 1 — в текстуру, привязанную к GL_COLOR_ATTACHMENT1). Те же самые числа используются в функциях glBlendFunci и glBlendEquationi, чтобы указать номер аттачмента, для которого мы устанавливаем параметры блендинга.
Фрагментный шейдер:
В функции ApplyTextures() мы просто рисуем прямоугольник на всё окно. Фрагментный шейдер запрашивает данные всех сформированных нами текстур, используя текущие экранные координаты в качестве текстурных координат и текущий номер сэмпла (gl_SampleID) в качестве номера сэмпла в multisample текстуре. Использование переменной gl_SampleID в шейдере автоматически включает режим, когда фрагментный шейдер вызывается один раз для каждого сэмпла (в нормальных условиях он вызывается один раз для всего пикселя, а результат записывается во все сэмплы, что оказались внутри примитива).
В вершинном шейдере нет ничего примечательного:
Фрагментный шейдер:
И наконец — содержимое функции ApplyTextures():
Ну и хорошо бы освободить OpenGL ресурсы после того как всё закончилось. У меня этот код вызывается в деструкторе моего OpenGL-виджета:
Когда на экране появляется несколько прозрачных объектов, цвет пикселя зависит от того, какой из них находится ближе к наблюдателю. Вот общеизвестная формула смешивания цветов для такого случая:
Для неё важен порядок расположения фрагментов: цвет ближнего фрагмента и его непрозрачность (opacity) обозначены как Cnear и ?, а результирующий цвет всех фрагментов, что расположились за ним — как Cfar. Непрозрачность — это свойство, принимающее значения от 0 до 1, где 0 означает, что объект настолько прозрачен, что его не видно, а 1 — что он настолько непрозрачен, что за ним ничего не видно.
Чтобы использовать эту формулу, нужно сперва отсортировать фрагменты по глубине. Представьте, сколько головной боли с этим связано! В общем случае, сортировку нужно делать в каждом кадре. Если вы сортируете объекты, то некоторые объекты сложной формы придётся разрезать на куски и сортировать по глубине отрезанные части (в частности, для пересекающихся поверхностей это совершенно точно нужно будет делать). Если вы сортируете фрагменты, то сортировка будет происходить в шейдерах. Такой подход называется «Order-independent transparency» (OIT), и в нём используется связный список, хранящийся в памяти видеокарты. Предсказать, сколько памяти придётся выделить под этот список — практически нереально. А если памяти не хватит, на экране появятся артефакты.
Повезло тем, кто может контролировать, сколько полупрозрачных объектов размещается на сцене, и где они относительно друг друга находятся. Но если вы делаете САПР, то прозрачных объектов у вас будет столько, сколько захочет пользователь, и располагаться они будут как попало.
Теперь вы понимаете желание некоторых людей упростить себе жизнь и придумать формулу смешивания цветов, не требующую сортировки. Такая формула есть в статье, на которую я сослался вначале. Там даже есть несколько формул, но самая лучшая по мнению авторов (и по моему мнению тоже) — вот эта:
На скриншоте — группы полупрозрачных треугольников, расположенных в четыре слоя по глубине. Слева они отрендерены с использованием техники WBOIT. Справа — картинка, полученная с использованием формулы (1), классический блендинг цветов с учётом порядка расположения фрагментов. Далее буду называть это CODB (Classic order-dependent blending).
Перед тем, как начать рендеринг прозрачных объектов, мы должны отрендерить все непрозрачные. После этого прозрачные объекты рендерятся с тестом глубины, но без записи в буфер глубины (это делается так:
glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE);
). То есть, вот что происходит в точке с некими экранными координатами (x, y): прозрачные фрагменты, оказавшиеся ближе непрозрачного, проходят тест глубины, независимо от того, как они располагаются по глубине относительно уже нарисованных прозрачных фрагментов, а прозрачные фрагменты, оказавшиеся дальше непрозрачного, не проходят тест глубины, и, соответственно, отбрасываются.C0 в формуле (2) — это цвет непрозрачного фрагмента, поверх которого рисуются прозрачные фрагменты, которых у нас n штук, обозначенных индексами с 1 по n. Ci — это цвет i-го прозрачного фрагмента, ?i — его непрозрачность.
Если присмотреться, то формула (2) немножко похожа на формулу (1). Если представить, что — это Cnear, C0 — это Cfar, а — это ?, то это и будет 1-я формула, один в один. И правда, — это взвешенное среднее цветов прозрачных фрагментов (по такой же формуле в механике определяется центр масс), оно сойдёт за цвет ближнего фрагмента Cnear. C0 — это цвет непрозрачного фрагмента, расположенного за всеми фрагментами, для которых мы посчитали это взвешенное среднее, и он сойдёт за Cfar. То есть мы заменили все прозрачные фрагменты одним “усреднённым” фрагментом и применили стандартную формулу смешивания цветов — формулу (1). Что же это за хитрая формула для ?, которую нам предлагают авторы оригинальной статьи?
Это скалярная функция в n-мерном пространстве, так что вспомним дифференциальный анализ функций нескольких переменных. Учитывая что все ?i принадлежат диапазону от 0 до 1, частная производная по любой из переменных всегда будет неотрицательной константой. Значит, opacity «усреднённого» фрагмента возрастает при увеличении opacity любого из прозрачных фрагментов, а это именно то, что нам надо. К тому же, она возрастает линейно.
Если непрозрачность какого-то фрагмента равна 0, то его вообще не видно, он не оказывает влияния на результирующий цвет.
Если непрозрачность хотя бы одного фрагмента равна 1, то ? равно 1. То есть непрозрачный фрагмент становится невидим, что в общем-то хорошо. Только вот прозрачные фрагменты, расположенные за фрагментом с opacity=1, всё еще просвечивают через него и влияют на результирующий цвет:
Тут сверху лежит синий треугольник, под ним зелёный, а под зелёным — белый и циановый, и всё это на чёрном фоне. У синего непрозрачность = 1, у всех остальных — 0,5. Картинка справа — это то, что должно быть. Как видите, WBOIT смотрится отвратительно. Единственное место, где проступает нормальный синий цвет — это кромка зелёного треугольника, обведённая непрозрачной белой линией. Как я только что сказал, непрозрачный фрагмент невидим, если opacity прозрачного фрагмента равна 1.
Ещё лучше это видно вот здесь:
Оранжевый треугольник имеет непрозрачность 1, зелёный с отключённой прозрачностью просто рисуется вместе с непрозрачными объектами. Выглядит это так, как будто ЗЕЛЁНЫЙ треугольник просвечивает ОРАНЖЕВЫМ цветом через оранжевый треугольник.
Чтобы картинка выглядела достойно, проще всего не назначать объектам высокую непрозрачность. В своём рабочем проекте я не позволяю задавать opacity больше 0,5. Это 3D CAD, в котором объекты рисуются схематично, и особого реализма не требуется, так что там допустимо такое ограничение.
С низкими значениями непрозрачности картинки слева и справа смотрятся почти одинаково:
А с высокими они заметно различаются:
Вот так выглядит прозрачный многогранник:
У многогранника оранжевые боковые и зелёные горизонтальные грани. К сожалению, с первого взгляда это не поймёшь, т.е. картинка не выглядит убедительно. Там, где спереди оказывается оранжевая стенка, нужно больше оранжевого, и там, где зелёная — больше зелёного. Будет гораздо лучше рисовать грани одним цветом:
WBOIT с учётом глубины
Чтобы хоть как-то возместить отсутствие сортировки по глубине, авторы статьи придумали несколько вариантов добавления глубины в формулу (2). Это делает реализацию сложнее, а результат — менее предсказуемым и зависящим от особенностей конкретной трёхмерной сцены. Я не стал углубляться в эту тему, так что кому интересно — предлагаю ознакомиться со статьёй.
Утверждается, что WBOIT иногда способен на то, чего не может классическая прозрачность с сортировкой. Например, вы рисуете дым как систему частиц, используя только две частицы — с тёмным и светлым дымом. Когда одна частица проходит через другую, классическое смешивание цветов с сортировкой даёт некрасивый результат — цвет дыма из светлого резко становится тёмным. В статье говорится, что WBOIT с учётом глубины позволяет добиться плавного перехода и выглядит правдоподобнее. То же можно сказать про моделирования меха и волос в виде тонких трубок.
Код
Теперь о том, как реализовать формулу (2) на OpenGL. Код примера лежит на Гитхабе (ссылка), и большая часть картинок в статье — оттуда. Можете собрать и поиграться с моими треугольничками. Используется фреймворк Qt.
Тем, кто только приступает к изучению рендеринга прозрачных объектов, рекомендую вот эти две статьи:
> Learn OpenGL. Урок 4.3 — Смешивание цветов
> Алгоритм Order-Independent Transparency c использованием связных списков на Direct3D 11 и OpenGL 4
Вторая, правда, не так важна для понимания этого материала, но первая обязательна к прочтению.
Чтобы вычислить формулу (2), нам понадобятся 2 дополнительных фреймбуфера, 3 multisample текстуры и рендербуфер, в который мы будем записывать глубину. В первую текстуру — colorTextureNT (NT означает non-transparent) — мы будем рендерить непрозрачные объекты. Она имеет тип GL_RGB16F. Вторая текстура (colorTexture) будет иметь тип GL_RGBA16F; в первые 3 компонента этой текстуры мы будем записывать вот этот кусок формулы (2): , в четвёртый — . Ещё одна текстура типа GL_R16F (alphaTexture) будет содержать .
Сначала надо создать эти объекты получить от OpenGL их идентификаторы:
f->glGenFramebuffers (1, &framebufferNT );
f->glGenTextures (1, &colorTextureNT );
f->glGenRenderbuffers(1, &depthRenderbuffer);
f->glGenFramebuffers(1, &framebuffer );
f->glGenTextures (1, &colorTexture);
f->glGenTextures (1, &alphaTexture);
Как я уже сказал, тут используется фреймворк Qt, и все вызовы OpenGL проходят через объект типа QOpenGLFunctions_4_5_Core, который у меня везде обозначается как f.
Теперь следует выделить память:
f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT);
f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples,
GL_RGB16F, w, h, GL_TRUE );
f->glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer);
f->glRenderbufferStorageMultisample( GL_RENDERBUFFER, numOfSamples,
GL_DEPTH_COMPONENT, w, h );
f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture);
f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples,
GL_RGBA16F, w, h, GL_TRUE );
f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture);
f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples,
GL_R16F, w, h, GL_TRUE );
И настроить фреймбуфферы:
f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT);
f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT, 0
);
f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
GL_RENDERBUFFER, depthRenderbuffer
);
f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D_MULTISAMPLE, colorTexture, 0
);
f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1,
GL_TEXTURE_2D_MULTISAMPLE, alphaTexture, 0
);
GLenum attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
f->glDrawBuffers(2, attachments);
f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
GL_RENDERBUFFER, depthRenderbuffer
);
На втором проходе рендеринга вывод из фрагментного шейдера пойдёт сразу в две текстуры, и это надо явно указать с помощью glDrawBuffers.
Большая часть этого кода выполняется один раз, при старте программы. Код, выделяющий память под текстуры и рендербуфферы, вызывается при каждом изменении размера окна. Далее пойдёт код рендеринга, который вызывается каждый раз при перерисовке окна.
f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT);
// ... код рендеринга ...
Только что мы нарисовали все непрозрачные объекты на текстуре colorTextureNT, а глубины записали в рендербуффер. Перед тем, как использовать тот же самый рендербуффер на следующем этапе рисования, надо убедиться, что туда уже записаны все глубины непрозрачных объектов. Для этого используется GL_FRAMEBUFFER_BARRIER_BIT. После рендеринга прозрачных объектов мы вызовем функцию ApplyTextures(), которая запустит финальную стадию рендеринга, на которой фрагментный шейдер будет считывать данные из текстур colorTextureNT, colorTexture и alphaTexture, чтобы применить формулу (2). Текстуры к тому моменту должны быть полностью записаны, поэтому перед вызовом ApplyTextures() мы применяем GL_TEXTURE_FETCH_BARRIER_BIT.
static constexpr GLfloat clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
static constexpr GLfloat clearAlpha = 1.0f;
f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
f->glClearBufferfv(GL_COLOR, 0, clearColor);
f->glClearBufferfv(GL_COLOR, 1, &clearAlpha);
f->glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT);
PrepareToTransparentRendering();
{
// ... код рендеринга ...
}
CleanupAfterTransparentRendering();
f->glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT);
f->glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO);
ApplyTextures();
defaultFBO — это фреймбуфер, через который мы выводим изображение на экран. В большинстве случаев это 0, но в Qt это QOpenGLWidget::defaultFramebufferObject().
При каждом вызове фрагментного шейдера у нас будет информация о цвете и непрозрачности текущего фрагмента. Но на выходе в текстуре colorTexture мы хотим получить сумму (а в текстуре alphaTexture — произведение) неких функций от этих величин. Для этого используется блендинг. Причём, коль скоро для первой текстуры мы вычисляем сумму, а для второй — произведение, настройки блендинга (glBlendFunc и glBlendEquation) для каждого аттачмента надо задавать отдельно.
Вот содержимое функции PrepareToTransparentRendering():
f->glEnable(GL_DEPTH_TEST); f->glDepthMask(GL_FALSE);
f->glDepthFunc(GL_LEQUAL);
f->glDisable(GL_CULL_FACE);
f->glEnable(GL_MULTISAMPLE);
f->glEnable(GL_BLEND);
f->glBlendFunci(0, GL_ONE, GL_ONE);
f->glBlendEquationi(0, GL_FUNC_ADD);
f->glBlendFunci(1, GL_DST_COLOR, GL_ZERO);
f->glBlendEquationi(1, GL_FUNC_ADD);
И содержимое функции CleanupAfterTransparentRendering():
f->glDepthMask(GL_TRUE);
f->glDisable(GL_BLEND);
В моём фрагментном шейдере непрозрачность обозначается буквой w. Произведение цвета на w и саму w мы выводим в один выходной параметр, а 1 – w — в другой. Для каждого выходного параметра задаётся layout qualifier в виде «location = X», где X — индекс элемента в массиве аттачментов, который мы в 3-м листинге передали функции glDrawBuffers (конкретно, выходной параметр с location = 0 отправляется в текстуру, привязанную к GL_COLOR_ATTACHMENT0, а параметр с location = 1 — в текстуру, привязанную к GL_COLOR_ATTACHMENT1). Те же самые числа используются в функциях glBlendFunci и glBlendEquationi, чтобы указать номер аттачмента, для которого мы устанавливаем параметры блендинга.
Фрагментный шейдер:
#version 450 core
in vec3 color;
layout (location = 0) out vec4 outData;
layout (location = 1) out float alpha;
layout (location = 2) uniform float w;
void main()
{
outData = vec4(w * color, w);
alpha = 1 - w;
}
В функции ApplyTextures() мы просто рисуем прямоугольник на всё окно. Фрагментный шейдер запрашивает данные всех сформированных нами текстур, используя текущие экранные координаты в качестве текстурных координат и текущий номер сэмпла (gl_SampleID) в качестве номера сэмпла в multisample текстуре. Использование переменной gl_SampleID в шейдере автоматически включает режим, когда фрагментный шейдер вызывается один раз для каждого сэмпла (в нормальных условиях он вызывается один раз для всего пикселя, а результат записывается во все сэмплы, что оказались внутри примитива).
В вершинном шейдере нет ничего примечательного:
#version 450 core
const vec2 p[4] = vec2[4](
vec2(-1, -1), vec2( 1, -1), vec2( 1, 1), vec2(-1, 1)
);
void main() { gl_Position = vec4(p[gl_VertexID], 0, 1); }
Фрагментный шейдер:
#version 450 core
out vec4 outColor;
layout (location = 0) uniform sampler2DMS colorTextureNT;
layout (location = 1) uniform sampler2DMS colorTexture;
layout (location = 2) uniform sampler2DMS alphaTexture;
void main() {
ivec2 upos = ivec2(gl_FragCoord.xy);
vec4 cc = texelFetch(colorTexture, upos, gl_SampleID);
vec3 sumOfColors = cc.rgb;
float sumOfWeights = cc.a;
vec3 colorNT = texelFetch(colorTextureNT, upos, gl_SampleID).rgb;
if (sumOfWeights == 0)
{ outColor = vec4(colorNT, 1.0); return; }
float alpha = 1 - texelFetch(alphaTexture, upos, gl_SampleID).r;
colorNT = sumOfColors / sumOfWeights * alpha +
colorNT * (1 - alpha);
outColor = vec4(colorNT, 1.0);
}
И наконец — содержимое функции ApplyTextures():
f->glActiveTexture(GL_TEXTURE0);
f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT);
f->glUniform1i(0, 0);
f->glActiveTexture(GL_TEXTURE1);
f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture);
f->glUniform1i(1, 1);
f->glActiveTexture(GL_TEXTURE2);
f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture);
f->glUniform1i(2, 2);
f->glEnable(GL_MULTISAMPLE); f->glDisable(GL_DEPTH_TEST);
f->glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
Ну и хорошо бы освободить OpenGL ресурсы после того как всё закончилось. У меня этот код вызывается в деструкторе моего OpenGL-виджета:
f->glDeleteFramebuffers (1, &framebufferNT);
f->glDeleteTextures (1, &colorTextureNT);
f->glDeleteRenderbuffers(1, &depthRenderbuffer);
f->glDeleteFramebuffers (1, &framebuffer);
f->glDeleteTextures (1, &colorTexture);
f->glDeleteTextures (1, &alphaTexture);
GCU
Интересно было бы сравнить результат с простым аддитивным смешиванием (возможно с premultiplied alpha).
clubs Автор
Если просто из формулы (2) убрать деление на , то выглядит это так:
Вы это имели в виду?
OldFisher
Думаю, имелось в виду накопление прямо в буфере цвета, когда вместо смешения (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) используется накопление (GL_ONE, GL_ONE), причём цвет прозрачного объекта умножается на альфу. Это, как я понимаю, позволяет получить примерно те же результаты (хотя и с пониженной точностью), но в обычном цветовом буфере вместо сложной конструкции из фреймбуферов, описанной в статье, да ещё и без постпроцессинга.
clubs Автор
Если я правильно понял, то это и есть то, что я нарисовал в предыдущем комментарии. Без деления на получается взвешенная сумма цветов вместо взвешенного среднего. Т.е. мы умножаем цвет каждого фрагмента на его альфу, складываем всё это и выводим на экран. Сложная конструкция из фреймбуферов нужна как раз для того, чтобы, во-первых, поделить на , во-вторых, особым образом смешать цвета прозрачных и непрозрачных объектов, раз уж мы решили рисовать их отдельно.
WBOIT, как и обычный блендинг, — это интерполяция; они не допускают того, чтобы цвет вышел за рамки смешиваемых цветов, поэтому они и используются для рендеринга прозрачных объектов. Аддитивное смешивание, насколько я знаю, рекомендуется применять для рисования источников света (частиц огня, например). Оно приводит к бесконтрольному осветлению цветов. Чем больше фрагментов смешивается, тем светлее получается результирующий цвет, вплоть до белого (в большинстве случаев). Это видно на картинке из предыдущего комментария. Единственное, что есть общего с WBOIT — это то, что нет необходимости в сортировке.
GCU
Да, это то, что я имел ввиду — накопление как с освещением.
Поскольку приведённые примеры на чёрном фоне — этого должно было быть достаточно для наглядной картинки с наложением.
Это очень простая техника — например ещё до шейдеров в древнем OpenGL прошлого века был буфер накопления (glAccum) c расширенным диапазоном для цветов.
Безусловно есть и недостатки, но на мой взгляд интересно сравнить как это работало 20 лет назад железно и сейчас с шейдерами.
Чтобы быстро не вываливаться в белый — лучше смешивать цвета в линейном цветовом пространстве (переводить из sRGB, работать с линейным, на выводе перевести назад в sRGB — это вроде включается расширением).
Можно добавить некий аналог экспозиции вместо обрезания значений сверху (clamp), выводить 1.0 — exp(-k*c), где с — это значение цвета, a k — некий коэффициент, подобранный чтобы было не слишком светло/темно.