Что такое стенсил буфер

Стенсил буфер (Stencil Buffer)

Стенсил буфер (Stencil Buffer — буфер шаблона или буфер трафарета) — это дополнительный буфер, соответствующий размеру выводимого кадра, то есть каждому пикселю изображения на экране соответствует свое значение в стенсил буфере. Каждый раз когда точка рисуется на экран, то кроме тестов, вроде сравнения с глубиной в Z-буфере, она проходит еще и стенсил тест. То есть, например, можно сказать — точка рисуется, только если в стенсиле значение больше единицы. С другой стороны, можно сказать, как изменить значение стенсила после того как пиксель в этом месте отрисуется.

Описание

Стенсил буфер используется при создании таких спецэффектов, как тени, отражения, плавные переходы из одной картинки в другую, создания конструктивной стереометрии (CSG) и др.

Имейте в виду, что видеокарты ATI и NVidia поддерживают только 8-ми битный стенсил (D3DFMT_D24S8), поэтому стоит использовать его.

Теперь можем установить условие, значение и маску:
В OpenGL это делается одной командой
glStencilFunc(func, value, mask);
Но для смены маски есть и дополнительная функция
glStencilMask(mask);

В DirectX9 мы можем устанавливать эти параметры по отдельности вызовом функций
SetRenderState(D3DRS_STENCILFUNC, func);
SetRenderState(D3DRS_STENCILREF, value);
SetRenderState(D3DRS_STENCILMASK, mask);

Для установки трёх операций в OpenGL опять же используется одна функция
glStencilOp(fail, zfail, pass);

В DirectX9 устанавливаем по отдельности:
SetRenderState(D3DRS_STENCILFAIL, fail);
SetRenderState(D3DRS_STENCILZFAIL, zfail);
SetRenderState(D3DRS_STENCILPASS, pass);

Параметры fail, zfail, pass могут принимать следующие значения:

ПараметрНазвание в Direct3D10Название в Direct3D9Название в OpenGLОписание
Не менятьD3D10_STENCIL_OP_KEEPD3DSTENCILOP_KEEPGL_KEEPНе менять значение в буфере трафарета
0D3D10_STENCIL_OP_ZEROD3DSTENCILOP_ZEROGL_ZEROУстановить значение в 0
ЗаменитьD3D10_STENCIL_OP_REPLACED3DSTENCILOP_REPLACEGL_REPLACEУстановить значение в value (stencil ref)
Увеличить на 1D3D10_STENCIL_OP_INCR_SATD3DSTENCILOP_INCRSATGL_INCRУвеличить значение на 1. Значение ограничено максимальным (255)
*Увеличить на 1D3D10_STENCIL_OP_INCRD3DSTENCILOP_INCRGL_INCR_WRAPУвеличить значение на 1. При превышении максимума сбрасывается в 0
Уменьшить на 1D3D10_STENCIL_OP_DECR_SATD3DSTENCILOP_DECRSATGL_DECRУменьшить значение на 1. Значение ограничено минимумом (0)
*Уменьшить на 1D3D10_STENCIL_OP_DECRD3DSTENCILOP_DECRGL_DECR_WRAPУменьшить значение на 1. При уменьшении нуля, оно сбрасывается в 255
ИнвертироватьD3D10_STENCIL_OP_INVERTD3DSTENCILOP_INVERTGL_INVERTПобитовое инвертирование значения (bitwise NOT)

Одно из самых популярных и часто используемых применений буфера трафарета — создание теней. Этот метод теней называется Shadow Volume. Он стал так популярен, что производители видеокарт начали вводить дополнительные расширения для ускорения этого метода.

Одно из таких расширений — двусторонний тест трафарета. Так как все грани разделяются на лицевые и не-лицевые, для каждого типа из них можно установить своё условие прохождения теста трафарета, и свои операции fail, zfail и zpass.

