После того, как фрагментный шейдер обработал фрагмент, выполняется так называемый тест трафарета (или «тестирование трафарета»), который, как и тестирование глубины, имеет возможность отбрасывать некоторые фрагменты. После тестирования трафарета оставшиеся фрагменты передаются для тестирования глубины, во время которого OpenGL может еще отбросить часть из оставшихся фрагментов. Тест трафарета основан на информации из дополнительного буфера, называемого буфером трафарета, значения которого мы можем изменять во время рендеринга, получая тем самым довольно интересные графические эффекты.
Буфер трафарета
Буфер трафарета (обычно) содержит 8 бит на каждое значение трафарета, что в общей сложности составляет 256 различных значений трафарета на пиксель. Мы можем сами задавать данные значения трафарета для каждого отдельно взятого фрагмента, указывая тем самым на то, какие фрагменты будут отброшены, а какие — оставлены.
Примечание: Каждая оконная библиотека должна настраивать для вас буфер трафарета. GLFW выполняет это автоматически, но другие оконные библиотеки по умолчанию могут и не создавать буфер трафарета, поэтому обязательно проверьте документацию вашей библиотеки.
Простой пример буфера трафарета показан ниже:
В начале происходит очищение буфера трафарета путем заполнения его нулями, а затем в буфере трафарета сохраняется открытый прямоугольник, составленный из единиц. В результате этого визуализируются только те фрагменты сцены, у которых значение трафарета соответствующего фрагмента равно 1
(остальные фрагменты при этом отбрасываются).
Операции с буфером трафарета позволяют нам устанавливать различные значения данного буфера везде, где мы визуализируем фрагменты. Изменяя содержимое буфера трафарета во время рендеринга, мы записываем данные в буфер трафарета. В том же (или последующих) кадре (кадрах) мы можем прочитать эти значения, чтобы оставить или отбросить определенные фрагменты. При использовании буфера трафарета общая последовательность действий выглядит следующим образом:
Шаг №1: Включить запись в буфер трафарета.
Шаг №2: Визуализировать объекты, обновляя содержимое буфера трафарета.
Шаг №3: Отключить запись в буфер трафарета.
Шаг №4: Визуализировать (другие) объекты, но теперь отбрасывая определенные фрагменты на основе содержимого буфера трафарета.
Таким образом, используя буфер трафарета, мы можем отбросить определенные фрагменты, зависящие от других объектов сцены.
Вы можете включить тестирование трафаретов, используя GL_STENCIL_TEST
. С этого момента все вызовы рендеринга так или иначе будут влиять на буфер трафарета.
1 |
glEnable(GL_STENCIL_TEST); |
Обратите внимание, что буфер трафарета (точно так же, как и буфер цвета/глубины) нужно очищать на каждой итерации цикла рендеринга:
1 |
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); |
Кроме того, аналогично операции тестирования глубины с функцией glDepthMask(), для буфера трафарета тоже существует подобная функция. Функция glStencilMask() позволяет нам задать битовую маску, выполняющую операцию побитового сравнения И с соответствующим значением трафарета, которое будет записано в буфер. По умолчанию значение битовой маски устанавливается равным 0xFF
(все биты равны 1
), тем самым не оказывая никакого влияния на выходные данные, но если бы мы установили это значение равным 0x00
, то все значения трафарета, записанные в буфер, были бы нулевыми. Следующие строки являются эквивалентами тестирования глубины при применении функции glDepthMask(GL_FALSE)
:
1 2 |
glStencilMask(0xFF); // каждый бит записывается в буфер трафарета как есть glStencilMask(0x00); // каждый бит в буфере трафарета становится нулем (отключение записи) |
В большинстве случаев в качестве маски трафарета вы будете использовать только значения 0x00
или 0xFF
, но также полезно будет знать, что существуют варианты установки пользовательских битовых масок.
Трафаретные функции
Подобно тестированию глубины у нас есть возможность контролировать, когда тест трафарета должен проходить успешно, а когда — нет, и как он должен повлиять на буфер трафарета. Существует в общей сложности две функции, которые мы можем использовать для настройки тестирования трафаретов: glStencilFunc() и glStencilOp().
Функция glStencilFunc(GLenum func, GLint ref, GLuint mask)
имеет три параметра:
func
— задает функцию, используемую при тесте трафарета, которая будет определять, остается ли фрагмент или отбрасывается. Данная функция тестирования применяется к сохраненному значению трафарета и ref
-значению функции glStencilFunc(). Возможные варианты: GL_NEVER
, GL_LESS
, GL_LEQUAL
, GL_GREATER
, GL_GEQUAL
, GL_EQUAL
, GL_NOTEQUAL
и GL_ALWAYS
. Как видно из названий, назначения этих функций аналогичны назначениям функций буфера глубины.
ref
— задает эталонное значение для теста трафарета. Содержимое буфера трафарета сравнивается с данным значением.
mask
— указывает маску, которая сопоставляется в операции побитового И как с эталонным значением, так и с сохраненным значением трафарета перед их сравнением. По умолчанию значение равно 1
.
Таким образом, в случае простого примера трафарета, который мы рассмотрели в начале статьи, функция будет иметь следующий вид:
1 |
glStencilFunc(GL_EQUAL, 1, 0xFF) |
Таким образом, мы сообщаем OpenGL, что всякий раз, когда значение трафарета фрагмента равно (GL_EQUAL
) эталонному значению 1
(т.е. значению ref
), фрагмент успешно проходит тест, и выполняется его визуализация, в противном случае — он отбрасывается.
Но функция glStencilFunc() описывает только необходимость для OpenGL на основе содержимого буфера трафарета оставлять или отбрасывать фрагменты, а не то, как мы можем фактически обновить буфер. В таком случае нам на помощь приходит функция glStencilOp().
Функция glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)
содержит три параметра, для каждого из которых мы можем указать, какие действия следует выполнить:
sfail
— действие, которое нужно выполнить, если тест трафарета не пройден.
dpfail
— действие, которое нужно выполнить, если тест трафарета пройден, но тест глубины — нет.
dppass
— действие, которое нужно предпринять, если пройдены и тест трафарета, и тест глубины.
Затем, для каждого из вариантов, вы можете выполнить любое из следующих действий:
Действие | Описание |
GL_KEEP | Сохраняется текущее значение трафарета. |
GL_ZERO | Значение трафарета устанавливается равным 0. |
GL_REPLACE | Значение трафарета заменяется значением ref, установленным с помощью функции glStencilFunc(). |
GL_INCR | Значение трафарета увеличивается на 1, если оно меньше максимального значения. |
GL_INCR_WRAP | То же, что и GL_INCR, с той лишь разницей, что, при превышении максимального значения, значение сбрасывается в 0. |
GL_DECR | Значение трафарета уменьшается на 1, если оно превышает минимальное значение. |
GL_DECR_WRAP | То же, что и GL_DECR, но значение становится максимальным, если оно оказывается ниже 0. |
GL_INVERT | Побитовое инвертирование текущего значения буфера трафарета. |
По умолчанию значения аргументов функции glStencilOp() установлены в (GL_KEEP, GL_KEEP, GL_KEEP)
, поэтому, независимо от результата любого из тестов, буфер трафарета сохраняет свои значения. Поведение по умолчанию не обновляет буфер трафарета, поэтому, если вы хотите сделать запись в буфер трафарета, то необходимо указать по крайней мере одно отличающееся от стандартного действие для любого из параметров.
Таким образом, используя функции glStencilFunc() и glStencilOp(), мы можем точно определить, когда и как нужно обновить буфер трафарета, а также в каких случаях оставлять фрагменты, а в каких — отбрасывать.
Очерчивание объекта
Тяжело сходу понять принципы работы тестирования трафаретов, поэтому давайте рассмотрим особо полезную функцию, называемую очерчиванием объектов, которая может быть реализована только с помощью тестирования трафаретов.
Очерчивание объекта делает именно то, о чем говорится в его названии. Для каждого объекта (или для одиночного) мы создадим небольшую цветную границу по его периметру. Например, данный эффект будет особенно полезен вам, когда пользователь, играя в стратегическую игру, захочет выбрать подручные юниты, и вам нужно будет отобразить пользователю, какие из этих юнитов были выбраны.
Процедура очерчивания ваших объектов выглядит следующим образом:
Шаг №1: Включите запись в трафарет.
Шаг №2: Перед рисованием объектов (которые требуется очертить) установите параметр GL_ALWAYS
в функции glStencilOp(), обновляя при этом буфер трафарета с помощью единиц везде, где должны отрисовываться фрагменты объектов.
Шаг №3: Визуализируйте объекты.
Шаг №4: Отключите запись в трафарет и тестирование глубины.
Шаг №5: Немного отмасштабируйте каждый из объектов.
Шаг №6: Используйте дополнительный фрагментный шейдер, выводящий выбранный сплошной цвет (границу).
Шаг №7: Нарисуйте объекты снова, но только в том случае, если значения трафарета их фрагментов не равны 1
.
Шаг №8: Снова включите тестирование глубины и восстановите функцию трафарета, указав значение GL_KEEP
.
Данный процесс заполняет содержимое буфера трафарета единицами для каждого фрагмента объекта, и когда приходит время рисовать границы, мы рисуем масштабированные версии объектов только там, где проходит тест трафарета. При этом все фрагменты масштабированных версий, которые являются частью фрагментов исходных объектов, отбрасываются при использовании буфера трафарета.
Поэтому сначала мы создадим очень простой фрагментный шейдер, который выводит цвет границы. Зафиксируем заданное значение цвета и вызовем шейдер shaderSingleColor
:
1 2 3 4 |
void main() { FragColor = vec4(0.04, 0.28, 0.26, 1.0); } |
Используя сцену из предыдущего урока, мы добавим контур к двум объектам-контейнерам, а пол оставим нетронутым. Сначала мы отобразим пол, затем два контейнера (с записью в буфер трафарета), а затем отобразим масштабированные контейнеры (с отбрасыванием фрагментов, которые находятся поверх ранее нарисованных фрагментов контейнера).
Сначала нам нужно включить тестирование трафаретов:
1 |
glEnable(GL_STENCIL_TEST); |
Затем в каждом кадре нам нужно указать действие, которое будет выполняться всякий раз, когда какой-либо из тестов трафарета будет возвращать успешный или неудачный результат:
1 |
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); |
Если какой-либо из тестов не проходит, мы ничего не делаем; просто сохраняем текущее значение, находящееся в буфере трафарета. Однако, если и тест трафарета, и тест глубины будут успешными, мы заменим сохраненное значение трафарета на значение ref
, установленное через glStencilFunc() (которое мы позже установим равным 1
).
Далее, в начале кадра, очищаем буфер трафарета с помощью 0
и для контейнера обновляем буфер трафарета значением 1
для каждого нарисованного фрагмента:
1 2 3 4 5 |
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); glStencilFunc(GL_ALWAYS, 1, 0xFF); // все фрагменты должны пройти тест трафарета glStencilMask(0xFF); // включаем запись в буфер трафарета normalShader.use(); DrawTwoContainers(); |
Используя в функции glStencilFunc() параметр GL_REPLACE
, мы следим за тем, чтобы каждый из фрагментов контейнеров обновлял буфер трафарета значением трафарета 1
. Поскольку фрагменты всегда проходят тест трафарета, то буфер трафарета обновляется значением ref
везде, где мы их нарисовали.
Теперь, когда буфер трафарета обновлен с помощью единиц в тех местах, где были нарисованы контейнеры, мы отобразим масштабированные контейнеры, но на этот раз с соответствующей функцией тестирования и отключением записи в буфер трафарета:
1 2 3 4 5 |
glStencilFunc(GL_NOTEQUAL, 1, 0xFF); glStencilMask(0x00); // отключаем запись в буфер трафарета glDisable(GL_DEPTH_TEST); shaderSingleColor.use(); DrawTwoScaledUpContainers(); |
Устанавливаем функцию трафарета в GL_NOTEQUAL
, чтобы убедиться, что визуализируем только те части контейнеров, которые не равны 1
. Таким образом, мы отображаем только ту часть контейнеров, которая находится вне ранее нарисованных контейнеров. Обратите внимание, что мы также отключаем тестирование глубины, чтобы масштабированные контейнеры (например, границы) не были перезаписаны полом. Обязательно после всего этого не забудьте включить буфер глубины.
Общая процедура очерчивания объекта для нашей сцены выглядит примерно так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
glEnable(GL_DEPTH_TEST); glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); glStencilMask(0x00); // убеждаемся, что мы не обновляем буфер трафарета во время рисования пола normalShader.use(); DrawFloor() glStencilFunc(GL_ALWAYS, 1, 0xFF); glStencilMask(0xFF); DrawTwoContainers(); glStencilFunc(GL_NOTEQUAL, 1, 0xFF); glStencilMask(0x00); glDisable(GL_DEPTH_TEST); shaderSingleColor.use(); DrawTwoScaledUpContainers(); glStencilMask(0xFF); glStencilFunc(GL_ALWAYS, 1, 0xFF); glEnable(GL_DEPTH_TEST); |
Если вы понимаете общую идею тестирования трафаретов, то вышеприведенный фрагмент кода не должен вызвать у вас затруднений. В противном случае попробуйте еще раз внимательно прочитать предыдущие разделы и попытаться полностью понять, как работает каждая из функций.
Результатом работы алгоритма очерчивания будет следующая сцена:
С полным исходным кодом алгоритма очерчивания объекта вы можете ознакомиться ниже.
GitHub / Урок №20. Тест трафарета в OpenGL — Исходный код
Примечание: Вы можете заметить, что между обоими контейнерами границы перекрываются, что обычно является желаемым эффектом (вообразите стратегическую игрушку, где мы хотим выбрать 10 юнитов; в таких случаях слияние границ является предпочтительным эффектом). Если вам нужна цельная граница для каждого отдельно взятого объекта, то необходимо очистить буфер трафаретов для каждого объекта и подойти к использованию буфера глубины более креативно.
Алгоритм выделения объектов, который вы могли наблюдать, обычно используется в играх для визуализации выбранных объектов (вспомните стратегические игры), и такой алгоритм легко может быть реализован в классе Model. Вы можете задать флаг логического типа данных в классе Model, чтобы иметь возможность выбора режима отображения объектов: с границами, либо без них. Можно проявить творческий подход и придать границам более естественный вид с помощью фильтров постобработки, таких как «Размытие по Гауссу».
Применение тестирования трафаретов имеет гораздо больше назначений (помимо очерчивания объектов), например, рисование текстур внутри зеркала заднего вида, чтобы они аккуратно вписывались в форму зеркала, или визуализация теней в реальном времени с использованием метода буфера трафаретов, называемого теневыми объемами (англ. «shadow volumes»). Буферы трафарета дают нам еще один хороший инструмент в нашем и без того обширном OpenGL-инструментарии.