вторник, 8 января 2013 г.

VBO. Обобщенные атрибуты

В предыдущих уроках мы с вами рассмотрели работу со стандартными (встроенными) вершинными атрибутами, устанавливаемыми через gl*Pointer, где вместо "*" мог быть один из стандартных атрибутов - Vertex/Normal/Color/TexCoord и прочие. Однако, в OpenGL3.х+ этот тип атрибутов был объявлен устаревшим и удален, то же самое произошло и в OpenGL ES 2.0/3.0.
К счастью не все так плохо как кажется, во-первых, начиная с OpenGL 3.2 существует два профиля OpenGL - "ядро" и "профиль совместимости". В профиле совместимости вернули весь "устаревший" функционал, в том числе и стандартные вершинные атрибуты, так что, если вы не планируете переходить на "core" версию или переходить на мобильные платформы, то вам бояться нечего. Во-вторых, если вам все же придется в будущем осуществить такой переход, то основы можно заложить уже сегодня, благо обобщенные атрибуты, на которые был осуществлен переход в современных версиях OpenGL (3.x+/ES2.0+) существуют еще с OpenGL 1.3 и с тех пор практически не изменились. О них мы и поговорим в этой части.

За обобщенные/пользовательские атрибуты (в англоязычной литературе можно встретить термины "generic, custom, user-defined") отвечает расширение ARB_vertex_program, введенное в ядро OpenGL 1.3 еще в 2002 году. Последующие обновления лишь расширили возможности работы с такими атрибутами (расширения ARB_vertex_attrib_64bit и ARB_vertex_attrib_binding).

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

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

Рассмотрим простейший вершинный шейдер:
Листинг 1:
void main(void)
{ 
  gl_Position = ftransform();
}

Такой пример можно встретить в большинстве учебников по шейдерам, но когда мы так пишем, мы даже не задумываемся о том, что же происходит "за кадром", к примеру что делает функция "ftransform", давайте "распишем" ее:
Листинг 2:
void main(void)
{ 
  gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex;
}

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

Именно координаты вершины и интересовали нас в этом примере, давайте разберемся что же это за "gl_Vertex", а это не что иное как наш вершинный атрибут, в частности - координаты вершины. Откуда он взялся? В GLSL есть ряд встроенных атрибутов и юниформ. Что значит "встроенных" - API OpenGL само занимается их объявлением и передачей, собственно gl_Vertex как раз пример одного из встроенных атрибутов, а gl_ModelViewProjectionMatrix - пример встроенной юниформы. По факту, компилятор GLSL добавляет к нашему коду такой вот блок:
Листинг 3:
attribute vec4 gl_Color;
attribute vec4 gl_SecondaryColor;
attribute vec3 gl_Normal;
attribute vec4 gl_Vertex; //<========================
attribute vec4 gl_MultiTexCoord0;
attribute vec4 gl_MultiTexCoord1;
attribute vec4 gl_MultiTexCoord2;
. . .
attribute vec4 gl_FogCoord;

void main(void)
{ 
  gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex;
}

Вот об этих "attribute" у нас и пойдет речь в этой статье.
Как видно с примера, кроме "gl_Vertex" у нас там так же объявлен атрибут "gl_Color", "gl_Normal", "gl_MultiTexCoord0" и т.д. Каждый вершинный атрибут, создаваемый через glVertex или glVertexPointer автоматически попадает в gl_Vertex, каждый раз когда мы пишем glColor или glColorPointer - мы помещаем атрибут в "gl_Color", каждый glNormal/glNormalPointer заполняет атрибут gl_Normal и т.д. Мы об этом даже не задумываемся, так как все за нас делает API OpenGL.

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

varying vec2 TexCoord;
varying vec3 T,B,N;
varying vec4 pos;

attribute vec3 Tangent;
attribute vec3 Binormal;

uniform mat4 ModelView;
uniform mat4 Proj;

