Бесплатная реклама

Автор: Роман Кузнецов «Romanek»

Опубликовано: 04.07.2010

Изменено: 04.07.2010

Постоянная ссылка

Комментарии [6]

Реализация прозрачности алгоритмом Stencil Routed A-Buffer на DX 10


Рассматривается интересный алгоритм Stencil Routed A-Buffer метода реализации прозрачности Order Independent Transparency с помощью Direct3D 10.


Содержание

Введение
Алгоритм Stencil Routed A-Buffer
Реализация на Direct3D 10
Демо программа и исходники
Заключение
Полезные ссылки

Введение

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

Для начала следует пояснить, как правильно рисовать полупрозрачные объекты (далее ПП-объекты) в классическом варианте, т.е. без использования специальных алгоритмов. Главный секрет здесь – рисование ПП-объектов в порядке от дальнего к ближнему. Возможно, у кого-то возникнет вопрос, почему так?

Предположим у нас есть 3 объекта: O1 (непрозрачный), O2 и O3 (полупрозрачные), расположенные согласно рисунку 1.

Реализация прозрачности алгоритмом Stencil Routed A-Buffer на DX 10
Рисунок 1. Три объекта: O1 – непрозрачный, O2 и O3 – полупрозрачные, ось Z обозначает глубину сцены.

Т.е. объект O1 мы видим через остальные два объекта. Формула альфа-блендинга (alpha blending - смешивание цветов c альфа-каналом) при наложении ПП-объекта на другой объект (как полупрозрачный, так и непрозрачный) следующая:

Реализация прозрачности алгоритмом Stencil Routed A-Buffer на DX 10

То есть, линейная интерполяция между цветом первого объекта (dest) и цветом ПП-объекта (src) по значению уровня прозрачности этого объекта (альфа).

Допустим, мы рисуем объекты в порядке O1 – O2 – O3, т.е. от дальнего к ближнему. Для этого случая получатся следующие формулы:

Реализация прозрачности алгоритмом Stencil Routed A-Buffer на DX 10

Даже не раскрывая конечную формулу, видно, что на вклад в результирующий цвет первого объекта O1 (с цветом C1) влияют значения альфы объектов O2 и O3, на O2 – альфа O3, на O3 – никакая чужая альфа не влияет. С логической точки зрения – это тот самый случай, когда мы видим объект O1, через два других.

А теперь предположим, что мы рисуем объекты в порядке O1 – O3 – O2. Нетрудно представить, как изменится формула результирующего цвета – C2 и C3 поменяются местами. Результирующий цвет будет такой, как будто объект O3 дальше объекта O2, хотя по расстоянию до камеры он ближе. Появятся артефакты, и картинка будет испорчена.

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

Поэтому уже достаточно давно была придумана техника, которая по-русски может звучать как порядко-независимая прозрачность, или Order Independent Transparency (OIT) в оригинале. Однако достаточно эффективная реализация этой техники стала возможна только на современных вычислительных мощностях.

Суть большинства алгоритмов можно свести к одному – необходимо наиболее эффективным способом получить изображение сцены по слоям. Слой – это отображение совокупности точек ближайшей к наблюдателю, второй после ближайшей, третьей и т.д. Все кроме первой ближайшей точки обычно отсекаются по тесту глубины. Для чего же нам понадобились остальные точки? Предположим, у нас есть модель комнаты с застекленным окном, через которое мы видим улицу. В первом слое нас интересует комната и полупрозрачное окно. Во втором – улица, которая видна через окно (а если бы мы воспользовались услугами отсечения, то этой улицы никогда бы и не увидели). Если на улице стоит другой дом с окном, и мы его видим, то в третьем слое нас интересует то, что видно через это окно. И так далее, думаю, логика понятна. Имея такой набор слоев, мы можем спокойно их смешивать по значениям альфы в нужном порядке, и не получим артефакты, пока будет хватать слоев.

