GlVertexAttribPointer и glVertexAttribFormat: Какая разница?

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

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

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

Ответ 1

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

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

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

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

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

glVertexAttribPointer предоставляет оба из них одновременно. GL_ARRAY_BUFFER буфера 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*Format предоставляют все параметры форматирования вершин для атрибута. Каждый из его параметров имеет то же значение, что и параметры из эквивалентного вызова glVertexAttrib*Pointer.

Все немного запутано с glBindVertexBuffer.

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

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

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

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

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

Смещение формата вершины определяет смещение от начала каждой вершины к данным конкретного атрибута. Если данные в буфере определены 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);
}

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