void main ()
{
  TexCoord = gl_MultiTexCoord0.xy;
  T = (ModelView*vec4(Tangent,0.0)).xyz;
  B = (ModelView*vec4(Binormal,0.0)).xyz;
  N = (ModelView*vec4(gl_Normal,0.0)).xyz;
  pos = ModelView*gl_Vertex;

  gl_Position = Proj*ModelView*gl_Vertex;
}

В этом примере вершинного шейдера для нормалмаппинга, тангента (Tangent) и бинормаль (Binormal) передаются в виде пользовательских атрибутов, в то время как нормаль (gl_Normal), позиция вершины (gl_Vertex) и ее текстурные координаты (gl_MultiTexCoord0) все так же передается через встроенные вершинные атрибуты.

Просматривая многие шейдера, можно найти пример использования для этих целей и стандартных атрибутов, тоесть вместо объявления пользовательских атрибутов:
attribute vec3 Tangent;
attribute vec3 Binormal;
используют встроенные атрибуты, часто в их роли выступают текстурные координаты:
vec3 Tangent  = gl_MultiTexCoord1.xyz;
vec3 Binormal = gl_MultiTexCoord2.xyz;
Так тоже можно (по крайней мере в старых версиях OpenGL), но это статья не о том, как не использовать обобщенные  атрибуты, а как раз об обратном :)

Как видите, на стороне шейдера нет ничего сложно в объявление пользовательских атрибутов, а их использование ничем не отличается от использования стандартных вершинных атрибутов. В "core" профиле GLSL, начиная с версии 130 и по 430 (последнюю на момент написания статьи) расширялись возможности объявления пользовательских атрибут, но об этом мы поговорим ниже, а пока перейдем к рассмотрению работы на стороне приложения.

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

glGenBuffers(1, @BufferId);
glBindBuffer(GL_ARRAY_BUFFER, BufferId);
glBufferData(GL_ARRAY_BUFFER, BuffSize, Data, Usage);

Отличия начинаются уже в момент активации этого буфера. Первое что необходимо сделать, прежде чем начать использование пользовательских атрибутов - узнать где они находятся в шейдере. Делается это по аналогии с получением адреса юниформы, посредством функции:
Location:=glGetAttribLocation(ShaderProgramId, AttributeName)
ShaderProgramId - идентификатор слинкованной шейдерной программы, AttributeName - указатель на имя атрибута, в примере "Листинг 4" в качестве имен атрибутов будет использоваться "Tangent" и "Binormal". Эта процедура может быть выполнена еще на этапе создания буфера, главное чтоб в этот момент уже существовал шейдер. При смене шейдера придется заново получать адреса атрибутов из нового шейдера.
Забегая наперед, если активация буфера будет помещена в объект вершинного массива (VAO), что обязательно для OpenGL ES 2.0+ и "core profile" OpenGL 3.x+, а адреса атрибутов изменятся, то придется пересобрать VAO.

Следующим этапом у нас рисование. Здесь возможны два варианта, первый вариант, как в примерах  из первых глав этой статьи - мы явно биндим все атрибуты перед началом рисования (работает до OpenGL 3.0 и в профиле совместимости). Второй вариант - мы можем поместить бинд всех атрибутов, в том числе и пользовательских, в объект вершинного массива (VAO). Как уже было сказано в ремарке выше - это обязательное действие для профиля ядра и OpenGL ES 2.0+.  В любом случае это происходит уже после того как мы получили адрес нужного атрибута (Location).

В обоих случаях процесс абсолютно идентичен, первым делом мы должны забиндить наш буфер "glBindBuffer(GL_ARRAY_BUFFER, BufferId)", после чего, по аналогии со стандартными вершинными атрибутами, нужно разрешить использовать нужный атрибут, для этого существует функция:
glEnableVertexAttribArray(Location)

Вызов этой функции эквивалентен вызову "glEnableClientState" для стандартных атрибутов, с той разницей, что мы явно указываем атрибут по какому номеру (в какой локации) будет активен. После чего, опять же по аналогии со стандартными вершинными атрибутами, OpenGL нужно указать формат этого атрибута (размер, смещение, повторяемость), для этого существует функция:
glVertexAttribPointer(Location, Size, Type, Normalized, Stride, Offset);
Ее формат полностью аналогичен уже знакомой нам glVertexPointer, с тем отличием что первым параметром этой функции идет адрес атрибута и существует перегруженная функция, в которой можно указать параметр "Normalized", отвечающий за нормирование значений атрибутов к 1 (то же самое, что и при вызове glColorPointer).

Вот собственно и весь процесс создания, инициализации и активации пользовательских атрибутов. В папке Demo1 вы найдете работающий пример со всеми исходными кодами. Ссылка на архив со всеми примерами находится в конце статьи.


С выходом OpenGL 3.0 и GLSL 1.30 произошли небольшие изменения, в частности вместо ключевого слова attribute теперь применяется ключевое слово in, а вместо varying ключевое слово out. Чтоб использовать такой вариант записи необходимо явно указать версию GLSL, прописав в заголовке шейдера "#version 130". Никаких дополнительных изменений в коде приложения или в шейдере не требуется. Вот пример модифицированной версии шейдера:
Листинг 5:

#version 130

out vec2 TexCoord;
out vec3 T,B,N;
out vec4 pos;

in vec3 Tangent;
in vec3 Binormal;

uniform mat4 ModelView;
uniform mat4 Proj;

void main ()
{
  TexCoord = gl_MultiTexCoord0.xy;
  T = (ModelView*vec4(Tangent,0.0)).xyz;
  B = (ModelView*vec4(Binormal,0.0)).xyz;
  N = (ModelView*vec4(gl_Normal,0.0)).xyz;
  pos = ModelView*gl_Vertex;

  gl_Position = Proj*ModelView*gl_Vertex;
}
Полный пример программы вы найдете в папке Demo2.

Однако стоит помнить что приведенные код использует профиль совместимости, что позволяет комбинировать встроенные вершинные атрибуты и обобщенные вершинные атрибуты в одном шейдере не смотря на явное указание версии шейдера. В более поздних версиях OpenGL, а так же в OpenGL ES 2.0/3.0 такой фокус не пройдет и вам придется объявлять все атрибуты, включая gl_Vertex, gl_Normal и gl_MultiTexCoord0 как обобщенные.

В реальной обстановке у нас может быть много однотипных шейдеров, но с разным набором вершинных атрибутов. Перед использованием, для каждого из атрибутов, мы должны были бы получить его индекс в шейдере. Так как набор атрибутов может быть разный - могут добавиться новые, или исчезнуть в следствии оптимизации шейдера компилятором. К примеру мы выводили объект со скелетной анимацией, а теперь хотим вывести его же, но как статическую статую, или хотим вместо бампмапинга использовать освещение по Гуро, то простая замена шейдера объекта приведет к необходимости перекомпиляции VAO с указанием нового положения индексов, о чем уже писалось выше. К счастью разработчики OpenGL пошли нам на встречу, позволив явно указать местоположение атрибута в шейдере. Для этих целей служит команда:

glBindAttribLocation(ShaderProgramId,  AttribIndex,  AttribName);

Вызывать эту команду нужно после создания шейдера но до его линковки:
ShaderProgramId := glCreateProgram;
glAttachShader(ShaderProgramId, some_shader);
glBindAttribLocation(ShaderProgramId, AttribIndex, AttribName);
glLinkProgram(ShaderProgramId);

Чуть позже появилось еще одно расширение - ARB_explicit_attrib_location, данное расширение позволило так же явно задавать положение атрибута, но уже прямо в тексте шейдера, тем самым сделав "не нужными" вызовы glGetAttribLocation и glBindAttribLocation, ненужным в том плане, что мы заранее можем сказать где будет находиться тот или иной атрибут, что очень удобно при создании библиотеки шейдеров. Не смотря на то, что это расширение было введено в OpenGL 4.1, сейчас, с последними драйверами видеокарт, его можно использовать и в OpenGL 3.3.  Использовать данное нововведение очень просто, фактически в шейдере, перед именем атрибута, нужно указать квалификатор:
layout(location = <index>)
где index это номер, за каким будет закреплен атрибут, аналогично индексу в glBindAttribLocation.

Вот модифицированный пример кода из "Листинг 5":
Листинг 6:

#version 330

out vec2 TexCoord;
out vec3 T,B,N;
out vec4 pos;


layout(location = 0) in vec3 in_Tangent;
layout(location = 1) in vec3 in_Binormal;
layout(location = 2) in vec3 in_Normal;
layout(location = 3) in vec3 in_Position;
layout(location = 4) in vec2 in_TexCoord;


uniform mat4 ModelView;
uniform mat4 Proj;

void main ()
{
  TexCoord = in_TexCoord.xy;
  T = (ModelView*vec4(in_Tangent,0.0)).xyz;
  B = (ModelView*vec4(in_Binormal,0.0)).xyz;
  N = (ModelView*vec4(in_Normal,0.0)).xyz;
  pos = ModelView*vec4(in_Position,1.0);

  gl_Position = Proj*pos;
}

Как видите, изменений минимум, главное не забыть поднять версию шейдера до "#version 330", но при этом, без явного указания использования профиля совместимости, мы теперь не можем использовать встроенные вершинные атрибуты, потому пришлось кроме тангенты и бинормали, как в предыдущих примерах, объявлять атрибутами так же нормаль, координаты вершин и текстурные координаты. Теперь в коде вместо получения индексов атрибутов по имени можно явно указывать их номер 0-4 соответственно, не боясь что в следствии добавления других атрибутов, или оптимизации компилятором неиспользуемых атрибутов, их индексация изменится.

Код генерации VAO с этим списком атрибутов приведен ниже:
Листинг 7:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
procedure CreateVAO(var aVAO: TVAO);
var attrLoc: integer;
begin
  glGenVertexArrays(1, @aVAO.VAO);
  glBindVertexArray(aVAO.VAO);

  attrLoc:=0; //Явно указываем индекс атрибута in_Tangent
    glBindBuffer(GL_ARRAY_BUFFER, aVAO.tId);
    glEnableVertexAttribArray(attrLoc);
    glVertexAttribPointer(attrLoc, 3, GL_FLOAT, false, 0, nil);

  attrLoc:=1; //Явно указываем индекс атрибута in_Binormal
    glBindBuffer(GL_ARRAY_BUFFER, aVAO.bId);
    glEnableVertexAttribArray(attrLoc);
    glVertexAttribPointer(attrLoc, 3, GL_FLOAT, false, 0, nil);

  attrLoc:=2; //Явно указываем индекс атрибута in_Normal
    glBindBuffer(GL_ARRAY_BUFFER, aVAO.nId);
    glEnableVertexAttribArray(attrLoc);
    glVertexAttribPointer(attrLoc, 3, GL_FLOAT, false, 0, nil);

  attrLoc:=3; //Явно указываем индекс атрибута in_Position
    glBindBuffer(GL_ARRAY_BUFFER, aVAO.vId);
    glEnableVertexAttribArray(attrLoc);
    glVertexAttribPointer(attrLoc, 3, GL_FLOAT, false, 0, nil);

  attrLoc:=4; //Явно указываем индекс атрибута in_TexCoord
    glBindBuffer(GL_ARRAY_BUFFER, aVAO.tcId);
    glEnableVertexAttribArray(attrLoc);
    glVertexAttribPointer(attrLoc, 2, GL_FLOAT, false, 0, nil);

  glBindVertexArray(0);
  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
end;
Полный код примера вы найдет в папке Demo3.


На этом история не заканчивается, в 5-й части уроков по VBO, я рассказывал о существовании способа хранения атрибутов в виде чередующихся пакетов данных. В OpenGL 4.3 появилось расширение ARB_vertex_attrib_binding, упрощающее работу с таким типом данных и предоставляющее некоторые дополнительные возможности. Работу с таким типампом атрибутов можно разделить на две части, так как теперь атрибут должен иметь с одной стороны всю информацию о структуре буфера VBO - смещение в буфере до начала данных вершинного атрибута в этом буфере и смещение до следующего пакета данных в буфере (размер пакета), а с другой стороны - нам нужно описать свойства самого атрибута - индекс обобщенного атрибута, количество компонент и формат каждого из атрибутов, а так же базовое смещение от начала буфера. Для этих целей были введены следующие команды:


VertexAttribFormat(uint attribindex, int size, enum type, 
                            boolean normalized, uint relativeoffset);
Эта команда описывает формат атрибута - индекс атрибута в шейдере, количество компонент, их тип, нужно ли нормировать значение и смещение от начала пакета. Тоесть все то же что и в уже исзвестной команде glVertexAttribPointer. Единственное отличие - здесь не указывается размер пакета (stride).

Вторая команда - описывает формат буфера VBO:

BindVertexBuffer(uint bindingindex, uint buffer, intptr offset, sizei stride);
Эта команда прикрепляет созданный буфер VBO к указанному bindingindex, а так же задает смещение от начала буфера и щаг до следующего пакета данных. Именно здесь мы и указываем последнюю часть нашего атрибута - шаг до следующего пакета данных, причем этот шаг теперь не может быть равен нулю, так как OpenGL теперь ничего не знает о данных, находящихся в нашем буфере. Так же это объясняет название параметра relativeoffset у VertexAttribFormat, так как смещение при описании формата атрибута теперь задается относительно смещения буфера offset.

Теперь нужно сделать самое главное - связать формат обобщенного атрибута с конкретным буфером VBO, делается это командой:

VertexAttribBinding(uint attribindex, uint bindingindex);
Где первым параметром указывается индекс обобщенного атрибута (attribindex), а вторым - индекс к которому был прикреплен наш буфер VBO (bindingindex).

Здесь стоит уточнить несколько важных моментов:
  • Формат атрибута и формат связанного с атрибутом буфера VBO задаются раздельно;
  • Следствие из предыдущего пункта - при создании буфера VAO не обязательно выполнять BindVertexBuffer, его можно задавать непосредственно пред рендерингом;
  • Единожды установив формат атрибута мы можем использовать его совместно с любым буфером VBO, в том числе помещать информацию о нескольких вершинных объектов в один буфер;
  • При формировании атрибутов можно одновременно использовать несколько буферов VBO, их количество определяется параметром GL_MAX_VERTEX_ATTRIB_BINDINGS.
Чтоб было понятно что изменилось - рассмотрим тот же пример нормалмаппинга, с теми  же вершинными атрибутами и теми же шейдерами. Если раньше каждый атрибут хранился в своем буфере VBO, то теперь, для предоставления информации о вершине, используется такая структура:

  TVertex = packed record
    T,        //Tangent;  Offset = 0
    B,        //Binormal; Offset = sizeof(vec3)
    N,        //Normal;   Offset = sizeof(vec3)*2
    P: vec3;  //Position; Offset = sizeof(vec3)*3
    TC: vec2; //TexCoord; Offset = sizeof(vec3)*4
  end;