А вот эффективное получение нужного количества слоев – целое искусство. Существует несколько методов. Самый примитивный – рисование сцены N раз, каждый с использованием последнего значения буфера глубины. То есть, рисуем объекты с применением Z-теста, который возвращает ближайшую видимую точку. При рисовании следующего слоя его Z-тест начнется со значений предыдущего Z-теста, т.о. мы получим следующую видимую точку. Процесс можно повторять сколь угодно долго. Минус очевиден – многократное рисование одной и той же сцены. Этот алгоритм называется Depth Peeling.

Существует несколько модификаций алгоритма, например, обратный Depth Peeling (Reverse Depth Peeling) позволяет не хранить слои в текстурах, а смешивать их "на лету", потому как слои получаются в обратном порядке, от самого дальнего к самому ближнему. Двойной Depth Peeling (Dual Depth Peeling) позволяет получать по 2 слоя (дальний и ближний) за один рендер, что тоже несколько повышает производительность.

Я же хочу рассказать об алгоритме Stencil Routed A-Buffer, который, на мой взгляд, даёт наилучшую производительность.

Алгоритм Stencil Routed A-Buffer

В основе данного алгоритма лежит нестандартное использование MSAA (Multisampling Anti-Aliasing). Сразу следует подчеркнуть, что данный алгоритм невозможно воспроизвести на Direct3D 9, поскольку этот API не поддерживает необходимых возможностей.

В рамках данной статьи мы не будем рассматривать принципы работы MSAA. Главное, что нам необходимо знать – при рендеринге с MSAA сцена отображается N раз по числу уровней MSAA. Т.е. при 8-кратном MSAA сцена будет нарисована 8 раз (будет 8 сэмплов). Благодаря Direct3D 10 мы можем в некоторой степени управлять сэмплами антиалиасинга, а значит использовать MSAA не только для его первичной цели. Наверно, вы уже догадались, что каждый сэмпл MSAA будет слоем в нашем алгоритме.

Возникает вопрос, каким образом мы заставим Direct3D 10 отклониться от привычного поведения, т.е. выполнения алгоритма MSAA? Начнем с того, что отключим антиалиасинг на этапе растеризации (RasterizerState). Таким образом, будем рендерить в текстуру с MSAA, но сам антиалиасинг будет выключен. В этом случае в каждом сэмпле будет одна и та же картинка. Для того чтобы в сэмплах рисовалось то, что нам нужно, воспользуется буфером трафарета (Stencil buffer). Буфер трафарета, понятно, тоже должен быть с поддержкой MSAA. Каждый сэмпл буфера трафарета мы заполним числами от 1 до N, где N – уровень MSAA. Теперь при рендеринге в текстуру необходимо выставить условия теста трафарета. Тест трафарета считается пройденным, если значение в буфере равно 1, а после теста в любом случае (пройден он или нет) выполняется операция декремента (уменьшение на 1) с усечением по нулю над значением в буфере трафарета. Тест глубины должен быть выключен. После этой операции в сэмплах текстуры с MSAA будет находиться изображение сцены по слоям. Такая текстура называется A-буфер (Accumulation buffer). Рассмотрим подробнее, почему так происходит.

Предположим, мы имеем текстуру для рендеринга 2 на 2 пикселя с 4xMSAA (рисунок 2).

Реализация прозрачности алгоритмом Stencil Routed A-Buffer на DX 10
Рисунок 2. Модель текстуры для рендеринга с MSAA.

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

Реализация прозрачности алгоритмом Stencil Routed A-Buffer на DX 10
Рисунок 3. Модель рендеринга в текстуру с MSAA.

То же самое может произойти с каждым пикселем в текстуре. В результате, мы получаем A-буфер, содержащий данные о точке по слоям. Однако эти данные не сортированы, объекты могли в каком угодно порядке рисоваться в буфер.