GL_ATI_separate_stencil:
Включаем: glEnable(GL_STENCIL_TEST);
Выбираем одновременно условие для передних и задних граней: glStencilFuncSeparateATI(frontfunc, backfunc, value, mask);
Устанавливаем операции для каждой стороны: glStencilOpSeparateATI(face, fail, zfail, pass);

Как мы видим, АТИ просто не поддерживает различное значение для сравнения и маску у передних и задних сторон.

В DirectX9 мы делаем следующее:
Включаем: SetRenderState(D3DRS_TWOSIDEDSTENCILMODE, true);
Устанавливаем условие для граней с clock-wise порядком следования вершин: SetRenderState(D3DRS_STENCILFUNC, cwfunc);
И для граней с conter clock-wise порядком следования вершин: SetRenderState(D3DRS_CCW_STENCILFUNC, ccwfunc);
Далее устанавливаем операции функциями (Допустим что у нас передние грани с clock-wise порядком следования вершин):

Что такое Стенсил буфер (Stencil Buffer)?

Источник

О Stencil buffer-е

Книги directx9 о Stencil Buffer-е оставляют неясность и больше вопросов, чем ответов, для тех кто только начал постигать азы 3д графики.
Не давая более простого и доступного понимания этой темы.
Что мешает увидеть простоту и большие возможности использования Stencil Buffer-а.
Надеюсь, эта статья поможет начинающим.
Я постарался написать просто и относительно доступно.
Вот ссылки полезных статей, материал которых я использовал в своей статье:

Буфер трафарета или Stencil buffer служит для определения изображения а точнее пикселей которые будут отображены в Back buffer-е и тех пикселей что не отобразятся в Back buffer-е. В свою очередь, все что будет в Back buffer-е, будет выведено на экран. Если мы сделаем SwapChain, Present.

А значит с помощью Stencil buffer-а можно спрятать или произвести операции вычитания над 3д объектами. Или чего-либо еще по страшнее (зеркальное отражение или тени).

Можно сказать, что попавшие 3д объекты в Stencil buffer выглядят белыми. Если бы мы могли увидеть изображение Stencil buffer-а.
Изображение или информация хранящаяся в Stencil buffer-е будет 2д.

Размер Stencil buffer-а равен действующему разрешению экрана в данный момент. То есть тому разрешению, которое вы установили. К примеру: 1024х768 или 1680х1050 или иному.
Stencil buffer имеет 8 битный формат на точку (диапазон чисел от 0 до 255) и является частью Depth buffer-а.
Есть форматы, где нет выделения под Stencil buffer.
Распространенный формат Depth/ Stencil buffer-а это DXGI_FORMAT_D24_UNORM_S8_UINT.

Принцип работы Stencil buffer.

Если проверка Stencil buffer-а включена, Stencil buffer заполняется при каждом рендере или отрисовке 3д объекта. То есть мы делаем рендер 1-го объекта, а затем 2-го объекта и эта информация будет занесена в Stencil buffer. И при каждом последующем рендере 3д объектов. До тех пор пока мы не выключим проверку Stencil buffer-а. Так же каждому объекту можно задать маркер или число, под которым он будет храниться в Stencil buffer-е. Чтобы потом, используя эти маркеры или числа делать операции над силуэтами 3д объектов.

Но чтобы это произошло нужно:

1.Создать текстуру буфер под Depth/ Stencil buffer.
2.Очистить Stencil buffer значением от 0 до 255.
Как правило, очищают 0. 0 не выводится в Back buffer-е.

3.Если мы не включим проверку Stencil buffer-а мы не сможем работать с Stencil buffer-ом а значит, никакой информации кроме нулей или того значения, которое мы дали при очистке в Stencil buffer-е не будет. Мы также можем в нужный момент отключить проверку Stencil buffer.

4.Перед выводом 3д объекта задаем, как будет
заполнен Stencil buffer. Заполнение Stencil buffer происходит через проверки. Условия проверок мы можем задать.

