glVertexAttribPointer и glVertexAttribFormat: в чем разница?

В OpenGL 4.3 и OpenGL ES 3.1 добавлено несколько альтернативных функций для задания массивов вершин: glVertexAttribFormat, glBindVertexBuffers и т. д. Но у нас уже были функции для указания массивов вершин. А именно glVertexAttribPointer.

  1. Зачем добавлять новые API, которые делают то же самое, что и старые?

  2. Как работают новые API?


person Nicol Bolas    schedule 22.06.2016    source источник
comment
Связанные вопросы, хотя они больше касаются glBindVertexBuffer(), чем glVertexAttribFormat(): index-or-only-in" title="do opengl объекты массива вершин хранят имена и индексы вершинного буфера или только в"> stackoverflow.com/questions/26767939/, stackoverflow.com/questions /29220416/.   -  person Reto Koradi    schedule 23.06.2016


Ответы (1)


glVertexAttribPointer имеет два недостатка, один из них полусубъективный, другой объективный.

Первый недостаток — его зависимость от GL_ARRAY_BUFFER. Это означает, что поведение glVertexAttribPointer зависит от того, что было связано с GL_ARRAY_BUFFER во время его вызова. Но как только он вызывается, то, что связано с GL_ARRAY_BUFFER, больше не имеет значения; ссылка на буферный объект копируется в VAO. Все это очень неинтуитивно и сбивает с толку даже некоторых неопытных пользователей.

Это также требует, чтобы вы предоставили смещение в объекте буфера как «указатель», а не как смещение целого байта. Это означает, что вы выполняете неудобное приведение от целого числа к указателю (которое должно сопровождаться таким же неудобным приведением в драйвере).

Второй недостаток заключается в том, что он объединяет две операции, которые логически совершенно независимы. Чтобы определить массив вершин, который может читать OpenGL, вы должны предоставить две вещи:

  • Как получить данные из памяти.
  • Как выглядят эти данные.

glVertexAttribPointer обеспечивает оба этих одновременно. Объект буфера GL_ARRAY_BUFFER, а также указатель смещения и шаг определяют, где хранятся данные и как их получить. Другие параметры описывают, как выглядит отдельная единица данных. Назовем это форматом вершин массива.

С практической точки зрения, пользователи гораздо чаще меняют источник данных вершин, чем форматы вершин. Ведь многие объекты в сцене точно так же хранят свои вершины. Как бы то ни было: 3 числа с плавающей запятой для позиции, 4 беззнаковых байта для цветов, 2 беззнаковых шорта для текс-коорд и т. д. Вообще говоря, у вас есть только несколько форматов вершин.

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

С glVertexAttribPointer вы не можете обновить только смещение. Вы должны указать всю информацию о формате + буфере сразу. Каждый раз.

VAO избавляют от необходимости делать все эти вызовы для каждого объекта, но оказывается, что на самом деле они не решают проблему. О, конечно, вам не обязательно звонить glVertexAttribPointer. Но это не меняет того факта, что изменение форматов вершин дорого.

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

glVertexAttribFormat и glBindVertexBuffer решают обе эти проблемы. glBindVertexBuffer напрямую указывает объект буфера и принимает смещение в байтах как фактическое (64-битное) целое число. Таким образом, привязка GL_ARRAY_BUFFER не вызывает затруднений; эта привязка используется исключительно для управления объектом буфера.

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

Обратите внимание, что это разделение формализовано в API прямого доступа к состоянию GL 4.5. То есть нет версии DSA glVertexAttribPointer; вы должны использовать glVertexArrayAttribFormat и другие API отдельных форматов.


Отдельные функции привязки атрибутов работают следующим образом. glVertexAttrib*Format предоставляет все параметры форматирования вершин для атрибута. Каждый из его параметров имеет то же значение, что и параметры эквивалентного вызова glVertexAttrib*Pointer.

Где все становится немного запутанным, так это с glBindVertexBuffer.

Его первый параметр — это индекс. Но это не расположение атрибута; это просто точка привязки буфера. Это отдельный массив из местоположений атрибутов с собственным максимальным ограничением. Таким образом, тот факт, что вы привязываете буфер к индексу 0, ничего не означает, откуда атрибут 0 получает данные.

Связь между привязками буфера и расположением атрибутов определяется glVertexAttribBinding. Первый параметр — это местоположение атрибута, а второй — индекс привязки буфера, с помощью которого извлекается местоположение этого атрибута. Так как имя функции начинается с "VertexAttrib", вы должны рассматривать его как часть состояния формата вершины и, следовательно, его изменение требует больших затрат.

Природа смещений также может поначалу немного сбивать с толку. glVertexAttribFormat имеет параметр смещения. Но то же самое делает и glBindVertexBuffer. Но эти смещения означают разные вещи. Самый простой способ понять разницу — использовать пример чередующейся структуры данных:

struct Vertex
{
    GLfloat pos[3];
    GLubyte color[4];
    GLushort texCoord[2];
};

Смещение привязки буфера вершин определяет смещение в байтах от начала объекта буфера до индекса первой вершины. То есть, когда вы визуализируете индекс 0, GPU будет извлекать память из адреса буферного объекта + смещения привязки.

Смещение формата вершины определяет смещение от начала каждой вершины до данных этого конкретного атрибута. Если данные в буфере определены как Vertex, то смещение для каждого атрибута будет следующим:

glVertexAttribFormat(0, ..., offsetof(Vertex, pos)); //AKA: 0
glVertexAttribFormat(1, ..., offsetof(Vertex, color)); //Probably 12
glVertexAttribFormat(2, ..., offsetof(Vertex, texCoord)); //Probably 16

Таким образом, смещение привязки определяет, где вершина 0 находится в памяти, а смещения формата определяют, откуда берутся данные каждого атрибута внутри вершины.

Последнее, что нужно понять, это то, что привязка буфера — это место, где определяется шаг. Это может показаться странным, но подумайте об этом с точки зрения аппаратного обеспечения.

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

Именно поэтому делитель экземпляра является частью состояния привязки буфера через glVertexBindingDivisor. Аппаратному обеспечению необходимо знать делитель, чтобы преобразовать индекс экземпляра в адрес памяти.

Конечно, это также означает, что вы больше не можете полагаться на OpenGL для вычисления шага за вас. В приведенном выше приведении вы просто используете sizeof(Vertex).

Отдельные форматы атрибутов полностью охватывают старую модель glVertexAttribPointer настолько хорошо, что старая функция теперь полностью определяется в терминах новой:

void glVertexAttrib*Pointer(GLuint index​, GLint size​, GLenum type​, {GLboolean normalized​,} GLsizei stride​, const GLvoid * pointer​)
{
  glVertexAttrib*Format(index, size, type, {normalized,} 0);
  glVertexAttribBinding(index, index);

  GLuint buffer;
  glGetIntegerv(GL_ARRAY_BUFFER_BINDING, buffer);
  if(buffer == 0)
    glErrorOut(GL_INVALID_OPERATION); //Give an error.

  if(stride == 0)
    stride = CalcStride(size, type);

  GLintptr offset = reinterpret_cast<GLintptr>(pointer);
  glBindVertexBuffer(index, buffer, offset, stride);
}

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

person Nicol Bolas    schedule 22.06.2016
comment
IIUC ваше объяснение, повторная реализация glVertexAttrib*Pointer имеет местами смещения. Приведенный указатель следует использовать с glVertexAttrib*Format, а 0 с glBindVertexBuffer. Или, может быть, мне нужно перечитать ваш ответ еще раз :) - person dvd; 04.01.2017
comment
@dvd: я скопировал это из спецификации ARB_vertex_attrib_binding. Смещение формата — это смещение от смещения привязки буфера для этого конкретного атрибута, и оно имеет фиксированный верхний предел. Смещение привязки буфера — это смещение от начала объекта буфера до позиции 0 для этой привязки. См. часть выше о чередовании и командах Format. - person Nicol Bolas; 04.01.2017
comment
Спасибо! Я перечитал ответ (в энный раз) и теперь он более понятен! - person dvd; 05.01.2017
comment
Очень интересное объяснение! - person Sébastien Bémelmans; 18.12.2020