Проблема сортировки на GPU многократно решалась. Авторы алгоритма предлагают использовать битоническую сортировку (bitonic sort), однако, Эмиль Перссон (Emil Persson) больше известный как Humus предлагает использовать восходящую сортировку слиянием (Odd-Even Merge Sort). Действительно, для сортировки 8 чисел восходящая сортировка слиянием дает 19 операций сравнения-обмена против 24 в битонической. Напомню, как организована восходящая сортировка слиянием (рисунок 4).

Реализация прозрачности алгоритмом Stencil Routed A-Buffer на DX 10
Рисунок 4. Восходящая сортировка слиянием
(стрелками показаны операции сравнения-перестановки).

Сортировать точки в слоях необходимо по значению глубины, а, значит, это самое значение необходимо сохранить. Если бы мы использовали Direct3D 10.1, то могли бы брать значения глубины непосредственно из буфера глубины-трафарета. В нашем случае, мы используем рендеринг в несколько текстур сразу (Multiple Render Target, MRT). В одну текстуру (в формате ARGB) будем записывать цвет, в другую (в формате 32-битного float) – глубину.

Отсортировав точки, мы получаем 8 полноценных слоев и можем реализовывать альфа-блендинг по вышеприведенным формулам.

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

У данного алгоритма, разумеется, есть преимущества и недостатки. К преимуществам здесь можно отнести производительность. К недостаткам – ограниченность числа слоев уровнем MSAA, а также отсутствие антиалиасинга (мы использовали его иначе).

Теперь перейдем к реализации алгоритма на Direct3D 10.

Реализация на Direct3D 10

Для реализации алгоритма нам потребуется 2 текстуры для рендеринга с MSAA и 1 буфер глубины-трафарета также с MSAA. Ниже приведенная структура содержит необходимую нам информацию о текстуре для рендеринга.

// Рендер-таргет
struct RenderTarget
{
  UINT mWidth;          // Ширина
  UINT mHeight;         // Высота
  UINT mSampleCount;    // Число сэмплов
  UINT mSampleQuality;  // Качество сэмплинга (стандартно 0)
  DXGI_FORMAT mFormat;  // Формат

  ID3D10Texture2D* mRenderTargetTexture;             // Ресурс текстуры
  ID3D10RenderTargetView* mRenderTargetView;         // Представление рендер-таргета
  ID3D10ShaderResourceView* mRenderTargetShaderView; // Представление для шейдера
};

Аналогично организована структура и для буфера глубины трафарета. Таким образом, создаем следующие объекты:

  // A-буферы цвета и глубины
  RenderTarget gABufferColorMSAA;
  RenderTarget gABufferDepthMSAA;
  // DS-буфер с MSAA
  DepthStencil gDepthStencilMSAA;

Процесс создания подобных объектов достаточно тривиален, следует лишь отметить, что размеры всех 3 текстур равны размеру заднего буфера. Формат gABufferColorMSAA соответствует формату заднего буфера, формат gABufferDepthMSAA равен значению DXGI_FORMAT_R32_FLOAT, формат gDepthStencilMSAA равен DXGI_FORMAT_D24_UNORM_S8_UINT.

Также необходимы меш (или несколько) и экранный квад, работу с которым можно повсеместно встретить в DirectX SDK.

Рассмотрим процесс рендеринга. Сначала произведем заполнение A-буферов.