И так посмотрим на примере программы(программа для directx 9 приведена для понимания и не является полноценным кодом).
Как это работает?

В качестве применения буфера трафарета рассмотрим пример «вырезания» одной плоской фигуры из другой. Пусть нам необходимо вырезать один треугольник (желтый) из другого (разноцветный), при этом должен быть виден квадрат, покрытый текстурой (как на рисунке с права).
Выполнение программного кода даст такую же картинку как на правом рисунке.

Что такое стенсил буфер. Смотреть фото Что такое стенсил буфер. Смотреть картинку Что такое стенсил буфер. Картинка про Что такое стенсил буфер. Фото Что такое стенсил буфер

device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_STENCIL,
D3DCOLOR_XRGB(255,255,255), 0.0f, 0);

2. С начало выводим квадрат, покрытый текстурой (тест трафарета при этом отключен). Мы не будем заносить информацию квадрата в Stencil buffer.

device->SetTexture(0, tex);
device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);

3. Включаем (разрешаем) тест трафарета.

5. В качестве значения запишем в буфер трафарета, например число 2. То есть наш желтый треугольник, а точнее его пиксели будут отмечены в Stencil buffer как число 2.

6. В качестве действия отрицательного прохождения теста трафарета установить D3DSTENCILOP_REPLACE (в буфере трафарета будет записана «двойка» на месте вырезки)

7. Теперь рисуем или рендерим желтый треугольник (сам треугольник не будет нарисован, но буфер трафарета обновит свое содержимое, то есть заполнится изображением двоек нашего желтого треугольника). Это нужно для дальнейших проверок Stencil buffer.

device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

8.Теперь нам нужно вывести разноцветный треугольник. Теперь в качестве параметра проверки трафарета установим D3DCMP_GREATER(Рисуем в буфере трафарета, если (STENCILREF & mask) > (значения в буфере трафарета & mask от предыдущих операций)).
Вот что делает команда D3DCMP_GREATER в нашем примере. Если место в буфере трафарета занято двойкой, то пиксель нашего разноцветного треугольника не будет занесен в буфер трафарета, а значит и в Back buffer. А если место в буфере трафарета занято 0 или 1, то пиксель нашего разноцветного треугольника будет занесен в буфер трафарета. А значит и в Back buffer.

device->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ GREATER);

9. В качестве значения STENCILREF, с которым будет сравниваться содержимое буфера трафарета, установим, например, число один.

10. В качестве действия отрицательного прохождения теста трафарета установим D3DSTENCILOP_REPLACE(заменим значением STENCILREF то есть 1).

11. Теперь рисуем или рендерим разноцветный треугольник.

device->DrawPrimitive(D3DPT_TRIANGLELIST, 3, 1);

Вот так выглядит содержимое stencil buffer-а, после выполнения программы.

Источник

Stencil Buffer на CUDA

Введение

Физически, стенсил хранится на GPU в том же буфере, где хранится глубина и бывает разного формата. Например, наиболее широко используемый формат D3DFMT_D24S8 означает, что 24 бита отводятся в бэк-буфере на глубину и 8 бит на стенсил. В данной статье, мы будет использовать упрощение и считать, что стенсил-буфер хранит на каждый пиксел (или на поток) всего один бит. Если бит равен 1, то пиксел (поток) активен. Если 0, то неактивен. Это позволит сэкономить немного памяти и упростит изложение.

Stencil Test часто используют для построения отражений таким методом:

Что такое стенсил буфер. Смотреть фото Что такое стенсил буфер. Смотреть картинку Что такое стенсил буфер. Картинка про Что такое стенсил буфер. Фото Что такое стенсил буфер

Рисунок 1. Stencil buffer нужен для маскирования отражений в тех местах где их на самом деле нет (как на рис. справа).

Собственно цвет зеркала и объекта удобно комбинировать при помощи пиксельного шейдера (ну можно при помощи альфа блендинга, если видеокарта совсем старая).

