Transform feedback в OpenGL 3.3 и 4.1

Вершинный шейдер является удобным и мощным средством для обработки отдельных вершин. Помимо традиционного преобразования координат и настройки освещения он способен осуществлять довольно сложные анимации вершин и строить новые атрибуты для каждой вершины.

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

Кроме того добавление в конвейер рендеринга геометрического шейдера (OpenGL 3) и тесселляции (OpenGL 4) позволяет прямо на GPU создавать и отбрасывать как отдельные вершины, так и целые примитивы. Соответственно возможность сохранить измененную геометрию также может оказаться очень удобной.

Так например можно один раз сгенерировать сложную геометрию и далее просто использовать ее на протяжении целого ряда кадров.

Изначально подобная возможность была введена расширениями NV_transform_feedback и EXT_transform_feedback. Далее эта возможность, называемая transform feedback была введена в состав OpenGL 3.3 и доступна теперь непосредственно миную расширения.

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

Рис 1. Место transform feedback в конвейере рендеринга.

На диаграмме стадия transform feedback идет сразу перед отсечением (clipping). Поддержка transform feedback входит в OpenGL 3.3, однако в OpenGL 4.1 в transform feedback добавлены новые возможности и также введен новый тип объекта, хранящий в себе состояние transform feedback.

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

Рис 2. Работа transform feedback.

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

При помощи асинхронных запросов можно получить информацию о количестве записанных данных.

Transform feedback в OpenGL 3.3

Для начала и окончания режима transform feedback служат команды glBeginTransformFeedback и glEndTransformFeedback.

void glBeginTransformFeedback ( GLenum primitiveMode );
void glEndTransformFeedback   ();

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

Таблица 1. Допустимые режимы для режима transform feedback.

primMode Допустимый к выводу тип примитива Комментарий
GL_POINTS GL_POINTS

Выводит атрибуты вершин обработанных вершин

GL_LINES

GL_LINES

GL_LINE_LOOP

GL_LINE_STRIP

Выводит попарно атрибуты вершин для каждого из концов сегмента

GL_TRIANGLES

GL_TRIANGLES

GL_TRIANGLE_FAN

GL_TRIANGLE_STRIP

GL_QUADS

GL_QUAD_STRIP

GL_POLYGON

Выводит атрибуты для каждой из трех вершин треугольника

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

Каждый вызов glBeginTransformFeedback должен быть закрыт парным вызовом glEndTransformFeedback, вложение или перекрытие недопустимо.

Запись выходных значений (при этом для составных примитивов, например GL_TRIANGLE_STRIP, производится автоматическое разбиение на элементарные примитивы) идет в вершинные буфера, привязанные к типу GL_TRANSFORM_FEEDBACK_BUFFER и к индексу. Подобная возможность привязки буфера сразу к типу (target'у) и индексу также поддерживается и для UBO-буферов. Для задания привязки служат команды glBindBufferRange и glBindBufferBase.

void glBindBufferRange ( GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeptr size );
void glBindBufferBase  ( GLenum target, GLuint index, GLuint buffer );

Параметр target для режима transform feedback всегда должен принимать значение GL_TRANFORM_FEEDBACK_BUFFER, параметр index задает номер атрибута (из списка в glTransformFeedbackVaryings), записываемого в данный буфер, параметр buffer задает вершинный буфер в который и будет осуществляться запись атрибута(атрибутов).

Команда glBindBufferRange позволяет задать для записи атрибутов не весь буфер, а только определенный его диапазон, параметры offset и size задает его начало и длину в байтах. Обратите внимание, что значение параметра offset должно быть кратно четырем.

Существует два различных способа записи выходных значений в режиме transform feedback - GL_INTERLEAVED_ATTRIBS (когда все атрибуты записываются в один вершинных буфер) и GL_SEPARATE_ATTRIBS (когда каждому выводимому атрибуту назначается свой вершинный буфер). При этом для последнего способа именно за счет возможности привязки буфера по индексу и осуществляется привязка атрибута и буфера - первая выводимая переменная будет записываться в вершинный буфер с индексом 0, вторая - в буфер с индексом 1 и т.д.

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

int maxInterlavedAttribs ()
{
    int maxSize;
    
    glGetIntegerv ( GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS, &maxSize );
    
    return maxSize;
}

int maxSeparateAttribs ()
{
    int maxSize;
    
    glGetIntegerv ( GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS, &maxSize );
    
    return maxSize;
}

int maxBuffers ()
{
    int maxBuffers;
    
    glGetIntegerv ( GL_MAX_TRANSFORM_FEEDBACK_BUFFERS, &maxBuffers );
    
    return maxBuffers;
}

Для задания выходных переменных, подлежащих записи в режиме transform feedback служит команда glTransformFeedbackVaryings.

void glTransformFeedbackVaryings ( GLuint program, GLsizei count, const char ** names, GLenum bufferMode );

Параметр names задает массив из count имен выходных переменных, подлежащих записи. Параметр bufferMode задает режим записи и принимает одно из следующих значений - GL_INTERLEAVED_ATTRIBS или GL_SEPARATE_ATTRIBS.

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

При помощи асинхронных запросов типа GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN и GL_PRIMITIVES_GENERATED можно получить информацию о количестве записанных примитивов и количестве примитивов дошедших до стадии transform feedback.

Обратите внимание, что включение режима transform feedback вовсе не выключает растеризацию и вывод примитива. Для того чтобы полностью отключить вывод примитива (т.е. оставить только запись вершин в буфера) следует использовать команду glDisable с параметром GL_RASTERIZER_DISCARD.

glDisable ( GL_RASTERIZER_DISCARD );

В качестве примера использования режима transform feedback рассмотрим моделирование разлета частиц после взрыва в "коробке". При попадании частицы в стенку происходит отражение с частичной потерей скорости. Также присутствует сила тяжести, влияющая на движение частиц.

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

Соответственно анимация и рендеринг части легко реализуются при помощи следующих шейдеров.

-- vertex

#version 330 core

uniform mat4 proj;
uniform mat4 mv;
uniform float dt;
uniform vec3 boxSize;

layout(location = 0) in vec3 pos;
layout(location = 1) in vec3 vel;

out vec3 newPos;
out vec3 newVel;

void main(void)
{
    const vec3 acc  = vec3 ( 0, -0.02, 0 );     // gravity acceleration
    const float damp = 0.9;                     // damping for each reflection
    
    bool       refl = false;
    
    gl_Position = proj * mv * vec4 ( pos, 1.0 );
    newPos      = pos + dt * vel;
    newVel      = vel + acc * dt;
    
    if ( abs ( newPos.x ) >= boxSize.x )
    {
        newPos  -= dt * vel;                    // return to state before collision
        newVel.x = -newVel.x;
        refl     = true;
    }
    
    if ( abs ( newPos.y ) >= boxSize.y )
    {
        newPos  -= dt * vel;                    // return to state before collision
        newVel.y = -newVel.y;
        refl     = true;
    }
    
    if ( abs ( newPos.z ) >= boxSize.z )
    {
        newPos  -= dt * vel;                    // return to state before collision
        newVel.z = -newVel.z;
        refl     = true;
    }
    
    if ( refl )
        newVel *= damp;
}

-- fragment

#version 330 core

out vec4 color;

void main(void)
{
    color = vec4 ( 1.0, 1.0, 0.0, 1.0 );
}

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

#include    <GL/glew.h>

#ifdef  _WIN32
    #include    <GL/wglew.h>
#else
    #include    <GL/glxew.h>
#endif

#include    <freeglut.h>
#include    <stdio.h>
#include    <stdlib.h>
#include    <string.h>

#include    "Program.h"
#include    "glUtilities.h"
#include    "mat4.h"
#include    "mat3.h"
#include    "vec2.h"
#include    "VertexArray.h"
#include    "VertexBuffer.h"
#include    "randUtils.h"

#define NUM_PARTICLES   5000

int     mouseOldX = 0;
int     mouseOldY = 0;
float   angle     = 0;
vec3    rot   ( 0.0f );
vec3    eye   ( 3, 3, 3 );
vec3    light ( 7, 7, 7 );

int     ind = 0;
int     loc1 = -1, loc2 = -1;
vec3    p [NUM_PARTICLES];
vec3    v [NUM_PARTICLES];

Program         program;
VertexArray     vao [2];
VertexBuffer    vertexBuf [2], velBuf [2];

void    initParticles ()
{
    for ( int i = 0; i < NUM_PARTICLES; i++ )
    {
        p [i] = vec3 ( 0, 0, 0 );
        v [i] = vec3 ( randUniform ( -0.1, 0.1 ), randUniform ( -0.1, 0.1 ), randUniform ( -0.1, 0.1 ) );
    }
}

void init ()
{
    glClearColor ( 0.5, 0.5, 0.5, 1.0 );
    glEnable     ( GL_DEPTH_TEST );
    glDepthFunc  ( GL_LEQUAL );
}

void display ()
{
    static float lastTime = 0;
    
    float tm  = 0.001f * glutGet ( GLUT_ELAPSED_TIME );

    glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    mat4    mv = mat4 :: rotateZ ( toRadians ( rot.z ) ) * mat4 :: rotateX ( toRadians ( rot.x ) ) * mat4 :: rotateY ( toRadians ( rot.y ) );

    program.bind ();
    program.setUniformMatrix ( "mv",  mv );
    program.setUniformFloat  ( "dt",  tm - lastTime );
    
    vertexBuf [ind^1].bindBase ( GL_TRANSFORM_FEEDBACK_BUFFER, 0 );
    velBuf    [ind^1].bindBase ( GL_TRANSFORM_FEEDBACK_BUFFER, 1 );

    glBeginTransformFeedback ( GL_POINTS );

    vao [ind].bind ();

    glDrawArrays ( GL_POINTS, 0, NUM_PARTICLES );
    
    vao [ind].unbind ();
    
    glEndTransformFeedback ();

    program.unbind ();

    lastTime = tm;
    
    glutSwapBuffers ();
}

void reshape ( int w, int h )
{
    glViewport ( 0, 0, (GLsizei)w, (GLsizei)h );
   
    mat4 proj = perspective ( 60.0f, (float)w / (float)h, 0.5f, 20.0f ) * lookAt ( eye, vec3 :: zero, vec3 ( 0, 1, 0 ) );

    program.bind ();
    program.setUniformMatrix ( "proj",    proj );
    program.setUniformVector ( "boxSize", vec3 ( 2 ) );
    program.unbind ();  
}

void key ( unsigned char key, int x, int y )
{
    if ( key == 27 || key == 'q' || key == 'Q' )    //  quit requested
        exit ( 0 );
}

void motion ( int x, int y )
{
    rot.x += ((mouseOldY - y) * 180.0f) / 200.0f;
    rot.z += ((mouseOldX - x) * 180.0f) / 200.0f;
    rot.y  = 0;

    if ( rot.z > 360 )
        rot.z -= 360;

    if ( rot.z < -360 )
        rot.z += 360;

    if ( rot.y > 360 )
        rot.y -= 360;

    if ( rot.y < -360 )
        rot.y += 360;

    mouseOldX = x;
    mouseOldY = y;

    glutPostRedisplay ();
}

void mouse ( int button, int state, int x, int y )
{
    if ( state == GLUT_DOWN )
    {
        mouseOldX = x;
        mouseOldY = y;
    }
}

void    animate ()
{
    ind ^= 1;
    
    glutPostRedisplay ();
}

int main ( int argc, char * argv [] )
{
                                // initialize glut
    glutInit            ( &argc, argv );
    glutInitDisplayMode ( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH );
    glutInitWindowSize  ( 500, 500 );

                                // prepare context for 3.3
    glutInitContextVersion ( 3, 3 );
    glutInitContextFlags   ( GLUT_FORWARD_COMPATIBLE | GLUT_DEBUG );
    glutInitContextProfile ( GLUT_CORE_PROFILE );

                                // create window
    glutCreateWindow ( "Transform feedback" );

    glewExperimental = GL_TRUE;

    glewInit ();

    if ( !GLEW_VERSION_3_3 )
    {
        printf ( "OpenGL 3.3 not supported.\n" );
        
        return 1;
    }
    
    getGlErrorString ();        // glew gives GL_INVALID_ENUM error, just remove it
    
                                // register handlers
    glutDisplayFunc  ( display );
    glutReshapeFunc  ( reshape );
    glutKeyboardFunc ( key     );
    glutMouseFunc    ( mouse   );
    glutMotionFunc   ( motion  );
    glutIdleFunc     ( animate );

    init ();
    
    if ( !GL_ARB_vertex_array_object )
        printf ( "No VAO support\n" );

    if ( !program.loadProgram ( "tf3.glsl" ) )
    {
        printf ( "Error loading shader: %s\n", program.getLog ().c_str () );
        
        return 1;
    }
    else
        printf ( "Shader loaded:\n%s\n", program.getLog ().c_str () );

    initParticles ();

    program.bind ();
    program.transformFeedbacksVars ( "newPos;newVel", GL_SEPARATE_ATTRIBS );
    program.relink ();
    program.unbind ();

    for ( int i = 0; i < 2; i++ )
    {
        vao [i].create ();
        vao [i].bind   ();

        vertexBuf [i].create     ();
        vertexBuf [i].bind       ( GL_ARRAY_BUFFER );
        vertexBuf [i].setData    ( NUM_PARTICLES * sizeof ( vec3 ), p, GL_STATIC_DRAW );
        vertexBuf [i].setAttrPtr ( 0, 3, sizeof ( vec3 ), (void *) 0 );

        velBuf [i].create     ();
        velBuf [i].bind       ( GL_ARRAY_BUFFER );
        velBuf [i].setData    ( NUM_PARTICLES * sizeof ( vec3 ), v, GL_STATIC_DRAW );
        velBuf [i].setAttrPtr ( 1, 3, sizeof ( vec3 ), (void *) 0 );
    
        vao [i].unbind     ();
    }
    
    glutMainLoop ();

    return 0;
}

Отличия Transform feedback в OpenGL 4.1

OpenGL 4.1 вводит ряд новых возможностей в transform feedback. В первую очередь вводится новый тип объекта - transform feedback object. Этот объект содержит в себе все состояние, связанное с настройками transform feedback'а - набор буферов, точки привязки и т.д. Как и остальные объекты OpenGL, такие объекты идентифицируются при помощи беззнакового целого числа, при этом ноль является зарезервированным значением, соответствующим объекту по умолчанию (default transform feedback object). Для создания и уничтожения подобных объектов служат функции glGenTransformFeedbacks и glDeleteTransformFeedbacks.

void glGenTransformFeedbacks    ( GLsizei n, GLuint * ids );
void glDeleteTransformFeedbacks ( GLsizei n, const GLuint * ids );

При попытке уничтожить используемый в данный момент transform feedback объект, его идентификатор освобождается сразу же, а сам объект продолжает существовать пока он используется. Для выбора объекта с идентификатором id как текущего служит функция glBindTransformFeedback. Параметр target всегда принимает значение GL_TRANSFORM_FEEDBACK, а параметр idзадает идентификатор выбираемого объекта.

void glBindTransformFeedback ( GLenum target, GLuint id );

Также была добавлена возможность приостанавливать запись атрибутов и возобновлять ее вновь. Для этого служат следующие команды:

void glPauseTransformFeedback  ();
void glResumeTransformFeedback ();

Обратите внимание, что когда transform feedback приостановлен, то в этот момент можно осуществить смену текущего transform feedback объекта.

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

Для этого для команды glTransformFeedbackVaryings было добавлено несколько предопределенных имен переменных, служащих для управления записью атрибутов. Эти специальные имена предназначены только для использования в режиме GL_INTERLEAVED_ATTRIBS.

Таблица 2. Специальные имена переменных для команды glTransformFeedbackVaryings.

Имя переменной Действие
gl_NextBuffer Переключиться на следующий буфер для записи - следующая переменная будет записана в буфер со следующим индексом
gl_SkipComponents1 Пропустить следующую одну компоненту
gl_SkipComponents2 Пропустить следующие две компоненты
gl_SkipComponents3 Пропустить следующие три компоненты
gl_SkipComponents4 Пропустить следующие четыре компоненты

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

Удобной возможностью, добавленной в OpenGL 4.1, является непосредственный рендеринг из буфера (буферов) с записанными атрибутами вершин. При этом нет необходимости явно задавать число выводимых примитивов - оно будет взято автоматически из результатов transform feedback'а. Для этого служат следующие две функции:

void glDrawTransformFeedback       ( GLenum mode, GLuint id );
void glDrawTransformFeedbackStream ( GLenum mode, GLuint id, GLuint stream );

При этом вызове параметр id задает идентификатор соответствующего transform feedback объекта, содержащего информацию о записанных атрибутах. Вызов glDrawTransformFeedback эквивалентен вызову glDrawArrays с параметром first равным нулю и параметром count равному числу записанных вершин.

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

Можно легко завернуть transform feedback object в следующий класс.

class TransformFeedback4
{
    GLuint  id;
    
public:
    TransformFeedback4 ()
    {
        id = 0;
    }
    
    ~TransformFeedback4 ()
    {
        if ( id != 0 )
            glDeleteTransformFeedbacks ( 1, &id );
    }
    
    bool    isOk () const
    {
        return id != 0;
    }
    
    void    create ()
    {
        glGenTransformFeedbacks ( 1, &id );
    }
    
    void    bind ()
    {
        glBindTransformFeedback ( GL_TRANSFORM_FEEDBACK, id );
    }
    
    void    unbind ()
    {
        glBindTransformFeedback ( GL_TRANSFORM_FEEDBACK, 0 );
    }
    
    bool    begin ( GLenum primitiveMode  )
    {
        if ( !isOk () ) 
            return false;
            
        glBeginTransformFeedback ( primitiveMode );
        
        return true;
    }
    
    void    end ()
    {
        glEndTransformFeedback ();
    }
    
    void    pause ()
    {
        glPauseTransformFeedback ();
    }
    
    void    resume ()
    {
        glResumeTransformFeedback ();
    }
    
    void draw ( GLenum mode )
    {
        glDrawTransformFeedback ( mode, id );
    }
    
    void    drawStream ( GLenum mode, GLuint stream )
    {
        glDrawTransformFeedbackStream ( mode, id, stream );
    }
};

По этой ссылке можно скачать весь исходный код к этой статье. Также доступны для скачивания откомпилированные версии для M$ Windows и Linux.