Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Работа с полупрозрачной геометрией обладает существенным недостатком - полученное изображение зависит от того, в каком порядке выводились полупрозрачные грани. Для получения корректного изображения все полупрозрачные грани должны выводиться в порядке back-to-front, т.е. начиная с самых дальних и заканчивая самыми ближними.
Обычно полупрозрачные грани просто сортируются перед выводом. Однако такой подход не всегда удобен и для динамичных сцен требует постоянной передачи нового упорядочивания на GPU (или сортировки прямо на GPU).
С появлением в OpenGL поддержки записи память GPU (расширения ARB_image_load_store и ARB_shader_storage_buffer_object) стало возможным реализовать корректный рендеринг полупрозрачной геометрии не зависимо от того, в каком порядке производится вывод граней. Этот подход получил название Order-Independent Transparency (OIT). И мы сейчас рассмотрим его реализацию в OpenGL.
Рассмотрим, каким образом мы можем это реализовать. Поскольку нет правильного упорядочивания граней, но нам необходимо обеспечить это упорядочивание на уровне отдельных фрагментов прямо в фрагментном шейдере.
Фактически в процессе рендеринга для каждого пиксела строится список всех фрагментов, попадающих в данный пиксел. Для каждого фрагмента мы должны запомнить его цвет и его глубины (для дальнейшей сортировки). После вывода всей геометрии для каждого пиксела его список сортируется и по отсортированному списку вычисляется итоговый цвет.
Тем самым, рендеринг осуществляется в два прохода. На первом проходе выводится вся полупрозрачная геометрия и для каждого пиксела строится список попадающих в него фрагментов. На втором проходе для каждого пиксела этот список сортируется и уже по отсортированному списку выполняется смешивание цветов и результате чего находится правильный итоговый цвет.
Каждый элемент списка будем представлять при помощи следующей структуры:
struct Node
{
vec4 color; // color of the pixel
float depth; // it's depth
uint next; // index of next fragment in the array
};
Здесь color - это цвет самого фрагмента (вместе с альфа), depth - его глубина (она понадобится доля сортировки). Поле next задает индекс следующего фрагмента для данного пиксела или -1 для последнего фрагмента.
Память под все эти списки будет выделяться в одном большом SSBO фиксированного (и задаваемого в приложении) размера.
layout(binding = 0, std430 ) buffer lists
{
Node nodes [];
uniform uint maxNodes;
};
Переменная maxNodes содержит размер буфера, чтобы мы случайно не вышли за его границы. Тогда в качестве структуры данных, хранящей для каждого пиксела указатель (индекс) на первый элемент его списка, можно использовать двухмерное изображение (точнее, поверхность) с форматом GL_R32UI.
layout (binding = 0, r32ui ) uniform uimage2D pixels;
Тогда для каждого пиксела соответствующий элемент этого изображения содержит индекс первого элемента его списка фрагментов. Изначально все изображение заполняется значением 0xFFFFFFFF.
Рис. 1. Используемые структуры данных.
Теперь давайте рассмотрим как именно осуществляется выделение очередного свободного элемента из буфера nodes. Для этого нужно ввести счетчик, хранящий в себе число уже выделенных элементов из этого буфера. Изначально его значение равно нулю.
Для выделения очередного элемента мы используем элемент, номер которого равен значению этого счетчика. Само же значение счетчика увеличивается при этом на единицу. Однако надо иметь в виду, что данная операция будет выполняться независимо большим количеством нитей, выполняющих фрагментный шейдер. Поэтому, чтобы у нас не возникло проблем, этот счетчик должен быть атомарным и сам доступ к нему также должен быть атомарным.
layout (binding = 0, offset = 0 ) uniform atomic_uint nextNode;
В результате мы приходим к фрагментному шейдеру, приводимому ниже.
#version 430 core
#define MAX_FRAGMENTS 75
layout (early_fragment_tests) in;
in vec2 tx;
in vec3 n;
in vec3 l;
out vec4 color;
struct Node
{
vec4 color;
float depth;
uint next;
};
layout (binding = 0, r32ui) uniform uimage2D heads;
layout (binding = 0, offset = 0) uniform atomic_uint numNodes;
layout (binding = 0, std430 ) buffer Lists
{
Node nodes [];
};
uniform int maxNodes;
uniform sampler2D image;
void main(void)
{
vec3 n2 = normalize ( n );
vec3 l2 = normalize ( l );
float diff = max ( dot ( n2, l2 ), 0.0 );
vec4 clr = vec4 ( 0.7, 0.1, 0.1, 1.0 );
float ka = 0.2;
float kd = 0.8;
color = (ka + kd*diff) * clr;
uint nodeIndex = atomicCounterIncrement ( numNodes );
// is there any space ?
if ( nodeIndex < maxNodes )
{
uint prev = imageAtomicExchange ( heads, ivec2 ( gl_FragCoord.xy ), nodeIndex );
nodes [nodeIndex].color = color;
nodes [nodeIndex].depth = gl_FragCoord.z;
nodes [nodeIndex].next = prev;
}
}
Задачей второго прохода является отсортировать каждый список по глубине и провести смешивание цветов с учетом порядка. Поскольку сортировать список не очень удобно, мы просто скопируем его в массив фиксированной длины и выполним сортировку вставкой уже в нем. Соответствующий фрагментный шейдер приводится ниже.
#version 430 core
#define MAX_FRAGMENTS 75
layout (early_fragment_tests) in;
out vec4 color;
struct Node
{
vec4 color;
float depth;
uint next;
};
layout (binding = 0, r32ui) uniform uimage2D heads;
layout (binding = 0, offset = 0) uniform atomic_uint numNodes;
layout (binding = 0, std430 ) buffer Lists
{
Node nodes [];
};
uniform sampler2D image;
void main(void)
{
Node frags [MAX_FRAGMENTS];
int count = 0;
// get the index of the head of the list
uint n = imageLoad ( heads, ivec2 ( gl_FragCoord.xy ) ).r;
// copy the linked list for this fragment into an array
while ( n != 0xFFFFFFFF && count < MAX_FRAGMENTS )
{
frags [count] = nodes [n];
n = frags[count].next;
count++;
}
// sort the array by depth using insertion sort (largest to smallest)
for ( int i = 1; i < count; i++ )
{
Node toInsert = frags [i];
uint j = i;
while ( j > 0 && toInsert.depth > frags [j-1].depth )
{
frags [j] = frags [j-1];
j--;
}
frags [j] = toInsert;
}
// traverse the array, and combine the colors using the alpha channel
color = vec4(0.5, 0.5, 0.5, 1.0);
for ( int i = 0; i < count; i++ )
color = mix ( color, frags [i].color, frags [i].color.a );
}
Ниже приводится код на С++.
class OitWindow : public GlutRotateWindow
{
Program program, program2;
BasicMesh * mesh; // mesh to render
glm::vec3 lightDir = glm::vec3 ( 1, 1, -1 );
VertexBuffer counter; // buffer for atomic counter
VertexBuffer lists; // buffer for fragment lists
VertexBuffer clearBuf; // buffer with 0xFFFFFFFF's to clear texture
GLuint frags; // texture, for every texel holds index into lists
ScreenQuad screen;
public:
OitWindow () : GlutRotateWindow ( 200, 50, 900, 900, "Order-Independent Transparency" )
{
GLuint maxNodes = 20 * getWidth () * getHeight ();
GLint nodeSize = 5 * sizeof(GLfloat) + sizeof(GLuint);
glClearColor ( 0, 0, 0, 1 );
glBlendFunc ( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glDisable ( GL_DEPTH_TEST );
glDepthMask ( GL_FALSE );
counter.create ();
counter.bind ( GL_ATOMIC_COUNTER_BUFFER );
counter.setData ( sizeof ( GLuint ), NULL, GL_DYNAMIC_DRAW );
lists.create ();
lists.bindBase ( GL_SHADER_STORAGE_BUFFER, 0 );
lists.setData ( maxNodes * nodeSize, NULL, GL_DYNAMIC_DRAW );
// create texture with indices into lists
glGenTextures ( 1, &frags );
glBindTexture ( GL_TEXTURE_2D, frags );
glTexStorage2D ( GL_TEXTURE_2D, 1, GL_R32UI, getWidth (), getHeight () );
glBindImageTexture ( 0, frags, 0, GL_FALSE, 0, GL_READ_WRITE, GL_R32UI );
// create buffer with 0xFFFFFFFF's to clear texture
std::vector<GLuint> ones ( getWidth () * getHeight (), 0xFFFFFFFF ); // 0xFFFFFFFF to init texture
clearBuf.create ();
clearBuf.bind ( GL_PIXEL_UNPACK_BUFFER );
clearBuf.setData ( ones.size () * sizeof ( GLuint ), ones.data (), GL_STATIC_COPY );
mesh = loadMesh ( "../../Models/teapot.3ds", 0.1f );
if ( mesh == NULL )
exit ( "Error loading mesh" );
// 1st pass shader
if ( !program.loadProgram ( "oit-collect.glsl" ) )
exit ( "Error building program: %s\n", program.getLog ().c_str () );
program.bind ();
program.setUniformVector ( "lightDir", lightDir );
program.setUniformInt ( "maxNodes", maxNodes );
program.unbind ();
// 2nd pass shader
if ( !program2.loadProgram ( "oit-render.glsl" ) )
exit ( "Error building program: %s\n", program2.getLog ().c_str () );
}
void redisplay ()
{
glm::mat4 mv = getRotation ();
glm::mat3 nm = normalMatrix ( mv );
clearBuffers ();
// pass 1 - render object to create fragment lists
glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
program.bind ();
program.setUniformMatrix ( "mv", mv );
program.setUniformMatrix ( "nm", nm );
mesh -> render ();
program.unbind ();
// pass 2 - sort lists and compute color
glFinish ();
glMemoryBarrier ( GL_SHADER_STORAGE_BARRIER_BIT );
glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
program2.bind ();
screen.render ();
program2.unbind ();
}
void reshape ( int w, int h )
{
GlutWindow::reshape ( w, h );
glm::mat4 proj = glm::perspective ( glm::radians(60.0f), getAspect(), 0.01f, 20.0f ) * glm::lookAt ( eye, glm::vec3 ( 0, 0, 0 ), glm::vec3 ( 0, 0, 1 ) );
program.bind ();
program.setUniformMatrix ( "proj", proj );
program.unbind ();
}
private:
void clearBuffers ()
{
GLuint zero = 0;
// clear atomic counter to zero
counter.bindBase ( GL_ATOMIC_COUNTER_BUFFER, 0 );
counter.setSubData ( 0, sizeof ( zero ), &zero );
// clear texture to 0xFFFFFFFF's using fast copy from clearBuf
clearBuf.bind ( GL_PIXEL_UNPACK_BUFFER );
glBindTexture ( GL_TEXTURE_2D, frags );
glTexSubImage2D ( GL_TEXTURE_2D, 0, 0, 0, getWidth (), getHeight (), GL_RED_INTEGER, GL_UNSIGNED_INT, NULL );
}
};
int main ( int argc, char * argv [] )
{
GlutWindow::init ( argc, argv );
OitWindow win;
return win.run ();
}
Рис 2. Получаемое изображение.