Программная реализация на CUDA

К сожалению, в куде, как и во всех остальных ‘compute’ технологиях (DX11 CS, OpenCL) механизм стенсил-теста просто отсутствует. В то же время, это вещь очень полезная, особенно если ваши вычисления реализованы в виде длинного конвейера из нескольких (часто довольно небольших) ядер (kernels). Допустим у вас имеется N потоков.

Что такое стенсил буфер. Смотреть фото Что такое стенсил буфер. Смотреть картинку Что такое стенсил буфер. Картинка про Что такое стенсил буфер. Фото Что такое стенсил буфер

Например, такая ситуация встречается при реализации на куде трассировки лучей. При глубине переотражений около 5, на некоторых сценах, меньше 10 % потоков будет активны на последнем уровне.

Для того, чтобы не выполнять работу для неактивных потоков вы, скорее всего, заведёте флаг в каком-нибудь буфере и будете проверять, если это флаг равен 0, то ничего не делать.

uint activeFlag = a_flags[tid];

Это решение в принципе работает, но оно имеет 2 недостатка. Первое – чрезмерная трата памяти (N потоков*sizeof(dataWithFlag)). Второе – кернел будет гонять данные ‘dataWithFlag’ по шине. Это плохо в том случае, если желательно быстро терминировать ничего не делающие потоки, потому что латентность памяти довольно значительная а каждый поток будет гарантированно читать свой флаг, так что кэш здесь не поможет.

В данной статье предлагается хранить в стенсил-буфере на 1 поток всего один бит и избежать массовых трансфертов данных по шине (или по крайней мере значительно их сократить, эффективно используя кэш).

Итак, заводим стенсил буфер размером ровно на (N/32)*sizeof(int) байт. И привязываем к нему текстуру.

cudaBindTexture (0, stencil_tex, m_stencilBuffer, N*sizeof(int)/32);

Сама текстура объявлена в каком-нибудь хедере (.h файл) следующим образом:

Далее, в том же файле объявим такой вспомогательный массив:

static __device__ int g_stencilMask[32] = <

0x00000001, 0x00000002, 0x00000004, 0x00000008, 0x00000010, 0x00000020, 0x00000040, 0x00000080,

0x00000100, 0x00000200, 0x00000400, 0x00000800, 0x00001000, 0x00002000, 0x00004000, 0x00008000,

0x00010000, 0x00020000, 0x00040000, 0x00080000, 0x00100000, 0x00200000, 0x00400000, 0x00800000,

0x01000000, 0x02000000, 0x04000000, 0x08000000, 0x10000000, 0x20000000, 0x40000000, 0x80000000

В этом массиве хранятся маски, с которыми мы будем делать логический & для того, чтобы быстро получить нужный потоку бит. То есть получить ровно тот бит, номер которого равен номеру потока внутри warp-а. Вот как будет выглядеть stencil test:

#define STENCIL_TEST (tid) \

if(!(tex1Dfetch(stencil_tex, (tid) >> 5) & g_stencilMask[(tid)&0x1f])) \

Для тех кернелов, которые только читают стенсил буфер, применять макрос следует в начале кернела следующим образом:

__global__ void my_kernel(…)

uint tid = blockDim.x * blockIdx.x + threadIdx.x;

На практике (GTX560) такой стенсил тест примерно на 20-25% быстрее, чем простая проверка проверка вида:

uint activeFlag = a_flags[tid];

Однако, с учетом экономии памяти, профит определенно есть. Следует так же отметить, что на видеокартах с менее широкой шиной (например GTS450) ускорение может быть более существенным.

Итак, осталось реализовать лишь запись в стенсил-буфер. Сначала читаем значение для всего в warp-а из стелсил-буфера в переменную activeWarp; Затем каждый поток получает из этой переменной свой бит при помощи логического & и хранит его в переменной active. В конце кернела мы соберем из всех переменных active для данного warp-а значения обратно в один 32 разрядный uint, и нулевой поток warp-а запишет результат назад в память.

// ( tid >> 5) same as (tid/32)

// ( tid & 0x1f) same as (tid%32)

__global__ void my_kernel2(…,uint* a_stencilBuffer)

uint tid = blockDim.x * blockIdx.x + threadIdx.x;

uint activeWarp = a_stencilBuffer[tid >> 5];

if(activeWarp==0) // all threads in warp inactive

// each threads will store it’s particular bit from group of 32 threads

WriteStencilBit(tid, a_stencilBuffer, active);

Если поток неактивен, он сразу перейдет в конек кернела. Если по какой-либо причине вы внутри вашего кода решили, что этот поток должен быть неактивен, сделайте так:

if (want to kill thread)

В примере намеренно использована метка и оператор goto. Хоть это и является плохим стилем программирования, в данном случае это добавляет безопасности вашему коду. Дело в том, что вы обязаны гарантированно достичь кода функции WriteStencilBit. Если по какой-то причине внутри вашего кода вы решите сделать return, всё поломается (чуть позже обсудим почему). Вместо return надо ставить goto WRITE_BACK_STENCIL_DATA, чтобы перед выходом, все потоки из warp-a могли собрать данные, а нулевой поток (нулевой внутри warp-a) запишет их в стенсил-буфер. Собственно, функция WriteStencilBit выглядит следующим образом:

uint stencilMask = __ballot(value);

if((tid & 0x1f) == 0) // same as tid%32 == 0

a_stencilBuffer [tid >> 5] = stencilMask;

Функция __ballot() возвращает uint, где каждый i-ый бит равен 1 тогда и только тогда, когда то, что находится в ее аргументе не равно нулю. То есть она делает в точности то, что там нужно, сшивая обратно в uint флаги от разных потоков внутри warp-а.

Функция __ballot() принадлежит к так называемым “warp vote functions” и работает очень быстро. К сожалению, она доступна только для compute capability 2.0, то есть видеокарт с архитектурой Fermi. Важное замечание по её работе, следующий код будет неправильным :

if((tid & 0x1f) == 0) // same as tid%32 == 0

a_stencilBuffer[tid >> 5] = __ballot(value);

Дело в том, что __ballot() будет всегда помещать 0 в те биты, чьи потоки замаскированы в данный момент. А все потоки с номером внутри варпа не равным нулю (1..31) будут замаскированны и не попадут внутрь оператора if, поэтому 1..31 биты результата функции __ballot() для такого кода всегда будут равны нулю. Отсюда правда следует интересный вывод. Если вы гарантированно пишете для видеокарт с архитектурой Fermi, то даже для кернелов которые пишут в стенсил буфе, вы можете убивать поток следующим образом:

if (want to kill thread)

Таким образом, потоки, для которых вы сделали return будут замаскированы и __ballot() вернет в своем результате нули для соответствующих бит. Есть правда одна тонкость. По крайней мере для нулевого потока внутри warp-а вы так сделать не можете, иначе результат просто не запишется назад. Поэтому на самом деле можно делать только так

Или пользуйтесь формой предложенной выше:

if (want to kill thread)

Особенности реализации для старой аппаратуры (G80-GT200)

Рассмотрим теперь, какие расширения должны быть сделаны, чтобы стенсил эффективно работал и на более старых GPU. На этих видеокартах не поддерживается функция __ballot(). Перепишем функцию WriteStencilBit в соответствие с теми возможностями, которые у нас имеются:

__device__ inline void WriteStencilBit(int tid, uint* a_stencilBuffer, uint value)

#if COMPUTE_CAPABILITY >= COMPUTE_CAPABILITY_GF100

uint stencilMask = __ballot(value);

a_stencilBuffer[tid >> 5] = stencilMask;

#elif COMPUTE_CAPABILITY >= COMPUTE_CAPABILITY_GT200

a_stencilBuffer[tid >> 5] = 0;

a_stencilBuffer[tid >> 5] = 0xffffffff;

__shared__ uint active_threads[CURR_BLOCK_SIZE/32];

uint* pAddr = active_threads + (threadIdx.x >> 5);

a_stencilBuffer[tid >> 5] = *pAddr;

__shared__ uint active_threads[CURR_BLOCK_SIZE];

if((threadIdx.x & 0x1) == 0)

active_threads[threadIdx.x] = value | active_threads[threadIdx.x+1];

if((threadIdx.x & 0x3) == 0)

active_threads[threadIdx.x] = active_threads[threadIdx.x] | active_threads[threadIdx.x+2];

if((threadIdx.x & 0x7) == 0)

active_threads[threadIdx.x] = active_threads[threadIdx.x] | active_threads[threadIdx.x+4];

if((threadIdx.x & 0xf) == 0)

active_threads[threadIdx.x] = active_threads[threadIdx.x] | active_threads[threadIdx.x+8];

if((threadIdx.x & 0x1f) == 0)

active_threads[threadIdx.x] = active_threads[threadIdx.x] | active_threads[threadIdx.x+16];

uint* perWarpArray = active_threads + ((threadIdx.x >> 5)

a_stencilBuffer[tid >> 5] = perWarpArray[0];

Таким образом, если у нас GT200 мы можем делать атомики в шаред-память + доступны 2 функции голосования, __any и __all, так что мы их можем использовать. Ну а если это G80 то остается только классическая редукция.

Так как на G80-GT200 обращения в глобальную память не кэшируются, то STENCIL_TEST лучше переписать без использования таблицы масок:

#define STENCIL_TEST (tid) \

if (!( tex1Dfetch (stencil_tex, (tid) >> 5) & (1

Persistent threads (task stealing на CUDA)

Раз уж мы затронули тему распределения работы, нельзя не упомянуть о таком важном трюке как persistent threads. Итак, вернемся к рисункам с замаскированными потоками.

Что такое стенсил буфер. Смотреть фото Что такое стенсил буфер. Смотреть картинку Что такое стенсил буфер. Картинка про Что такое стенсил буфер. Фото Что такое стенсил буфер

Что происходит в железе, когда вы убиваете потоки некоторым случайным образом, и среди массива активных потоков (зеленых) образуются рваные дырки неактивных (красных)? Что касается последнего железа (Fermi), распределение работы производится на уровне warp-ов. Например, если у вас идут 32 потока активны, потом 32 неактивны, а потом снова 32 потока активны, а потом снова 32 неактивны, то всё будет хорошо (ну при условии что вы не используете явный __syncthreads() где-нибудь в коде). Активные потоки будут работать, а неактивные не будут занимать место и тратить ресурсы. Однако, на старых архитектурах (G80-GT200) менеджмент потоков всегда осуществлялся на уровне блоков. Например, если размер блока 128 потоков, и мы берем рассмотренный выше пример, то неактивные потоки будут висеть на мультипроцессоре до тех пор, пока весь блок не завершит свою работу. В случае неравномерного распределения работы для разных потоков эта проблема может стать довольно существенной. Решение этой проблемы – собственный, софтверный менеджер задач. Идею persistent threads можно в кратце выразить следующим образом:

Первое реализовать не трудно, зная количество работы (то, сколько всего вы бы хотели запустить потоков, без использования persistent threads – аргумент a_size), количество используемых кернелом регистров (полагаем что разделяемая память не является ограничивающим occupancy фактором) и размер блока:

__constant__ int cm_maxThread;

static __device__ int g_warpCounter;

int CalcPersistentThreadsAndResetWarpCounter(int a_size, int regCount, int myThreadsPerBlock)

int threadBlocksPerSM = devProp.regsPerBlock/(regCount*myThreadsPerBlock);

int waprsPerSM = (threadBlocksPerSM*myThreadsPerBlock)/devProp.warpSize;

int launchedWarps = devProp.multiProcessorCount*waprsPerSM;

int launchedThreads = launchedWarps*devProp.warpSize;

void* g_warpCountAddress = NULL;

cudaMemcpy(g_warpCountAddress, value, sizeof(int), cudaMemcpyHostToDevice);

Использовать эту функцию следует примерно следующим образом:

int threads = CalcPersistentThreadsAndResetWarpCounter (size, 24, 192);

Теперь, каждый kernel, который использует persistent threads, должен выглядеть следующим образом:

__global__ void my_kernel(…)

__shared__ int s_nextWarpId[(BLOCK_SIZE)/32];

if ( (threadIdx.x & 0x1f) == 0)

tid = nextWarpId + (threadIdx.x & 0x1f);

if (tid >= cm_maxThread)

if (!( tex1Dfetch (stencil_tex, (tid) >> 5) & (1

Работает это следующим образом. Когда warp завершил свою работу (например он находится в конце цикла while или был откинут стенсил-тестом), он крадет следующую порцию работы для себя, увеличивая глобальный счетчик сразу на 32. Счетчик указывает на то, сколько еще свободной работы осталось.

На G80 именно так persistent threads реализовать не получится, вследствии отсутствия атомарных операций. Но можно просто сделать цикл вида “for(int i=0;i до 2 раз.

Тестируем Stencil

Собственно для нужд рейтрейсинга, такой стенсил буфер подошел довольно удачно. Если уткнуться в пустоту, на GTX560 возможно получается около 4 миллиардов вызов кернелов в секунду (то есть 4 миллиарда пустых вызовов в секунду). При увеличении глубины трассировки производительность практически не падала (вернее падала в соответствии с тем, насколько реально много отраженных объектов мы видим). Тесты специально производились на как можно более простой отражающей сцене и на полностью диффузной, где отражений нет вообще. На глубине трассировке >=2 все потоки неактивны. К сожалению не все кернелы в моем рейтрейсере можно было откинуть стенсилом, поэтому с ростом глубины отражений даже для диффузной сцены FPS падает. Динамика FPS следующая:

Для зеркальной сцены : 30, 25, 23.7, 20, 19.4, 18.8 (рис. 2)

Для диффузной сцены : 40, 37, 34, 32, 30, 29.5

Для сравнения, на более сложной зеркальной сцене:

Для зеркальной сцены 2: 32, 23, 18.6, 16.1, 14.4 (рис.3)

Что такое стенсил буфер. Смотреть фото Что такое стенсил буфер. Смотреть картинку Что такое стенсил буфер. Картинка про Что такое стенсил буфер. Фото Что такое стенсил буфер

Рисунок 2. простая сцена, менее 100 треугольников.

Источник

Learn OpenGL. Часть 4.2. — Тест трафарета

Что такое стенсил буфер. Смотреть фото Что такое стенсил буфер. Смотреть картинку Что такое стенсил буфер. Картинка про Что такое стенсил буфер. Фото Что такое стенсил буфер

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

Тест трафарета

Трафаретный буфер, обычно, содержит 8 бит на каждое трафаретное значение, что в сумме дает 256 различных значений трафарета на фрагмент/пиксель. Мы можем установить эти значения на наш вкус, а затем отбрасывать или сохранять фрагменты, всякий раз, когда определенный фрагмент имеет определенное трафаретное значение.

Каждая оконная библиотека должна настроить буфер трафарета для вас. GLFW делает это автоматически, так что вам не нужно беспокоиться об этом, но другие оконные библиотеки могу не создавать трафаретный буфер по умолчанию, так что убедитесь, что просмотрели документацию вашей библиотеки.

Простой пример трафаретного буфера приведен ниже:

Что такое стенсил буфер. Смотреть фото Что такое стенсил буфер. Смотреть картинку Что такое стенсил буфер. Картинка про Что такое стенсил буфер. Фото Что такое стенсил буферСначала трафаретный буфер заполняется нулями, и затем область буфера, выглядящая как прямоугольная рамка, заполняется единицами. Отображаются только те фрагменты сцены, трафаретное значение которых равно единице, остальные — отбрасываются.

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

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

Обратите внимание, что вам нужно очищать трафаретный буфер на каждой итерации, так же как буфер цвета и глубины:

В большинстве случаев просто записывайте 0x00 или 0xFF в трафаретную маску, но неплохо было бы знать, что существует возможность устанавливать пользовательские битовые маски.

Трафаретные функции

Функция glStencilFunc(GLenum func, GLint ref, GLuint mask) имеет три параметра:

Итак, в случае нашего простого примера трафарета, который мы показали в начале, функция будет такой:

Это говорит OpenGL, что, всякий раз, когда трафаретное значение фрагмента равно ссылочному значению 1, фрагмент проходит тест и рисуется, в противном случае — отбрасывается.

Функция glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass) содержит три параметра, с помощью которых мы можем определить действия для каждого варианта:

Затем, для каждого случая можно выполнить одно из следующих действий:

Итак, используя glStencilFunc и glStencilOp мы можем точно установить когда и как мы хотим обновлять трафаретный буфер, и мы также определяем, когда трафаретный тест будет пройден, а когда — нет, то есть когда фрагменты должны быть отброшены.

Обводка объектов

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

Что такое стенсил буфер. Смотреть фото Что такое стенсил буфер. Смотреть картинку Что такое стенсил буфер. Картинка про Что такое стенсил буфер. Фото Что такое стенсил буфер

Нет нужды объяснять, что подразумевается под обводкой объекта. Для каждого объекта (или только для одного) мы создаем маленькую цветную рамку. Этот эффект особенно полезен, когда нам, к примеру, нужно выделить юнитов в стратегической игре, а затем показать пользователю, какие были выбраны. Алгоритм формирования обводки для объекта такой:

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

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

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

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

Буфер очищается заполнением нулями, а для контейнеров мы обновляем трафаретный буфер до 1 для каждого нарисованного фрагмента:

Используя GL_ALWAYS в функции glStencilFunc мы гарантируем, что каждый фрагмент контейнеров обновит трафаретный буфер с трафаретным значением 1. Так как фрагменты всегда проходят трафаретный тест, то трафаретный буфер обновляется ссылочным значением, везде, где мы их нарисовали.

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

Также убедитесь, что снова включили тест глубины.

Общий шаблон обводки объекта для нашей сцены выглядит как-то так:

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

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

Что такое стенсил буфер. Смотреть фото Что такое стенсил буфер. Смотреть картинку Что такое стенсил буфер. Картинка про Что такое стенсил буфер. Фото Что такое стенсил буфер

Проверьте исходный код здесь, чтобы увидеть полный код алгоритма обводки объекта

Можно заметить, что границы между двумя контейнерами перекрываются. Обычно, это именно то, что нужно (вспомните стратегические игры, когда мы выделяем несколько юнитов). Если вам нужна полная граница вокруг каждого объекта, нужно очищать трафаретный буфер для каждого объекта и немного пошаманить с настройкой теста глубины.

Алгоритм обводки объектов, который вы видели, довольно часто используется в некоторых играх для визуализации выбранных объектов (вспомните стратегические игры), и такой алгоритм может быть легко реализован в классе модели. Затем можно просто установить логический флаг в классе модели для рисования с границами или без них. Если проявить немного творчества, то можно сделать границы более органичными с помощью фильтров пост-обработки, например размытия по Гауссу.

С помощью трафаретного тестирования можно делать больше вещей, чем просто обводить объекты, например рисовать текстуры внутри зеркала заднего вида так, что они вписываются в рамку зеркала. Или рендерить тени в реальном времени с помощью техники shadow volumes. Трафаретный буфер обеспечивает нас еще одним прекрасным инструментом в нашем и без того обширном инструментарии OpenGL.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *