Расширения NV_transform_feedback и EXT_transform_feedback.

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

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

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

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

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

Данное расширение (работающее начиная с GPU GeForce 8xxx) позволяет все обработанные вершины (в том числе и созданные геометрическим шейдером) вывести в один или несколько вершинных буферов, а не использовать для рендеринга.

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

Рис 1. Двухшаговая анимация с сохранением вычисленных атрибутов вершин после каждого шага анимации.

Расширение NV_transform_feedback вводит новый режим работы OpenGL, служащий для записи атрибутов вершин. Этот режим работает как для программируемого конвейера рендеринга, так и для фиксированного. В этом режиме вершины всех примитивов до момента отсечение по плоскостям (clip planes) записываются в один или несколько вершинных буферов.

Также данное расширение вводит способы запросов (queries), позволяющие организовать работу асинхронно.

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

void glBeginTransformFeedbackNV ( GLenum primMode );
void glEndTransformFeedbackNV   ();

Параметр primMode принимает одно из следующий значений - GL_TRIANGLES, GL_LINES и GL_POINTS - и задает способ записи примитивов. Выбор значения для данного параметра накладывает ограничения на допустимые режимы вывода примитивов, которые перечислены в следующей таблице.

Таблица 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

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

Использование примитивов недопустимых типов ведет к ошибкам, неполные примитивы не записываются (т.е. запись вершин носит атомарный с точки зрения примитивов характер).

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

Существуют два варианта задания атрибутов вершин - interleaved (все атрибуты помещаются в один общий вершинный буфер) и separate (каждый атрибут хранится в своем вершинном буфере).

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

void glBindBufferRangeNV  ( GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size );
void glBindBufferOffsetNV ( GLenum target, GLuint index, GLuint buffer, GLintptr offset );
void glBindBufferBaseNV   ( GLenum target, GLuint index, GLuint buffer );

Параметр target всегда принимает значение GL_TRANSFORM_FEEDBACK_BUFFER_NV.

Параметр index задает номер атрибута, записываемого в данный буфер.

Параметр buffer задает вершинный буфер (buffer object) для записи атрибута, соответствующему индексу index.

Параметры offset и size задают начальное смещение в буфере и максимальный размер выводимых в буфер данных (в байтах).

Вызов функции glBindBufferBaseNV эквивалентен вызову glBindBufferOffsetNV с параметром offset равным нулю.

Вызов функции glBindBufferOffsetNV эквивалентен вызову glBindBufferRange с параметром size равным свободному месту в буфере начиная с заданного смещения и выравненному по словам (word-aligned).

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

В первом случае используется команда glTransformFeedbackVaryingsNV, служащая для явного задания списка выводимых varying переменных из вершинного шейдера.

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

void glTransformFeedbaclVaryingsNV ( GLuint program, GLsizei count, const int * locations, GLenum bufferMode );
void glTransformFeedbackAttribsNV  ( GLsizei count, const int * attribs, GLenum bufferMode );

Для команды glTransformFeedbackVaryingsNV явно задается идентификатор программы (program), количество выводимых varying-переменных (count) и массив locations, содержащий положения для всех выводимых varying-переменных.

Параметр bufferMode задает режим записи - идет ли запись всех атрибутов в один буфер (GL_INTERLEAVED_ATTRIBS_NV) или же каждый из выводимых атрибутов записывается в свой буфер (GL_SEPARATE_ATTRIBS_NV).

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

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

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

Для получения положений (locations) varying-переменных по именам служит следующая команда:

void glGetVaryingLocationNV ( GLuint program, const char * varyingName );

Расширение NV_transform_feedback добавляет два новых асинхронных запроса (queries) к функциям glBeginQuery и glEndQuery.

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

Существует способ явно задать линкеру какие varying-переменные должны быть активными при помощи следующей команды:

void glActiveVaryingNV ( GLuint program, const char * varyingName );

После задания при помощи этой команды дополнительных активных переменных, следует произвести перелинковку программы. Для поддержки работы с активными varying-переменными добавим в класс GlslProgram следующие методы:

void addActiveVaryings ( const string& name );
bool relink            ();

void glBeginQuery ( GLenum target, GLuint id );
void glEndQuery   ( GLenum target );

Значение параметра target равное GL_PRIMITIVES_GENERATED_NV позволяет получить количество сгенерированных примитивов. Значение, равное GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN_NV позволяет отслеживать количество записанных вершин

Для получения информации о запросах служит следующая функция:

void glGetQueryiv ( GLenum target,  GLenum pname, int    * param );

В качестве параметра target могут выступать константы GL_TRANSFORM_FEEDBACK_PRIMITIVES_GENERATERD_NV и GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN_NV. Параметр pname может принимать следующие значения - GL_CURRENT_QUERY и GL_QUERY_COUNTER_BITS. В первом случае мы получаем идентификатор текущего запроса, а во втором случае - разрядность используемого счетчика.

Информация по запросы с заданным идентификатором queryId может быть получена при помощи следующий команд:

void glGetQueryObjectiv  ( GLuint queryId, GLenum pname, int    * param );
void glGetQueryObjectuiv ( GLuint queryId, GLenum pname, GLuint * param );

Если pname равно GL_QUERY_RESULT, то в params записывается одно значение - значение соответствующего запросу счетчика. В случае pname равного GL_QUERY_RESULT_AVAILABLE, то мы получаем признак готовности результата (для GL_QUERY_RESULT).

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

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

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

//
// NV_transform_feedback vertex shader
//

varying vec4  newPos;
varying vec4  newVel;
uniform float dt;

//
// check bounce from a horizontal rectangle
//
bool checkBounce ( inout vec4 pos, inout vec4 vel, in float dt, in vec3 boxMin, in vec3 boxMax )
{
    float   h  = boxMin.z;              // z-coord of the rectangle
    float   dz = vel.z * dt;
    
    if ( (pos.z - h) * (pos.z + dz - h) > 0.0 )
        return false;                   // no collision
        
    float   t  = (h - pos.z) / vel.z;   // time of collision
    vec3    pc = pos.xyz + t * vel.xyz; // poin of collision with z plane
    
    if ( !all(lessThan((pc.xy - boxMin.xy)*(pc.xy - boxMax.xy), vec2 ( 0.0 ))))
        return false;                   // collision point out of rect
        
    vel.z = -vel.z;                     // bounce off
    vel  *= 0.9;                        // dumping
    
    pos.xyz = pc + (dt - t) * vel.xyz;
    
    return true;
}

void main ()
{
    const vec3 box1Min = vec3 ( -10.0, -10.0, -2.0  );
    const vec3 box1Max = vec3 (  10.0,  10.0, -2.0  );
    const vec3 box2Min = vec3 (  0.0,  0.0, -1.5    );
    const vec3 box2Max = vec3 (  2.0,  2.0, -1.5    );
    const vec3 box3Min = vec3 ( -2.0, -2.0, -1.0    );
    const vec3 box3Max = vec3 (  0.0,  0.0, -1.0    );
    const vec4 g       = vec4 (  0.0,  0.0, -1.0, 0.0 );
    
    vec4    pos = gl_Vertex;
    vec4    vel = gl_MultiTexCoord0 + dt * g;
    
    if ( !checkBounce ( pos, vel, dt, box1Min, box1Max ) )
        if ( !checkBounce ( pos, vel, dt, box2Min, box2Max ) )
            if ( !checkBounce ( pos, vel, dt, box3Min, box3Max ) )
                pos += vel * dt;

    newPos      = vec4 ( pos.xyz, 1.0 );
    newVel      = vel;
    gl_Position = pos;
}

Входные данные - положение частицы и ее скорость берутся из gl_Vertex и gl_MultiTexCoord0 и выходные данные записываются в varying-переменные newPos и newVel.

Приведенный выше вершинный шейдер отвечает только за корректный расчет анимации частиц с учетом отскоков. Для вывода частиц используются обычные шейдеры (они содержатся в исходной коде к статье).

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

На каждом шаге сперва первая вершинная программа вычисляет параметры частиц на очередном шаге, после чего осуществляется их рендеринг. После чего буфера меняются местами. Весь расчет анимации целиком ведется на GPU.

#include    "libExt.h"

#ifdef  MACOSX
    #include    <GLUT/glut.h>
#else
    #include    <glut.h>
#endif

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

#include    "libTexture.h"
#include    "TypeDefs.h"
#include    "Vector3D.h"
#include    "Vector2D.h"
#include    "GlslProgram.h"
#include    "utils.h"
#include    "Camera.h"
#include    "VertexBuffer.h"

#define NUM_PARTICLES   2000

Vector3D    eye   ( -7.0, 0, 2 );       // camera position
unsigned    decalMap;                   // decal (diffuse) texture
unsigned    stoneMap, woodMap;
GLuint      query;

float   angle = 0;
float   yaw   = 0;
float   pitch = 0;
float   roll  = 0;

Camera      camera ( eye, 0, 0, 0 );    // camera to be used

GlslProgram program1;                       // build G-buffer
GlslProgram program2;                       // test G-buffer
VertexBuffer * posBuf [2];                  // buffer with vertices
VertexBuffer * velBuf [2];                  // buffers with velocities
int            bufIndex = 0;

void displayBoxes ();
void reshape      ( int w, int h );

inline  float   rnd ()
{
    return (float) rand () / (float) RAND_MAX;
}

inline  float   rnd ( float a, float b )
{
    return a + rnd () * (b - a);
}

void createBuffers ()
{
    float   posData [NUM_PARTICLES * 4];
    float   velData [NUM_PARTICLES * 4];
    int     i;
    
    for ( i = 0; i < NUM_PARTICLES; i++ )
    {
        posData [4*i + 0] = 0;
        posData [4*i + 1] = 0;
        posData [4*i + 2] = 5;
        posData [4*i + 3] = 1;
        velData [4*i + 0] = rnd (-1,1)*0.4;
        velData [4*i + 1] = rnd (-1,1)*0.4;
        velData [4*i + 2] = rnd (-1,1)*0.4;
        velData [4*i + 3] = 0;
    }
    
    for ( i = 0; i < 2; i++ )
    {
        posBuf [i] = new VertexBuffer ();
        velBuf [i] = new VertexBuffer ();
        
        posBuf [i] -> bind    ( GL_ARRAY_BUFFER_ARB );
        posBuf [i] -> setData ( NUM_PARTICLES * 4 * sizeof ( float ), posData, GL_STREAM_COPY );
        posBuf [i] -> unbind  ();

        velBuf [i] -> bind    ( GL_ARRAY_BUFFER_ARB );
        velBuf [i] -> setData ( NUM_PARTICLES * 4 * sizeof ( float ), velData, GL_STREAM_COPY );
        velBuf [i] -> unbind  ();
    }
}

void displayPoints ( int index, GlslProgram& program )
{
    float quadratic [] =  { 1.0f, 0.0f, 0.01f };

    glEnable      ( GL_TEXTURE_2D );
    glBindTexture ( GL_TEXTURE_2D, decalMap );
    glDepthMask   ( GL_FALSE );
    glEnable      ( GL_BLEND );
    glBlendFunc   ( GL_ONE, GL_ONE );
    glPointSize   ( 5 );
    
    glEnable              ( GL_POINT_SPRITE_ARB );
    glPointParameterfvARB ( GL_POINT_DISTANCE_ATTENUATION_ARB, quadratic );
    glPointParameterfARB  ( GL_POINT_FADE_THRESHOLD_SIZE_ARB,  20.0f );
    glEnable              ( GL_VERTEX_PROGRAM_POINT_SIZE_ARB );
    glTexEnvf             ( GL_POINT_SPRITE_ARB, GL_COORD_REPLACE_ARB, GL_TRUE );

    glPushClientAttrib  ( GL_CLIENT_VERTEX_ARRAY_BIT );

                                                // setup vertices array
    posBuf [index] -> bind ( GL_ARRAY_BUFFER_ARB );
    glEnableClientState ( GL_VERTEX_ARRAY );
    glVertexPointer     ( 4, GL_FLOAT, 0, (void *)0 );

    posBuf [index] -> unbind ();
    velBuf [index] -> bind ( GL_ARRAY_BUFFER_ARB );
                                                // setup texcoord0 array
    glEnableClientState ( GL_TEXTURE_COORD_ARRAY );
    glTexCoordPointer   ( 4, GL_FLOAT, 0, (void *)0 );

    velBuf [index] -> unbind ();
                                                // setup l array
    glDrawArrays ( GL_POINTS, 0, NUM_PARTICLES );

    glPopClientAttrib ();
    glDepthMask       ( GL_TRUE );
    glDisable         ( GL_BLEND );
}

void display ()
{
    unsigned primitivesWritten;
    
    glClear                       ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    program1.bind                 ();
    glBindBufferOffsetNV          ( GL_TRANSFORM_FEEDBACK_BUFFER_NV, 0, posBuf [bufIndex ^ 1] -> getId (), 0 );
    glBindBufferOffsetNV          ( GL_TRANSFORM_FEEDBACK_BUFFER_NV, 1, velBuf [bufIndex ^ 1] -> getId (), 0 );
    glBeginTransformFeedbackNV    ( GL_POINTS );
    glEnable                      ( GL_RASTERIZER_DISCARD_NV );    // disable rasterization
    glBeginQueryARB               ( GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN_NV, query );
    displayPoints                 ( bufIndex, program1 );
    glDisable                     ( GL_RASTERIZER_DISCARD_NV );
    glEndQueryARB                 ( GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN_NV );
    glEndTransformFeedbackNV      ();
    program1.unbind               ();

    glGetQueryObjectuivARB ( query, GL_QUERY_RESULT, &primitivesWritten );
  
	printf ( "Prim. written: %d\n", primitivesWritten );

    camera.apply ();

// draw the reflectors

    glEnable      ( GL_TEXTURE_2D );
    glBindTexture ( GL_TEXTURE_2D, stoneMap );
    glBegin       ( GL_QUADS );
        glTexCoord2f ( 0, 0 );
        glVertex3f   ( -10.0, -10.0, -2.0  );
        glTexCoord2f ( 10, 0 );
        glVertex3f   (  10.0, -10.0, -2.0  );
        glTexCoord2f ( 10, 10 );
        glVertex3f   (  10.0,  10.0, -2.0  );
        glTexCoord2f ( 0, 10 );
        glVertex3f   (  -10.0,  10.0, -2.0  );
    glEnd         ();
        
    glEnable      ( GL_TEXTURE_2D );
    glBindTexture ( GL_TEXTURE_2D, woodMap );
    glBegin       ( GL_QUADS );
        glTexCoord2f ( 0, 0 );
        glVertex3f ( 0.0, 0.0, -1.5 );
        glTexCoord2f ( 1, 0 );
        glVertex3f ( 2.0, 0.0, -1.5 );
        glTexCoord2f ( 1, 1 );
        glVertex3f ( 2.0, 2.0, -1.5 );
        glTexCoord2f ( 0, 1 );
        glVertex3f ( 0.0, 2.0, -1.5 );
        
        glTexCoord2f ( 0, 0 );
        glVertex3f ( -2.0,  -2.0, -1.0 );
        glTexCoord2f ( 1, 0 );
        glVertex3f (  0.0,  -2.0, -1.0 );
        glTexCoord2f ( 1, 1 );
        glVertex3f (  0.0,   0.0, -1.0 );
        glTexCoord2f ( 0, 1 );
        glVertex3f ( -2.0,   0.0, -1.0 );
    glEnd         ();
    
    program2.bind   ();
    displayPoints   ( bufIndex ^ 1, program2 );
    program2.unbind ();
    
    bufIndex ^= 1;
    
    glutSwapBuffers ();
}

void reshape ( int w, int h )
{
    camera.setViewSize ( w, h, 60 );
    camera.apply       ();
}

void key ( unsigned char key, int x, int y )
{
    if ( key == 27 || key == 'q' || key == 'Q' )        // quit requested
        exit ( 0 );
    else
    if ( key == 'w' || key == 'W' )
        camera.moveBy ( camera.getViewDir () * 0.2 );
    else
    if ( key == 'x' || key == 'X' )
        camera.moveBy ( -camera.getViewDir () * 0.2 );
    else
    if ( key == 'a' || key == 'A' )
        camera.moveBy ( -camera.getSideDir () * 0.2 );
    else
    if ( key == 'd' || key == 'D' )
        camera.moveBy ( camera.getSideDir () * 0.2 );

    glutPostRedisplay ();
}

void    specialKey ( int key, int x, int y )
{
    if ( key == GLUT_KEY_UP )
        yaw += M_PI / 90;
    else
    if ( key == GLUT_KEY_DOWN )
        yaw -= M_PI / 90;
    else
    if ( key == GLUT_KEY_RIGHT )
        roll += M_PI / 90;
    else
    if ( key == GLUT_KEY_LEFT )
        roll -= M_PI / 90;

    camera.setEulerAngles ( yaw, pitch, roll );

    glutPostRedisplay ();
}

void    mouseFunc ( int x, int y )
{
    static  int lastX = -1;
    static  int lastY = -1;

    if ( lastX == -1 )              // not initialized
    {
        lastX = x;
        lastY = y;
    }

    yaw  -= (y - lastY) * 0.02;
    roll += (x - lastX) * 0.02;

    lastX = x;
    lastY = y;

    camera.setEulerAngles ( yaw, pitch, roll );

    glutPostRedisplay ();
}

void    animate ()
{
    static  float   lastTime = 0.0;
    float           time     = 0.001f * glutGet ( GLUT_ELAPSED_TIME );
    float           dt       = time - lastTime;
    
    lastTime = time;

    program1.bind   ();
    program1.setUniformFloat ( "dt", dt );
    program1.unbind ();
    
    glutPostRedisplay ();
}

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


                                // create window
    glutCreateWindow ( "OpenGL NV_transform_feedback example" );

                                // register handlers
    glutDisplayFunc       ( display    );
    glutReshapeFunc       ( reshape    );
    glutKeyboardFunc      ( key        );
    glutSpecialFunc       ( specialKey );
    glutPassiveMotionFunc ( mouseFunc  );
    glutIdleFunc          ( animate    );

    init           ();
    initExtensions ();

    assertExtensionsSupported ( "NV_transform_feedback" );

    decalMap = createTexture2D ( true, "../../Textures/Fire.bmp" );
    stoneMap = createTexture2D ( true, "../../Textures/16.jpg"   );
    woodMap  = createTexture2D ( true, "../../Textures/oak.bmp"  );

    createBuffers   ();
    glGenQueriesARB ( 1, &query );
    
    if ( !program1.loadShaders ( "transform.vsh", "transform.fsh" ) )
    {
        printf ( "Error loading transform shaders:\n%s\n", program1.getLog ().c_str () );

        return 3;
    }
    
    if ( !program2.loadShaders ( "particles.vsh", "particles.fsh" ) )
    {
        printf ( "Error loading drawing shaders:\n%s\n", program2.getLog ().c_str () );

        return 3;
    }
    
                                    // register newPos and newVal as active
    program1.addActiveVaryings ( "newPos" );
    program1.addActiveVaryings ( "newVel" );
    program1.relink            ();          // relink required
    
    int locs [2];
    
    locs [0] = glGetVaryingLocationNV ( program1.getProgram (), "newPos" );
    locs [1] = glGetVaryingLocationNV ( program1.getProgram (), "newVel" );

    program1.bind                 ();
    glTransformFeedbackVaryingsNV ( program1.getProgram (), 2, locs, GL_SEPARATE_ATTRIBS_NV );
    program1.unbind               ();

    program2.bind        ();
    program2.setTexture  ( "decalMap", 0 );
    program2.unbind      ();

    camera.setRightHanded ( false );
    
    glutMainLoop ();

    return 0;
}

Рис 2. Скриншот анимации.

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

Хорошая статья по этому расширению также есть на gamedev.ru.

Используются технологии uCoz