Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Вершинный шейдер является удобным и мощным средством для обработки отдельных вершин. Помимо традиционного преобразования координат и настройки освещения он способен осуществлять довольно сложные анимации вершин и строить новые атрибуты для каждой вершины.
Однако обычно результаты его работы идут дальше по конвейеру рендеринга и не переиспользуются и не сохраняются. В то же время возможность сохранить результаты работы (или определенную их часть) вершинного шейдера оказывается очень удобной. Например целый ряд сложных анимаций требует для своей работы данные о предыдущем состоянии анимируемого объекта.
Кроме того добавление в конвейер рендеринга геометрического шейдера (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 служат команды 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;
}
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.