// Рендер A-буферов
{
  // Устанавливаем MRT
  const int rtCount = 2;
  ID3D10RenderTargetView* views[rtCount] = 
    {
     gABufferColorMSAA.mRenderTargetView,
     gABufferDepthMSAA.mRenderTargetView
    };
  pd3dDevice->OMSetRenderTargets(rtCount, views, gDepthStencilMSAA.mDepthStencilView );
        
  // Очищаем
  pd3dDevice->ClearRenderTargetView( gABufferColorMSAA.mRenderTargetView, ClearColor );
  const float ClearColor2[4] = {0,0,0,0};
  pd3dDevice->ClearRenderTargetView( gABufferDepthMSAA.mRenderTargetView, ClearColor2 );
  pd3dDevice->ClearDepthStencilView( gDepthStencilMSAA.mDepthStencilView, 
                                     D3D10_CLEAR_DEPTH | D3D10_CLEAR_STENCIL, 1.0f, 2 );

  // Заполняем stencil буфер от 2 до 2 + MSSA Samples
  pd3dDevice->IASetInputLayout( gQuadVertexLayout );
  const UINT uOffset = 0;
  const UINT uStride = sizeof( FSVertex );
  pd3dDevice->IASetVertexBuffers(0, 1, &gFullScreenVertexBuffer, &uStride, &uOffset);
  pd3dDevice->IASetPrimitiveTopology( D3D10_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP );
  gTechSetStencil->GetPassByIndex(0)->Apply(0);
  for (unsigned int i = 0; i < gMSAASampleCount; i++)
    {
     pd3dDevice->OMSetDepthStencilState(gSetStencilState, i + 2);
     pd3dDevice->OMSetBlendState(NULL, ClearColor, 1 << i);
     pd3dDevice->Draw(4,0);
    }

  // Рендерим объекты
  RenderSceneMesh(pd3dDevice, &gSceneMesh[gCurrentMesh]);
}

Наиболее интересным местом здесь является рендеринг квада. Рендеринг осуществляется при помощи специальной техники gTechSetStencil, которая в шейдере представлена следующим образом.

RasterizerState NoMSAARasterizer
{
    CullMode = NONE;
    MultiSampleEnable = false;
};

// Заполнение буфера трафарета
technique10 tSetStencil
{
    pass p0
    {
    	SetVertexShader( CompileShader( vs_4_0, vsQuad() ) );
		SetGeometryShader( NULL );
     	SetPixelShader( NULL );
		SetRasterizerState( NoMSAARasterizer );
    }
}

Как и было описано в предыдущем разделе, рендеринг осуществляется с выключенным мультисэмплингом (MultiSampleEnable = false). Нет ни пиксельного, ни геометрического шейдера, только вершинный. К тому же выставляется специальное состояние глубины-трафарета (gSetStencilState), которое определяет поведение буфера глубины-трафарета.

  D3D10_DEPTH_STENCIL_DESC dsDesc;
  dsDesc.DepthEnable = FALSE;
  dsDesc.DepthWriteMask = D3D10_DEPTH_WRITE_MASK_ALL;
  dsDesc.DepthFunc = D3D10_COMPARISON_LESS;
  dsDesc.StencilEnable = TRUE;
  dsDesc.StencilReadMask = 0xFF;
  dsDesc.StencilWriteMask = 0xFF;
  dsDesc.FrontFace.StencilFailOp = D3D10_STENCIL_OP_REPLACE;
  dsDesc.FrontFace.StencilDepthFailOp = D3D10_STENCIL_OP_REPLACE;
  dsDesc.FrontFace.StencilPassOp = D3D10_STENCIL_OP_REPLACE;
  dsDesc.FrontFace.StencilFunc = D3D10_COMPARISON_ALWAYS;
  dsDesc.BackFace.StencilFailOp = D3D10_STENCIL_OP_REPLACE;
  dsDesc.BackFace.StencilDepthFailOp = D3D10_STENCIL_OP_REPLACE;
  dsDesc.BackFace.StencilPassOp = D3D10_STENCIL_OP_REPLACE;
  dsDesc.BackFace.StencilFunc = D3D10_COMPARISON_ALWAYS;

То есть использовать только тест трафарета, и всегда (D3D10_COMPARISON_ALWAYS) заменять (D3D10_STENCIL_OP_REPLACE) значение трафарета в буфере на переданное в графический конвейер. Передаваемое значение – второй параметр в методе OMSetDepthStencilState. Метод OMSetBlendState позволяет выставить сэмпл, в который будет производиться вывод.

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

DepthStencilState SceneDS
{
  DepthEnable = false;
  StencilEnable = true;
  StencilReadMask = 0xFF;
  StencilWriteMask = 0xFF;
  FrontFaceStencilFail = Decr_Sat;
  FrontFaceStencilPass = Decr_Sat;
  FrontFaceStencilFunc = Equal;
  BackFaceStencilFail = Decr_Sat;
  BackFaceStencilPass = Decr_Sat;
  BackFaceStencilFunc = Equal;
};

// Сцена
technique10 tScene
{
  pass P0
  {
    SetVertexShader( CompileShader( vs_4_0, vsScene() ) );
    SetGeometryShader( NULL );
    SetPixelShader( CompileShader( ps_4_0, psScene() ) );
    SetRasterizerState( NoMSAARasterizer );
    SetDepthStencilState( SceneDS, 2 );
    SetBlendState( NULL, float4( 0.0f, 0.0f, 0.0f, 0.0f ), 0xFFFFFFFF );
  }
} 

Ключевым элементом здесь является состояние глубины-трафарета, согласно которому тест трафарета считается пройденным при равенстве (Equal) значения 2, после чего над значением в буфере производится операция декремента с усечением (Decr_Sat).

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

  // Рисуем на экран
  ID3D10RenderTargetView* views[1] = { pRTV };
  pd3dDevice->OMSetRenderTargets(1, views, pDSV);

  gVarABufferColor8->SetResource(gABufferColorMSAA.mRenderTargetShaderView);
  gVarABufferDepth8->SetResource(gABufferDepthMSAA.mRenderTargetShaderView);
  gTechRenderQuad8->GetPassByIndex(0)->Apply(0);
  RenderScreenQuad(pd3dDevice);

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

Реализация прозрачности алгоритмом Stencil Routed A-Buffer на DX 10
Рисунок 5. Полупрозрачная беседка, полученная алгоритмом Stencil Routed A-Buffer.

 

Реализация прозрачности алгоритмом Stencil Routed A-Buffer на DX 10
Рисунок 6. Ну, и чайник, куда без него :-)

Демо программа и исходники

Скачать демонстрационную программу с исходниками: Stencil_Routed_A_Buffer.zip

Для запуска требуется видеокарта с поддержкой DirectX 10.0; ОС Windows Vista/7; установленные драйвера на видеокарту; установленный DirectX 10; возможно, Visual C++ 2008 SP1 Redistributable Package (x86/x64).

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

Управление:
WASD - перемещение камеры
левая кнопка мыши + движение – вращение камеры

Заключение

В этой статье мы рассмотрели один из алгоритмов Order Independent Transparency и его реализацию при помощи Direct3D 10. Алгоритм оказался не столько сложным, сколько хитрым, по крайней для меня. В перспективе я рассчитываю объединить данный алгоритм с отложенным освещением (Deferred Shading), для которого остро стоит проблема отображения полупрозрачных объектов. По результатам надеюсь написать новую статью, если на то будет Ваше желание и данная тематика интересна :)

Также хочется выразить благодарность товарищу Humus’у за неплохой (и, пожалуй, до сего дня единственный) рабочий пример алгоритма, и сайту www.turbosquid.com за модель беседки. На этом прощаюсь.

Полезные ссылки

1. Презентация алгоритма от его авторов
http://developer.download.nvidia.com/presentations/2007/siggraph/stencil_routed_a-Buffer_sigg07.ppt

2. Описание алгоритма от его авторов
http://www.sci.utah.edu/~bavoil/research/kbuffer/StencilRoutedABuffer_Sigg07.pdf

3. Демо Humus’а "Order Independent Translucency"
http://www.humus.name/index.php?page=3D

 


 

Статья участвовала в конкурсе статей по программированию #4 (2009).