Войти
ПрограммированиеФорумГрафика

Uniform buffer'ы и рендеринг нескольких объектов (OpenGL)

#0
12:57, 10 сен. 2019

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

В шейдере:

// Uniform переменная с матрицей для каждого меша
uniform mat4 model;

В C++ коде:

// Передать матрицу модели в шейдер (запись в uniform-буфер)
auto model = mesh.getModelMatrix();
glUniformMatrix4fv(glGetUniformLocation(shaderId, "model"), 1, GL_FALSE, glm::value_ptr(model));

// Привязать геометрию и нарисовать ее
glBindVertexArray(mesh.geometry->getVaoId());
glDrawElements(GL_TRIANGLES, mesh.geometry->getIndexCount(), GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);

Все работало хорошо. И вот, я решил заменить обычную uniform переменную буфером.

В шейдере я сделал следующее:

layout (std140, binding = 0) uniform matrices
{ 
    mat4 model;
    mat4 projection;
    mat4 model; 
};

Далее, в коде, я в начале создал сам UBO, выделил память и привязал буфер к binding point'у под номером 0 (который явно задан в шейдере)

glGenBuffers(1, &_uboMatrices);
glBindBuffer(GL_UNIFORM_BUFFER, _uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, sizeof(glm::mat4) * 3, nullptr, GL_STREAM_DRAW);
glBindBufferBase(GL_UNIFORM_BUFFER, 0, _uboMatrices);
glBindBuffer(GL_UNIFORM_BUFFER, 0);

Затем, в теле цикла, который проходится по всем мешам, во время отрисовки очередного меша я просто обновляю часть этого буфера, для передачи матрицы модели.

auto model = mesh.getModelMatrix();
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(model));

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

И тут я переключился на встроенную графику ноутбука...

После этого на экране все начало дергаться, если рисовалось более одного объекта.

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

В начале я попытался найти способ заставить OpenGL ждать, пока каждый из объектов не будет отрисован. Я попробовал сделать это при помощи glFinish после каждого вызова glDrawElements, но это решение полностью убило производительность. Подумав немного, я пришел к выводу, что буфер матриц модели должен из себя представлять именно массив с матрицами под каждый объект, и тогда подобной ситуации не возникнет, так как у каждого объекта будет своя часть буфера (поправьте, если этот вывод ошибочен).

Но возник следующий вопрос...

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

Я стал искать ответ на этот вопрос, и наткнулся на такую штуку как glBindBufferRange. Я думал что эта функция привязывает часть буфера к конкретному блоку у шейдера. То есть, мне казалось что если в шейдере создать uniform блок, например, только с одной структурой mat4, то именно к этой структуре можно будет осуществить привязку части буфера перед очередным draw call'ом.

Я попробовал провернуть это таким образом:

// Вычислить смещение для текущего мега
offset = sizeof(glm::mat4) * _currentMeshIndex;
// Обновить часть буфера с учетом смещения
_currentMesh.writeModelMatrixToUniformBufferStd140(ubo::_modelMatrices, offset);
// Привязать к точке привязке только что обновленную часть буфера
glBindBufferRange(GL_UNIFORM_BUFFER, ubo_idx::_modelMatrices, ubo::_modelMatrices, offset, sizeof(glm::mat4));

Но судя по всему я не правильно понял, как работает glBindBufferRange. Ничего толкового из этого не вышло.

Но тогда как?

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


#1
13:15, 10 сен. 2019

AlexNem
> В итоге все стало действительно работать заметно быстрее.

на сколько в %?

#2
13:43, 10 сен. 2019

AlexNem
> glUniformMatrix4fv(glGetUniformLocation(shaderId, "model")

это не хорошо

#3
14:42, 10 сен. 2019

AlexNem
> Я стал искать ответ на этот вопрос, и наткнулся на такую штуку как
> glBindBufferRange. Я думал что эта функция привязывает часть буфера к
> конкретному блоку у шейдера.
Так и есть, но размещение в памяти буфера надо делать не как попало: offset должно быть кратно GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT. Ты ошибки, я смотрю, не проверяешь? После каждого вызова любой gl-функции надо делать checkGLError().

#4
18:49, 10 сен. 2019

AlexNem
>В итоге все стало действительно работать заметно быстрее
Пробуй теперь glMapBuffer/glMapBufferRange/glUnmapBufferвсего буфера 1 раз за кадр и обновлять через memcpy по частям с правильно рассчитанными смещениями как сказал BingoBongo. В glMap* можно еще флажками играться. На Vulkan это мне дало около 12%.

#5
18:54, 10 сен. 2019

Andrey
> через memcpy по частям

а что всё сразу ?

#6
19:20, 10 сен. 2019

AlexNem
> и видимо возникают ситуации, когда слабая встроенная графическая карта не
> успевает отрисовать объект, а буфер уже обновлен матрицей для другого объекта
> (поправьте, если это не так).
Это не так. Не знаю почему у тебя не работало с glBufferSubData, должно работать. В целом юниформ буфера обычно обновляют плность через map c GL_MAP_INVALIDATE_BUFFER_BIT, а не через SubData.

#7
19:29, 10 сен. 2019

AlexNem
> Подумав немного, я пришел к выводу, что буфер матриц модели должен из себя
> представлять именно массив с матрицами под каждый объект, и тогда подобной
> ситуации не возникнет, так как у каждого объекта будет своя часть буфера
> (поправьте, если этот вывод ошибочен).
На самом деле вывод правильный. Делаешь апдейт большого буфера, потом рисуешь из него со смещением. Это наиболее выгодный способ.

AlexNem
> Я попробовал провернуть это таким образом:
У тебя оффсет неправильный. Он должен быть не в байтах, а в basic machine units, для видеокарточки это 4 float-а. Обрати внимание, что он должен быть еще и выровнен на 256 байт или 64 basic machine units. То есть тебе в свой буфер из 3-х матриц надо добывить еще одну dummy матрицу.

#8
20:05, 10 сен. 2019
У тебя оффсет неправильный. Он должен быть не в байтах, а в basic machine units, для видеокарточки это 4 float-а
Так и есть, но размещение в памяти буфера надо делать не как попало: offset должно быть кратно GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT

То есть, если, например, у меня есть огромный uniform-буфер, рассчитанный на 1024 объекта, способный вместить 1024 модельные матрицы 4*4 (каждая из которых занимает по 64 байта или же 16 basic machine units), то ОБНОВЛЯТЬ этот буфер (glBufferSubData) надо с обычным байтовым смещением, а ПРИВЯЗЫВАТЬ нужную часть этого буфера (glBindBufferRange) нужно со смещением в машинных единицах?

То есть если я, допустим хочу, обновить 2-ую матрицу в этом массиве, а затем привязать ее, то смещение для ОБНОВЛЕНИЯ у меня должно быть 64, а для ПРИВЯЗКИ 16? А размер, во время привязки тоже должен быть в машинных единицах? Или в байтах?

#9
20:27, 10 сен. 2019

AlexNem
Я бы запихнул в UBO только view, projection, источники света и все что остается постоянным в течении кадра,
а model matrix и тому подобное (уникальное для каждого меша) передавал бы по старому через uniform,
и не морочил себе голову)

#10
(Правка: 21:14) 21:10, 10 сен. 2019

AlexNem
> а ПРИВЯЗЫВАТЬ нужную часть этого буфера (glBindBufferRange) нужно со смещением
> в машинных единицах?
Да, привязывать со смещением в машинных единицах. Только у тебя опять ошибка:
> (каждая из которых занимает по 64 байта или же 16 basic machine units)
Матрица 4*4 - это 4 machine units. Один machine unit - это четыре флоата. 1 machine unit = 16 байт.

>то смещение для ОБНОВЛЕНИЯ у меня должно быть 64, а для ПРИВЯЗКИ 16?
исходя из вышесказанного для обновления должно быть 64, а для привязки 4

upd. Ну и до кучи: size в glBindBufferRange тоже должен быть в машинных юнитах

#11
21:24, 10 сен. 2019

innuendo
> > glUniformMatrix4fv(glGetUniformLocation(shaderId, "model")
>
> это не хорошо

Почему это не хорошо? Как надо?

putNik
> Я бы запихнул в UBO только view, projection,

Ну дело в том что у меня может быть несколько камер, которые одновременно "рендерят" сцену в разные части кадра (каждая может быть со своим viewport'ом), поэтому в таком случае их тоже придется передавать по старинке, ибо view projection матрицы не будут постоянны в течении одного кадра. Вот я и ищу способы как бы это можно было оптимизировать.. uniform-буферы вроде хороши, но вот только непонятки с несколькими объектами и массивами..

#12
(Правка: 0:27) 0:26, 11 сен. 2019

AlexNem
ну смотри сам тогда...

З.Ы. innuendo  говорит, что взятие индекса по строке тоже не бесплатно,
нужно взять его один раз и хранить для последующего использования.

ПрограммированиеФорумГрафика