Как повторно использовать вершины в примитивах в OpenGL

Я использую OpenGL на C++ (технически EGL на Jetson Nano).

Допустим, я хочу нарисовать N квадроциклов. Представьте себе просто список цветных прямоугольников. Таких прямоугольников в кадре может быть несколько тысяч.

Я хочу использовать два буфера вершин:

  1. Тот, который определяет геометрию каждого квадроцикла.
  2. Тот, который определяет свойства, общие для каждого квадроцикла.

Первый буфер вершин должен определять геометрию каждого четырехугольника. В нем должно быть только 4 вершины, и его данные будут просто углами четырехугольника. Что-то типа:

0, 0, // top left
1, 0, // top right
0, 1, // bottom left
1, 1, // bottom right

Тогда второй буфер вершин должен иметь только x, y, ширину, высоту всех прямоугольников.

x1, y1, width1, height1, color1,
x2, y2, width2, height2, color2,
x3, y3, width3, height3, color3,
x4, y4, width4, height4, color4,
x5, y5, width5, height5, color5,
x6, y6, width6, height6, color6,
... etc.

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

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

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

Как мне это настроить?

Что я делаю сейчас:

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

0, 0, // (1) top left
1, 0, // 
0, 1, // 
1, 1  // 
0, 0, // (2) top left
1, 0, // 
0, 1, // 
1, 1, // 
0, 0, // (3) top left
1, 0, // 
0, 1, // 
1, 1, // 
... etc

И мой второй буфер дублирует свои данные для каждой вершины:

x1, y1, width1, height1, color1,
x1, y1, width1, height1, color1,
x1, y1, width1, height1, color1,
x1, y1, width1, height1, color1,
x2, y2, width2, height2, color2,
x2, y2, width2, height2, color2,
x2, y2, width2, height2, color2,
x2, y2, width2, height2, color2,
x3, y3, width3, height3, color3,
x3, y3, width3, height3, color3,
x3, y3, width3, height3, color3,
x3, y3, width3, height3, color3,
x4, y4, width4, height4, color4,
x4, y4, width4, height4, color4,
x4, y4, width4, height4, color4,
x4, y4, width4, height4, color4,
x5, y5, width5, height5, color5,
x5, y5, width5, height5, color5,
x5, y5, width5, height5, color5,
x5, y5, width5, height5, color5,
x6, y6, width6, height6, color6,
x6, y6, width6, height6, color6,
x6, y6, width6, height6, color6,
x6, y6, width6, height6, color6,
... etc.

Это кажется действительно неэффективным, и я просто хочу указать первые 4 вершины один раз и заставить их каким-то образом повторно использовать их, а не дублировать эти 4 вершины N раз, чтобы иметь в общей сложности 4 * N вершин в моем первом буфере. . И я хочу указать атрибуты x,y,width,height,color только один раз для каждого четырехугольника, всего N вершин, и не один раз для каждой общей вершины, всего 4*N вершин. .

Что я делаю?


person Wyck    schedule 16.10.2019    source источник
comment
Какая проблема решается по сравнению с обычным способом?   -  person Nicol Bolas    schedule 16.10.2019
comment
@NicolBolas Как обычно? Проблема, которую я решаю, заключается в обновлении только N вершин за кадр, а не 4 * N вершин за кадр.   -  person Wyck    schedule 16.10.2019
comment
Да, обычным способом является обновление 4*N вершин за кадр. И я не имею в виду ваши x, y, w, h вещи; Я имею в виду стандартное положение + цвет для каждой вершины. Мой вопрос был в том, почему вы считаете, что это неправильно; какую проблему вы пытаетесь решить, делая это таким образом, а не ожидаемым способом?   -  person Nicol Bolas    schedule 16.10.2019
comment
Похоже, вы используете или хотите использовать создание экземпляров. В этом случае ваш первый буфер вершин только с четырьмя вершинами с нулевой или одной вершиной используется для атрибута вершины без экземпляра, а ваш второй буфер вершин используется для атрибутов вершины с экземпляром (через glVertexAttribDivisor), продвигаясь по экземпляру. Чтобы это работало, вам понадобится вершинный шейдер, который выполняет gl_Position = pos * widthAndHeight + xAndY, где pos — ваш неэкземплярный атрибут, подаваемый первым вершинным буфером, а xAndY, а также widthAndHeight — экземплярные атрибуты, подаваемые вторым вершинным буфером.   -  person Kai Burjack    schedule 16.10.2019
comment
@NicolBolas Если бы я хотел делать додекаэдры вместо прямоугольников, было бы это более уместно? ... если бы в фигуре, которую я рисовал вместо четырехугольника, было более 100 вершин? Будет ли тогда эта техника иметь смысл? Только потому, что четверной так прост, тебе трудно принять мою мотивацию для этого? Мне кажется очевидным, что лучше загружать только N вершин на кадр, чем N*M вершин на кадр. Мне просто нужно знать технику: как сообщить OpenGL, как настроены буферы.   -  person Wyck    schedule 16.10.2019
comment
@httpdigest, звучит полезно. Действительно ли экземпляры поступают из второго буфера? У меня сложилось впечатление, что данные экземпляра по существу передаются в виде массива через униформу.   -  person Wyck    schedule 16.10.2019
comment
@Wyck: Если бы я хотел сделать додекаэдры вместо прямоугольников, было бы это более уместно? ...если бы в фигуре, которую я рисовал, было более 100 вершин вместо четырехугольника? Да, это было бы более актуально, потому что вы бы рассматривали проблему производительности: как нарисовать (предположительно ) большое количество средних сеток. Но даже в этом случае это будет зависеть от проблемы с производительностью. То есть ответ на вопрос, который я задаю, будет заключаться в том, что я профилировал обычный код и обнаружил, что он медленный.   -  person Nicol Bolas    schedule 16.10.2019
comment
@Wyck: Потому что, если вы не знаете, что это реальная проблема с производительностью, то используемое вами решение вполне может сделать ваш код медленнее.   -  person Nicol Bolas    schedule 16.10.2019
comment
@NicolBolas Моя проблема в том, что я хочу сравнить технику 1 и технику 2, и я не знаю, как создать реализацию техники 2, чтобы даже протестировать мою реализацию техники 1.   -  person Wyck    schedule 16.10.2019
comment
@Wyck: Проблема в том, что ваша техника 1, если это то, что вы описали, является неправильной техникой. Сравнивать с ней вашу гипотетическую технику 2 было бы неправильно, потому что вы не начинаете с лучшей версии вашего текущего механизма. То есть техника 2 будет быстрее только потому, что техника 1 плоха, а не потому, что техника 2 хороша.   -  person Nicol Bolas    schedule 16.10.2019
comment
@NicolBolas, какая правильная техника?   -  person Wyck    schedule 16.10.2019
comment
@Wyck: Именно то, что я сказал: я не имею в виду ваши x, y, w, h вещи; Я имею в виду стандартное положение + цвет для каждой вершины. Не пытайтесь отправлять данные для каждого экземпляра как отдельный канал; просто укажите положение и цвет для четырех четырехугольных вершин.   -  person Nicol Bolas    schedule 16.10.2019
comment
Зачем мне просить ЦП вычислять данные для каждого экземпляра, если GPU может сделать всю эту математику за меня?   -  person Wyck    schedule 16.10.2019
comment
Давайте продолжим обсуждение в чате.   -  person Nicol Bolas    schedule 16.10.2019


Ответы (1)


Вообще говоря, самый эффективный способ рендеринга серии квадроциклов — это... рендеринг серии квадроциклов. Вы не отправляете ширину/высоту или другую информацию для каждого экземпляра; вы вычисляете фактическое положение 4 вершин на CPU и записываете их в память GPU, используя соответствующий буфер методы потоковой передачи объектов. В частности, не пытайтесь изменить только несколько четырехугольников; если ваши данные не являются статическими, вероятно, будет лучше повторно загрузить их все (в другой/недействительный буфер), а не изменять только несколько байтов на месте.

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

Вы можете смягчить эту проблему, уменьшив размер данных вершин. Поскольку мы говорим о 2D-четырехугольниках, вы вполне можете использовать шорты для положения XY каждой вершины. Или 16-битные числа с плавающей запятой. В любом случае это означает, что каждая вершина (позиция + цвет) занимает всего 8 байтов, а это означает, что четверка занимает всего 32 байта данных. Очевидно, что 12 байтов меньше, чем 32 (12 — это стоимость экземпляра, если вы используете аналогичное сжатие), но это все же сокращение на 33% по сравнению с 48 байтами, которые будут использовать полные float позиции.


Если вы выполнили домашнюю работу по профилированию и определили, что 32 байта на квадрат — это слишком много, создание экземпляров вершин по-прежнему является плохой идеей. Хорошо известно, что на некоторых аппаратных средствах очень маленькие экземпляры могут снизить производительность VS. Поэтому его следует избегать.

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

Входное значение gl_VertexID говорит вам, какой индекс вершины визуализируется. Учитывая, что вы визуализируете квадроциклы, текущий экземпляр будет gl_VertexID / 4. И текущая вершина в квадроцикле — gl_VertexID % 4. Таким образом, ваш VS будет выглядеть примерно так:

struct instance
{
  vec2 position;
  vec2 size;
  uint color; //Packed as 4 bytes; unpack with unpackUnorm4x8
  uint padding; //Padding needed due to alignment/stride of 8 bytes.
};

layout(binding = 0, std430) buffer instance_data
{
  instance instances[];
};

vec2[4] vertex_table =
{
  vec2{0, 0},
  vec2{1, 0},
  vec2{0, 1},
  vec2{1, 1},
};

void main()
{
    instance curr_instance = instances[gl_VertexID / 4];
    vec2 vertex = vertex_table[gl_VertexID % 4];

    vertex = curr_instance.position + (curr_instance.size * vertex);
    gl_Position = vec4(vertex.xy, 0.0, 1.0);
}

Насколько быстрыми будут такие вещи, полностью зависит от того, насколько хорошо ваш графический процессор обрабатывает такие виды чтения глобальной памяти. Обратите внимание, что по крайней мере гипотетически возможно уменьшить размер данных для каждого экземпляра обратно до 12. Вы можете упаковать позицию и размер в два 16-битных шорта или полуплава, используя unpackUnorm2x16 или unpackHalf2x16 для распаковки этих значений соответственно. . Если вы сделаете это, то ваша структура instance будет состоять всего из 3 значений uint, и нет необходимости в дополнении.

person Nicol Bolas    schedule 16.10.2019