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