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

Основы VBO в OpenGL. Часть пятая, дополнительные возможности


  1. Часть первая, первый квадрат
  2. Часть вторая, добавляем атрибуты
  3. Часть третья, индексный буфер
  4. Часть четвертая, динамика
  5. Часть пятая, дополнительные возможности
  6. Часть шестая, VAO
  7. Ссылки по теме

Сейчас мы рассмотрим еще пару возможностей работы с вершинными буферами о которых упоминалось в статье, но полное описание которых осталось за кадром, в частности работа с так называемыми Interleaved Arrays и рассмотрим возможность чтения данных из видеопамяти.
Начнем с чтения данных из видеопамяти, так как это напрямую связано с изложенным выше материалом. До этого момента у нас все буферы создавались с режимом GL_X_DRAW, это говорило о том, что данные буферные объекты будут использоваться в основном для рисования, но кроме этого существует еще несколько констант, задающих доступ к буферу на «чтение»:
GL_STREAM_READ, GL_DYNAMIC_READ, GL_STATIC_READ. Кроме режима чтения есть еще комбинированный режим доступа – чтение + рисование, константы задающие данный режим доступа имею в конце слово _COPY: GL_STREAM_COPY, GL_DYNAMIC_COPY и GL_STATIC_COPY.

Давайте рассмотрим случай, когда нам нужно прочитать данные из VBO буфера в оперативную память. Я уже говорил раньше, что после того как мы передали данные в видеопамять, из оперативной памяти эти же данные можно удалить, так как в них уже нет необходимости. Допустим вы послушались меня и действительно удалили эти данные, но, вдруг, вы решили внести в эти данные какие-то изменения, к примеру уменьшить высоту в точке ландшафта вдвое. Задача тривиальная, записывается как H:=H/2 но беда в том, что у нас нет исходной высоты, так как все данные о высотах мы удалили. Вот тут-то и может пригодиться возможность прочесть данные напрямую из видеопамяти, изменить их и вернуть назад в видеопамять.

Давайте попробуем это реализовать на базе предыдущих примеров. Так как данные у нас будут не только читаться, но и использоваться для рисования, то очевидно что нам потребуется создать буфер с режимом доступа GL_X_COPY, так как данные мы будем менять всего один раз, то нам будет достаточно использование режима GL_STATIC_COPY.
Второй момент – выбор варианта чтения/записи видеопамяти, как вы помните у нас для этого есть два способа – отображение буфера в оперативную память посредством glMapBuffer и чтение фрагмента буфера используя команду glGetBufferSubData, который мы еще не рассматривали, но как вы уже догадались принцип действия его аналогичен использованию рассмотренной нами команды glBufferSubData. Так как данный буфер будет находиться где-то в видеопамяти, то мы о нем ничего не знаем – ни его тип, ни размер, ни способ доступа к нему. Без этой информации правильно прочитать данные из такого буфера будет затруднительно. К счастью у нас есть возможность получить исчерпывающую информацию о заданном буфере зная лишь его идентификатор и поможет нам в этом команда glGetBufferParameteriv.

Итак, первым делом нам нужно получить информацию о текущем буфере, попробуем сделать это. Для удобства создадим новую структуру, которая и будет содержать полученную нами информацию:
Листинг 25:
1
2
3
4
5
6
  TBuffParam = record
    Size: GLUInt;   //Размер буфера
    Usage: GLUInt;  //назначение буфера
    Access: GLUInt; //режим доступа к буферу
    Mapped: Boolean;//захвачен ли буфер
  end;

Теперь напишем функцию которая будет возвращать нам информацию по указанному буферу:
Листинг 26:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function GetVBOParam(BuffId:integer):TBuffParam;
var Temp:PGLInt;
begin
  Assert(glIsBuffer(BuffId),'Буфера с таким идентификатором не существует!');
  new(Temp);
  glBindBuffer( GL_ARRAY_BUFFER, BuffId );
  with Result do begin
    glGetBufferParameteriv(GL_ARRAY_BUFFER,GL_BUFFER_SIZE,Temp);
    Size:=Temp^;
    glGetBufferParameteriv(GL_ARRAY_BUFFER,GL_BUFFER_USAGE,Temp);
    Usage:=Temp^;
    glGetBufferParameteriv(GL_ARRAY_BUFFER,GL_BUFFER_ACCESS,Temp);
    Access:=Temp^;
    glGetBufferParameteriv(GL_ARRAY_BUFFER,GL_BUFFER_MAPPED,Temp);
    Mapped:=Boolean(Temp^);
  end;
  glBindBuffer( GL_ARRAY_BUFFER, 0 );
end;

Функция glGetBufferParameteriv как и многие функции OpenGL работает по принципу – сказали что нам нужно, сказали куда вернуть результат и OpenGL его вернет. Как вы уже могли догадаться – узнать мы можем любой из 4-х возможных параметров – размер буфера в байтах, назначение буфера (_STATIC_, _DYNAMIC_,_STREAM_), режим доступа к буферу (_DRAW,_READ,_COPY) и узнать отображен ли в текущий момент этот буфер в оперативную память.
Прежде чем вызывать данную функцию нам нужно сделать нужный буфер активным посредством glBindBuffer, но на всякий случай проверим существует ли данный буфер вообще – это выполняется при помощи команды glIsBuffer, как ей пользоваться понятно из примера.
После того как мы забиндили нужный буфер, просто вызываем функцию glGetBufferParameteriv с разными константами и заполняем результирующий буфер полученными данными. Строки 4, 6 и 17 нужны в общем случае и в качестве примера, хотя в реальной задаче обычно вначале биндится нужный буфер, потом считываются его параметры, потом осуществляется запись/чтение в буфер и только потом освобождается, тоесть выполняется в теле одной процедуры. Мы же сейчас для надежности выполняем бинд буфера дважды – один раз для получения параметров и второй раз для чтения записи.
Итак, информацию по буферу мы получили, самое время попытаться этот буфер прочитать и внести в него изменения, реализуем эту задачу обоими подходами. Вариант первый:
Листинг 27:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
procedure VBOReadBuffer;
var BParam:TBuffParam;
    TempBuff:TVertexArray;
    Count, i:integer;
begin
  BParam := GetVBOParam (vId);
  if BParam.Access<>GL_READ_WRITE then begin
  showmessage('Unsupported buffer access type!');exit; end;
  Count:=BParam.Size div sizeof(TVertex);
  setlength(TempBuff,Count);
  glBindBuffer( GL_ARRAY_BUFFER, vId);
  glGetBufferSubData(GL_ARRAY_BUFFER, 0, BParam.Size, @TempBuff[0]);
  for i:=0 to Count-1 do with TempBuff[i] do 
  begin X:=X/2; Y:=Y/2; Z:=Z/2;  end;
  glBufferSubData(GL_ARRAY_BUFFER, 0, BParam.Size, @TempBuff[0]);
  glBindBuffer( GL_ARRAY_BUFFER, 0);
end;
Кода конечно много, но давайте все же соберемся с силами и попробуем его разобрать. Вначале разберем объявленные переменные – в BParam у нас будут содержатся параметры интересующего нас буфера, переменная TempBuff будет хранить прочитанные из видеопамяти данные, так как размер данных у нас возвращается в байтах, то зная размер одной записи вычислим количество записей в нашем буфере Count.
Теперь по коду процедуры – первым делом получаем параметры нашего вершинного буфера (строка 6), проверяем подходит ли этот буфер нам – нам необходимо иметь возможность осуществлять чтение и запись в этот буфер, потому мы и проверяем режим доступа на соответствие константе GL_READ_WRITE. Вычислили количество записей в буфере (строка 9) и создали динамический массив с указанным количеством записей (10).
Подготовительные этапы закончены, самое время прочитать данные, для этого мы в первую очередь биндим нужный нам буфер и вызываем функцию glGetBufferSubData, указав что нам нужно прочитать из видеопамяти BParam.Size байт, начиная с самого начала буфера (0) и поместить эти данные в наш массив TempBuff. После того как данные прочитаны мы можем с ними делать все что нам вздумается, я решил уменьшить наш квадрат вдвое, что и делается в строке 14 для каждого из элементов массива. После того, как данные изменены, мы уже известным нам способом возвращаем их в видеопамять, воспользовавшись функцией glBufferSubData. Ну и после всех изменений освободим данный буфер в строке 16.
Все что нам осталось сделать – из какого-то места вызвать данную функцию, я решил ее вызывать после нажатия кнопки на форме. Единственный ньюанс – пока мы изменяем данные нужно запретить сцене работать с буферами, это мы сделаем путем установки флага Ready := false, не забыв включить флаг после завершения изменений:
Листинг 28:
1
2
3
4
procedure TForm1.Button1Click(Sender: TObject);
begin
  Ready:=false; VBOReadBuffer; Ready:=true;
end;
Теперь реализуем эту же задачу с использованием второго метода – glMapBuffer. Для этого немного перепишем нашу функцию VBOReadBuffer, заменив соответствующие инструкции, думаю данный код не нуждается в комментариях, так как использует все изученное нами ранее:
Листинг 29:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
procedure VBOReadMappedBuffer;
var BParam:TBuffParam;
    TempBuff:TVertexArray;
    Count, i:integer;
    p:pointer;
begin
  BParam := GetVBOParam (vId);
  if BParam.Access<>GL_READ_WRITE then begin
  showmessage('Unsupported buffer access type!');exit; end;
  Count:=BParam.Size div sizeof(TVertex);
  setlength(TempBuff,Count);
  glBindBuffer( GL_ARRAY_BUFFER, vId);
  p := glMapBuffer(GL_ARRAY_BUFFER, GL_READ_WRITE);
  for i:=0 to Count-1 do with TVertexArray(p)[i] do begin
    X:=X/2; Y:=Y/2; Z:=Z/2;
  end;
  glUnMapBuffer(GL_ARRAY_BUFFER);
  glBindBuffer( GL_ARRAY_BUFFER, 0);
end;
Аналогичным образом добавим на форму еще одну кнопку и добавим туда вызов нашей функции VBOReadMappedBuffer, запустим пример и убедимся что все работает именно так как нам нужно.
Результат работы данных функций можно увидеть на картинках ниже, соответствующие исходники тут: Demo7
Рис.1. Пример чтения и записи из/в VBO
Думаю как читать и писать в буферные объекты вы разобрались, теперь рассмотрим еще один вариант представления буферов VBOInterleaved arrays, о которых упоминалось в первой части статьи. Суть данной технологии заключается в объединении (склеивании) нескольких вершинных буферов в один. Идея данного подхода заключается в том, что используя glBufferSubData мы можем менять или задавать части нашего буфера. В результате, когда нам понадобится его отрисовать, мы биндим только один буфер, вместо того чтоб делать бинд для каждого из буферов (вершинного, цветов, нормалей, текстурных координат), после чего вызовом функций gl*Pointer указываем где искать наши данные в этом цельном массиве. Данныя технология позволяет немного повысить фпс, но снижает удобство работы с таким буфером. 
Структура такого «чередующегося» массива может быть двух видов, первый вариант:
X1,Y1,Z1,X2,Y2,Z2,…,Xn,Yn,Zn,Nx1,Ny1,Nz1,Nx2,Ny2,Nz2,…, Nxn,Nyn,Nzn, Cr1,Cg1,Cb1…..
Тоесть вначале идет целиком один буфер, к концу первого буфера приклеивается второй буфер и так далее.
Второй вариант, из-за чего эта технология и получила свое название, выглядит примерно так:
X,Y,Z,Nx,Ny,Nz,Cr,Cg,Cb,Ca,Ts,Tt, X,Y,Z,Nx,Ny,Nz,Cr,Cg,Cb,Ca,Ts,Tt
Тоесть данные в таком массиве представлены в виде чередующихся пакетов атрибут

В зависимости от того, какой из вариантов используется, отличаются и способы создания и обращения к таким буферам. Рассмотрим их. Первый вариант – буферы следуют один за другим:
Листинг 30:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
var BuffId: Cardinal;
    Count, VSize,CSize,NSize,TSize: integer;
begin
  Count := High(VertexBuffer)+1;
  VSize := sizeof(single)*3*Count;
  Csize := sizeof(byte)*3*count;
  NSize := sizeof(single)*3*count;
  TSize := sizeof(single)*2*count;
  glGenBuffers( 1, @BuffId );
  glBindBuffer(GL_ARRAY_BUFFER, BuffId );
  glBufferData(GL_ARRAY_BUFFER, VSize+CSize+NSize+TSize, nil, GL_STATIC_DRAW);
  glBufferSubData(GL_ARRAY_BUFFER, 0, VSize, @VertexBuffer[0]);
  glBufferSubData(GL_ARRAY_BUFFER, VSize, CSize, @ColorBuffer[0]);
  glBufferSubData(GL_ARRAY_BUFFER, VSize+CSize, NSize, @NormalsBuff[0]);
  glBufferSubData(GL_ARRAY_BUFFER, VSize+CSize+NSize, TSize, @TexCoordsBuff[0]);
  glBindBuffer(GL_ARRAY_BUFFER,0);
  glGenBuffers( 1, @iiId );
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iiId1);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, high(Indices)+1, @Indices[0], GL_STATIC_DRAW);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
end;
Как видите, идея очень простая – выделяем в видеопамяти блок равный суммарному размеру всех буферов, после чего через glBufferSubData по очереди вставляем туда наши буферы вершинных атрибутов, каждый раз задавая новое смещение от начала, равное суммарному размеру всех предыдущих блоков. Рендеринг такого буфера будет происходить так:
Листинг 31:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var Count, VSize,CSize,NSize: integer;
begin
  Count := High(VertexBuffer)+1;
  VSize := sizeof(single)*3*Count;
  Csize := sizeof(byte)*3*count;
  NSize := sizeof(single)*3*count;
  glEnableClientState( GL_VERTEX_ARRAY );
  glEnableClientState( GL_NORMAL_ARRAY );
  glEnableClientState( GL_TEXTURE_COORD_ARRAY );
  glEnableClientState( GL_COLOR_ARRAY );
  glBindBuffer( GL_ARRAY_BUFFER, iBuffId );
  glNormalPointer(GL_FLOAT, 0, PGLUINT(VSize+CSize) );
  glTexCoordPointer(2, GL_FLOAT, 0, PGLUINT(VSize+CSize+NSize) );
  glColorPointer(3, GL_UNSIGNED_BYTE, 0, PGLUINT(VSize) );
  glVertexPointer(3, GL_FLOAT, 0, PGLUINT(0) );
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iiId);
  glDrawElements(GL_TRIANGLES, high(Indices)+1, GL_UNSIGNED_BYTE, nil);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
  glDisableClientState(GL_VERTEX_ARRAY);
  glDisableClientState(GL_COLOR_ARRAY);
  glDisableClientState(GL_NORMAL_ARRAY);
  glDisableClientState(GL_TEXTURE_COORD_ARRAY);
end;
Основное отличие от рендеринга описанного ранее заключается в использовании одного бинда буфера вместо 4-х (glBindBuffer( GL_ARRAY_BUFFER, iBuffId )) и указание для каждого из gl*Pointer смещение от начала буфера.

Теперь рассмотрим второй вариант чередующихся массивов – вариант с чередующимися пакетами данных. Для данного варианта подразумевается что все данные находятся в одном массиве примерно такого вида:
Листинг 32:
1
2
3
4
5
6
7
8
Type 
  TInterleaved = packed record
    Vertex: TVertex;
    Normal: TVertex;
    Color: TRGBTriple;
    TexCoord: TTexCoord2;
  end;
var InterleavedArray: array of TInterleaved;

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

Листинг 33:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var BuffId: Cardinal;
    Count, Size: integer;
begin
  Count := High(InterleavedArray)+1;
  Size := sizeof(TInterleaved)*Count;
  glGenBuffers( 1, @BuffId );
  glBindBuffer(GL_ARRAY_BUFFER, BuffId );
  glBufferData(GL_ARRAY_BUFFER, Size, @InterleavedArray[0], GL_STATIC_DRAW);
  glBindBuffer(GL_ARRAY_BUFFER,0);
  glGenBuffers( 1, @iiId );
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iiId1);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, high(Indices)+1, @Indices[0], GL_STATIC_DRAW);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
end;
Ну а для рендеринга нам придется вновь воспользоваться калькулятором, так как нам потребуется вычислить размер и соответственно смещение для каждого из элементов TInterleaved. Это конечно все можно было бы сделать и в самой процедуре рендеринга, но чтоб лучше понять как это считается я приведу здесь табличку:
Элемент структуры
Размер элемента
Смещение от начала записи
Vertex
3 компоненты по 4 байта = 12 байт
0 байт
Normal
3 компоненты по 4 байта = 12 байт
12 байт
Color
3 компоненты по 1 байту = 3 байта
24 байт
TexCoord
2 компоненты по 4 байта = 8 байт
27 байт
Суммарный размер структуры
35 байт
Теперь, основываясь на данных этой таблицы, напишем процедуру рендеринг для чередующегося массива второго типа:
Листинг 34:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
begin
  glEnableClientState( GL_VERTEX_ARRAY );
  glEnableClientState( GL_NORMAL_ARRAY );
  glEnableClientState( GL_TEXTURE_COORD_ARRAY );
  glEnableClientState( GL_COLOR_ARRAY );
  glBindBuffer( GL_ARRAY_BUFFER, iBuffId );
  glNormalPointer(GL_FLOAT, 35, PGLUINT(12) );
  glTexCoordPointer(2, GL_FLOAT, 35, PGLUINT(27) );
  glColorPointer(3, GL_UNSIGNED_BYTE, 35, PGLUINT(24) );
  glVertexPointer(3, GL_FLOAT, 35, PGLUINT(0) );
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iiId);
  glDrawElements(GL_TRIANGLES, high(Indices)+1, GL_UNSIGNED_BYTE, nil);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glDisableClientState(GL_VERTEX_ARRAY);
  glDisableClientState(GL_COLOR_ARRAY);
  glDisableClientState(GL_NORMAL_ARRAY);
  glDisableClientState(GL_TEXTURE_COORD_ARRAY);
end;
В каждой функции gl*Pointer мы третьим параметром указываем размер всей структуры, в нашем случае 35 байт, что говорит OpenGL что следующий атрибут данного типа нужно брать из этого же массива но через 35 байт. В последний параметр мы передаем смещение данного элемента от начала структуры, берется оно из последней колонки нашей таблички.
Соответствующий код можно посмотреть тут Demo8.

Кроме явного задания чередующегося массива эту же технологию можно использовать для пропуска некоторых данных, к примеру у нас создается плоскость, у которой координата Z = 0, тоесть по сути она не используется, но она участвует в расчетах трансформаций. С одной стороны – мы могли бы создать новую структуру данных содержащую только две компоненты – X,Y, но с другой стороны – мы можем воспользоваться приведенной выше методикой для передачи толко двух компонент из трех, для этого достаточно вызывать функцию:
glVertexPointer(2, GL_FLOAT, sizeof(TVertex), nil );
Тоесть мы указали что использоваться будет только 2 компоненты, но следующие данные нужно брать через sizeof(TVertex) байт, тоесть через полный размер структуры. Данный подход так же часто применяется при передаче текстурных координат, которых как я говорил может быть одна, две, три или даже 4. Вот для того, чтоб не делать разных 4 структуры (и соответственно 4 массива и 4 буфера) для хранения этих координат, достаточно создать одну структуру с 4-мя компонентами, а уже при рисовании, вызывая функцию glTexCoordPointer, передавать в качестве параметров количество используемых компонент и полный размер структуры.

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

Бинарники с исходными кодами описанных демок: Demo7, Demo8.
Так же немного о чередующихся массивах и их форматах можно почитать здесь: Vertex Specification Best Practices

На этом пожалуй я закончу этот цикл уроков по VBO. За кадром осталось рассмотрение еще нескольких техник, в частности использование пользовательских атрибутов, использование аппаратного инстансинга, работа с буферами PBO и UBO, а так же использование XBO (Transform feedback).
Все эти техники тесно связаны с другими понятиями, такими как FBO, шейдера, геометрические шейдера, OpenGL 3.x, потому рассмотрение их отдельно, в рамках статьи о VBO, будет неполноценным, но мы обязательно вернемся к этому в других статьях и уроках.

Комментариев нет:

Отправить комментарий