steps3D - Tutorials - Order Independent Transparency в OpenGL

Order Independent Transparency в OpenGL

Работа с полупрозрачной геометрией обладает существенным недостатком - полученное изображение зависит от того, в каком порядке выводились полупрозрачные грани. Для получения корректного изображения все полупрозрачные грани должны выводиться в порядке 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.

used data structures

Рис. 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 ();
}

screenshot

Рис 2. Получаемое изображение.