Генерация VAO по этим данным имеет такой вот вид:
Листинг 8:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
procedure CreateVAO(var aVAO: TVAO);
var attrLoc, BindingIndex: integer;
begin
  BindingIndex := 0; //у нас все атрибуты в одном буфере

  glGenVertexArrays(1, @aVAO.VAO);
  glBindVertexArray(aVAO.VAO);
  //Биндим буфер VBO и задаем для него смещение и шаг
  //Эту операцию можно выполнять непосредственно перед рисованием
  glBindVertexBuffer(BindingIndex, VAO.abId, 0, sizeof(TVertex));

  //Задаем формат атрибутов:
  // T, B, N, P: vec3; TC: vec2;
  attrLoc:=0; //Тангента
  glVertexAttribFormat(attrLoc, 3, GL_FLOAT, false, 0 );
  glVertexAttribBinding(attrLoc, BindingIndex);
  glEnableVertexAttribArray(attrLoc);
  attrLoc:=1; //Бинормаль
  glVertexAttribFormat(attrLoc, 3, GL_FLOAT, false, sizeof(vec3));
  glVertexAttribBinding(attrLoc, BindingIndex);
  glEnableVertexAttribArray(attrLoc);
  attrLoc:=2; //Нормаль
  glVertexAttribFormat(attrLoc, 3, GL_FLOAT, false, sizeof(vec3)*2);
  glVertexAttribBinding(attrLoc, BindingIndex);
  glEnableVertexAttribArray(attrLoc);
  attrLoc:=3; //Позиция
  glVertexAttribFormat(attrLoc, 3, GL_FLOAT, false, sizeof(vec3)*3);
  glVertexAttribBinding(attrLoc, BindingIndex);
  glEnableVertexAttribArray(attrLoc);
  attrLoc:=4; //Текстурные координаты
  glVertexAttribFormat(attrLoc, 2, GL_FLOAT, false, sizeof(vec3)*4);
  glVertexAttribBinding(attrLoc, BindingIndex);
  glEnableVertexAttribArray(attrLoc);

  glBindVertexArray(0);
end;


Начало кода вам уже должно быть знакомо, в строках 6-7 мы создаем VAO и делаем его активным. Дальше уже начинаются отличия - в 10 строке мы прикрепляем созданный ранее буфер VBO к BindingIndex и указываем что размер пакета будет равен размеру TVertex, а смещение от начала буфера будет равно 0. В нашем примере используется только один буфер, потому BindingIndex так же равен нулю. Этот биндинг будет сохранен в VAO, потому отпадает необходимость повторного биндинга этого буфера непосредственно перед рисованием.

Дальше, в строках 14-17, 18-21, 22-25, 26-29 и 30-33, мы задаем формат каждого из атрибутов, прикрепляя его к указанному BindingIndex и индексу обобщенного атрибута в шедере. Так же не забываем разрешить использование этого атрибута командой glEnableVertexAttribArray.
Ну и 35-й строкой завершаем создание VAO.

Ну и теперь непосредственно рисование этого буфера.
Листинг 9:

1
2
3
4
5
6
7
8
  //биндим буферы VAO и рендерим квад
  glBindVertexArray(vao.VAO);
  //  Здесь мы могли бы явно указать из какого буфера брать атрибуты
  //  glBindVertexBuffer(0, VAO.abId, 0, sizeof(TVertex));
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vao.iId);
  glDrawElements(GL_QUADS,4,GL_UNSIGNED_INT, nil);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
  glBindVertexArray(0);

Как видите, это вполне обычный код - активация VAO, активация индексного буфера, рисование и деактивация индексного буфера и VAO. Как вы уже наверное заметили из коментариев в коде - если бы мы не указали биндинг буфера в предыдущей процедуре (Листинг 8), то его можно было бы указать прямо здесь.

Вот собственно и весь процесс работы с расширением ARB_vertex_attrib_binding и форматом атрибут. Весь приведенный выше код вы можете найти в папке Demo4, однако следует помнить чтодля запуска примера вам потребуется видеокарта и драйвера с поддержкой OpenGL 4.3, на момент написания статьи таковые имелись только у NVidia.


Сам пакет демок можно скачать здесь: Generic Attributes Demos.

2 комментария: