вторник, 21 февраля 2012 г.

Основы VBO в OpenGL. Часть четвертая, динамика


До этого момента мы рассматривали работу со статикой – объектами геометрия которых не изменяется на протяжении выполнения программы, к примеру здание, стол, 3D модель и т.д.
К сожалению не смотря на все свои возможности VBO при работе со статикой проигрывает дисплейным спискам. Каким образом они формируют свой буфер – науке не известно, никакой литературы по этому вопросу я не нашел, а NV лишь ссылается на свою утилиту NVTriStrip, но даже оптимизированный VBO буфер не способен превзойти по скорости дисплейные списки при работе со статикой, особенно низкополигональной и особенно при большом количестве переключений состояний ОГЛ (не исключено что в ближайшем будущем эта ситуация кардинально измениться, но пока тесты говорят об обратном).

К сожалению в OpenGL 3.x дисплейные списки перестали существовать(в профиле "Core", в профиле совместимости их все так же можно использовать, но на свой страх и риск), потому так или иначе нам придется работать с VBO, но, не смотря на то что дисплейные списки выигрывают у VBO по скорости работы со статикой, у них есть существенный недостаток – у них нельзя изменить структуру или часть данных, без перестройки всего списка, что занимает очень существенное время (с учетом генерации, оптимизации и передачи в видеопамять).

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

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

Второй вариант изменить данные в видеопамяти заключается в использовании функций
glBufferSubData. Данная функция позволяет заменить часть данных (или все данные) в видеопамяти. Рассмотрим простейший случай из первой первого урока (Листинг 1):
1
2
3
4
5
6
7
var vId: Cardinal;
begin
  glGenBuffers( 1, @vId );
  glBindBuffer(GL_ARRAY_BUFFER, vId );
  glBufferData(GL_ARRAY_BUFFER, sizeof(GLFLoat)*3*Count, @VertexBuffer[0], GL_STATIC_DRAW);
  glBindBuffer(GL_ARRAY_BUFFER,0);
end;

Это уже хорошо известный нам пример создания вершинного буфера, ключевыми для нас сейчас является функция glBufferData и способ использования буфера как GL_STATIC_DRAW.
Команда glBufferData копирует все содержимое нашего массива VertexBuffer в видеопамять, и говорит драйверу что данные будут меняться очень редко. Это классический пример создания вершинного буфера, но есть и второй способ:
Листинг 21:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var vId: Cardinal;
    Size: integer;
begin
  glGenBuffers( 1, @vId );
  glBindBuffer(GL_ARRAY_BUFFER, vId );
  Size := sizeof(GLFLoat)*3*Count;
  glBufferData(GL_ARRAY_BUFFER, Size, nil, GL_STREAM_DRAW);
  glBufferSubData(GL_ARRAY_BUFFER, 0, Size, @VertexBuffer[0]);
  glBindBuffer(GL_ARRAY_BUFFER,0);
end;

Как видите, создание буфера немного изменилось, во-первых – вместо указателя на наш вершинный буфер используется указатель nil, во-вторых – добавилась команда glBufferSubData. Что все это означает? Это означает что командой glBufferData мы только зарезервировали в видеопамяти участок размером Size (указатель nil говорит драйверу что нас не волнуют старые данные, говорят это добавляет производительности), а второй командой – мы осуществили передачу данных из VertexBuffer в видеопамять. Тоесть повторили действие команды glBufferData с явным указанием участка оперативной памяти, из которого нужно скопировать данные. Данный подход к объявлению вершинных буферов очень часто используется при работе с так называемыми Interleaved Arrays, по сути – массив вершинных координат, склеенный с массивами вершинных атрибутов в один буфер VBO. Такой подход при рендеринге позволяет сэкономить на нескольких вызовах функции  glBindBuffer, так как все данные находятся в одном буфере. В литературе 2003-2006 годов очень часто можно найти именно такой пример работы с вершинными атрибутами, из-за небольшой экономии на биндах отдельных буферов VBO. В OpenGL 2.1 появилось новое расширение ARB_vertex_array_object, или сокращенно VAO, которое, по аналогии с дисплейными списками, позволяет объединить процесс биндинга всех буферов VBO в одно действие, таким образом, сделав данную оптимизацию неактуальной. Более подробно работу с VAO и с Interleaved Arrays мы рассмотрим чуть позже.

Все это пока что касалось все той же статики, но это привело нас к идее как можно изменить участок данных в видеопамяти – ведь не принципиально передаем мы данные в только что созданный буфер или в созданный ранее. Вот именно для этой цели мы и изменили назначение буфера со STATIC_DRAW на STREAM_DRAW. На самом деле это было не обязательно, так как менять данные мы можем в обоих случаях, а тесты показывают что на сегодняшний день нет разницы в фпс при изменении назначения буфера, потому считайте что мы используем «зарезервированные» возможности, но все может измениться с выходом новой версии драйвера/чипа, где оптимизация будет проявляться более существенно, потому рекомендуется выставлять правильное назначение буфера.

Давайте теперь попробуем изменить содержимое данного буфера во время выполнения нашей программы, для этого напишем еще одну функцию и назовем ее VBOUpdate.
Листинг 22:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
procedure VBOUpdate(time:single);
var Vertex: TVertex;
begin
  glBindBuffer(GL_ARRAY_BUFFER, vId );
  Vertex.X:=sin(time);
  Vertex.Y:=1;
  Vertex.Z:=0;
  glBufferSubData(GL_ARRAY_BUFFER, 0, SizeOf(Vertex), @Vertex);
  glBindBuffer(GL_ARRAY_BUFFER,0);
end;

Ну и чтоб начать анимацию – добавим вызов нашей функции в каденсер, добавив туда строку VBOUpdate(newTime).
Идея данной функции заключается в том, чтоб по гармоническому закону, в зависимости от величины time, менять координату X одной из вершин нашего четырехугольника, в частности – левой верхней. Результат работы данной функции можно посмотреть на картинке ниже.
На скриншоте конечно динамики не видно, но вы можете запустить соответствующую демку Demo5

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

Точечное изменение данных в буфере может быть очень полезным, но иногда бывает необходимость внести массовые изменения. Такие изменения могут быть двух типов - изменился весь буфер (или его кусок) и изменилась часть данных по всему буферу. В первом случае более эффективно использование команды glBufferSubData, так как мы просто заливаем данные в видеопамять. Во втором случае не рационально обновлять многомегабайтный буфер только из-за того, что в нем поменялся первый и последний байт. Безусловно, для этого можно дважды воспользоваться командой glBufferSubData, но что делать если таких изменений сотни? В этом случае нам на помощь приходит команда glMapBuffer, которая отображает область видеопамяти в системную память, таким образом, любые действия с предоставленным указателем на стороне CPU сразу же отражаются в видеопамяти. Сама процедура отображения буфера очень дорогостоящая, потому пользоваться ей нужно аккуратно и желательно после нескольких тестов.

По аналогии с предыдущим примером рассмотрим как вариант создания буфера с нуля (к примеру для статики) так и изменение данного буфера во времени.

Итак, третий способ изменения данных в видеопамяти – glMapBuffer. Разберем вначале идею создания буфера посредством данной команды – прежде чем использовать команду glMapBuffer, нам нужно указать с каким буфером мы собираемся работать а так же его размер, делается это как и в предыдущем случае посредством вызова пары функций glBindBuffer + glBufferData, после того как мы сделали нужный буфер активным, мы можем попросить OpenGL сделать отображение данного буфера из видеопамяти в оперативную память. В таком режиме все изменения в указанном блоке оперативной памяти будут перенесены видеопамятью, на которую ссылается наш буфер. Пока мы это делаем – запрещено производить какие либо манипуляции с видеопамятью, или вызывать какие либо команды OpenGL, так как это может привести к непредвиденным результатам. Так же нельзя использовать данный блок памяти в качестве указателя для других функций, так как после glUnMapBuffer он перестанет существовать. После того как мы закончили работать с данным буфером, нам нужно в обязательном порядке освободить данный указатель. Делается это при помощи специальной команды glUnMapBuffer.
Попробуем реализовать все написанное:
Листинг 23:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Type TVertexArray = array of TVertex;
var p:pointer;
    i:integer;
    Size, Count:integer;
begin
  Count := High(VertexBuffer)+1;
  Size := sizeof(GLFLoat)*3*Count;
  glGenBuffers( 1, @vId );
  glBindBuffer(GL_ARRAY_BUFFER, vId );
  glBufferData(GL_ARRAY_BUFFER, Size, nil, GL_STREAM_DRAW);
  p := glMapBuffer(GL_ARRAY_BUFFER, GL_READ_WRITE);
  for i:=0 to Count-1 do TVertexArray(p)[i]:=VertexBuffer[i];
  glUnMapBuffer(GL_ARRAY_BUFFER);
  glBindBuffer(GL_ARRAY_BUFFER, 0 );
end;

Итак, строки 6-10 у нас полностью повторяют вариант использования glBufferSubData, точно так же создаем новый буфер, биндим его, выделяем в видеопамяти место под него, после чего в 11 строке командой glMapBuffer делаем отображение этого буфера из видеопамяти в оперативную память, и получаем указатель на эту область памяти (p). Данный буфер можно отобразить в оперативную память тремя способами – только для чтения, только для записи и для чтения и записи. Этим вариантам доступа соответствуют константы GL_READ_ONLY, GL_WRITE_ONLY и GL_READ_WRITE. К данной области памяти мы можем обращаться как угодно, в данном примере мы типизировали указатель как одномерный массив типа TVertex, благодаря чему можем достаточно удобно обращаться к любому элементу массива, чем мы и воспользуемся в строке 12,  заполнив в цикле данную область памяти элементами нашего вершинного буфера. После того как буфер заполнен (или внесены нужные изменения) мы в строке 13 освобождаем данную область памяти командой glUnMapBuffer.

Ну и для закрепления – напишем небольшую функцию, меняющую по времени цвет наших вершин, используя отображение буфера в оперативную память посредством glMapBuffer, ничего нового в этом примере нет, потому особо останавливаться на нем не буду – отображаем буфер в память, приводим указатель к типу TColorArray и по гармоническому закону меняем компоненты цвета, после чего освобождаем буфер через glUnMapBuffer.
Листинг 24:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Type TColorArray = array of TRGBTriple;
procedure VBOColorUpdate(time:single);
var p:pointer;
    i:integer;
    Count:integer;
begin
  Count := High(VertexBuffer)+1;
  glBindBuffer(GL_ARRAY_BUFFER, cId );
  p := glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
  for i:=0 to Count-1 do begin
    with TColorArray(p)[i] do begin
      rgbtBlue:=trunc(abs(ColorBuffer[i].rgbtBlue*sin(time)));
      rgbtGreen:=trunc(abs(ColorBuffer[i].rgbtGreen*cos(time)));
      rgbtRed:=trunc(abs(ColorBuffer[i].rgbtRed*sin(time)*cos(time)));
    end;
  end;
  glUnMapBuffer(GL_ARRAY_BUFFER);
  glBindBuffer(GL_ARRAY_BUFFER, 0 );
end;

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

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

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

Бинарники с исходниками описанных демок можно взять тут: Demo5, Demo6.

1 комментарий: