В предыдущих статьях я уже рассказывал о проблемах связанных с выводом низкополигональных объектов, таких как текст, спрайты или элементы GUI. В этой статье я предложу одно интересное решение данной проблемы, позволяющее обойти эти проблемы, сведя рисование самого сложного GUI до одной команды рисования.
Что из себя представляют элементы GUI? Правильно, в общем случае это квад или серия квадов, из которых составляется элемент управления. Теперь подумаем как можно задать местоположение отдельного квада? Существует три основных способа:
Третий вариант основан на использовании возможности обновлять содержимое буфера VBO в видеопамяти. Используя эту возможность, задание координат прямоугольника можно записать так:
Тоесть мы биндим буфер VBO, содержащий координаты нашего прямоугольника, и загружаем в него новые координаты из структуры "Rectangle". Рисование, как и в предыдущем тесте, осуществляется с использованием VAO. Давайте посмотрим что из этого получится:
Как и в предыдущем тесте, по вертикале отложено время в миллисекундах, замеренное посредством прецизионного таймера, по горизонтали - количество выведенных квадов. Красная линия соответствует первому случаю (положение задается через glTranslate), зеленая линия соответствует обновлению геометрии через VBO.
Как видно из графика, попытка обновить данные в видеопамяти оказалась существенно дороже. Давайте разберем почему. Во-первых нам приходится биндить каждый буфер дважды, один раз для обновления данных в видеопамяти, второй раз - бинд VAO для рисования, во-вторых - эту процедуру придется повторять при выводе каждого четырехугольника. В результате это привело к двукратному снижению производительности, что совсем не хорошо...
Отсюда очевидна первая оптимизация - уменьшить количество биндов буферов, другими словами - поместить все квады (в примере их 8192) в один большой буфер и обновлять координаты всех квадов "одним махом". В этом случае алгоритм перепишется таким образом:
Таким образом мы одним выстрелом убили сразу целый косяк зайцев - избавились от необходимости дважды биндить буферы VBO, избавились от необходимости многократного вызова процедуры glBufferSubData и избавились от множественного вызова команд рисования, обсуждаемого в предыдущей статье. Давайте проверим, так ли это и что нам это дало:
Для более честной оценки, я для первого варианта (glTranslate) вынес бинд VAO за цикл, чтоб исключить из теста время, затрачиваемое на бинд буферов.
Как видно из графика - за счет такой оптимизации, используя обновление буфера VBO вместо конвеера трансформаций OGL, нам удалось сократить время отрисовки 8192 полигонов в сто раз!
Обращаю внимание на еще один момент - в случае использования первого подхода (glTranslate) мы вынуждены повторять эту процедуру на каждом кадре, но как часто и сколько элементов GUI на самом деле "движутся"? Правильно, в большинстве случаев GUI статично, обычно изменения происходят эпизодически, по событию таймеру или клику мыши и таких элементов GUI очень мало. Это позволяет рассматривать данных подход как вывод статичной модели на 8192 полигонов. Давайте сравним производительность при обновлении на каждом кадре и единоразовое обновление:
Зеленая линия соответствует случаю обновления всего массива данных на каждом кадре, синяя - единоразовому обновлению. Разница не столь существенна как для предыдущего теста, но тем не менее позволяет еще на 20% повысить производительность при обновлении большого количества полигонов.
На этом тесты подошли к концу, но читатели, знакомые с проблемой GUI, наверняка уже приметили у этого подхода существенный недостаток и готовы заявить - мы выводим все элементы GUI, а как же быть с невидимыми? Что делать если у контрола есть несколько разных состояний, а одновременно нужно отображать только одно, как скрыть остальные?
Но об этом в следующей части ;)
Что из себя представляют элементы GUI? Правильно, в общем случае это квад или серия квадов, из которых составляется элемент управления. Теперь подумаем как можно задать местоположение отдельного квада? Существует три основных способа:
- Используем стандартный стэк трансформаций (glTranslate)
- Передавать эти трансформации в шейдер (псевдоинстансинг)
- Явно задать координаты вершин в координатах окна.
glPushMatrix;
glTransletef(x,y,z);
glBindVertexArray(VAO);
glDraw...
glBindVertexArray(0);
glPopMatrix;
Третий вариант основан на использовании возможности обновлять содержимое буфера VBO в видеопамяти. Используя эту возможность, задание координат прямоугольника можно записать так:
var Rectangle: record Left,Top, Right, Bottom: TAffineVector; end;
...
glBindBuffer(GL_ARRAY_BUFFER, vId);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(Rectangle), @Rectangle);
glDraw...
glBindBuffer(GL_ARRAY_BUFFER,0);
Тоесть мы биндим буфер VBO, содержащий координаты нашего прямоугольника, и загружаем в него новые координаты из структуры "Rectangle". Рисование, как и в предыдущем тесте, осуществляется с использованием VAO. Давайте посмотрим что из этого получится:
Как и в предыдущем тесте, по вертикале отложено время в миллисекундах, замеренное посредством прецизионного таймера, по горизонтали - количество выведенных квадов. Красная линия соответствует первому случаю (положение задается через glTranslate), зеленая линия соответствует обновлению геометрии через VBO.
Как видно из графика, попытка обновить данные в видеопамяти оказалась существенно дороже. Давайте разберем почему. Во-первых нам приходится биндить каждый буфер дважды, один раз для обновления данных в видеопамяти, второй раз - бинд VAO для рисования, во-вторых - эту процедуру придется повторять при выводе каждого четырехугольника. В результате это привело к двукратному снижению производительности, что совсем не хорошо...
Отсюда очевидна первая оптимизация - уменьшить количество биндов буферов, другими словами - поместить все квады (в примере их 8192) в один большой буфер и обновлять координаты всех квадов "одним махом". В этом случае алгоритм перепишется таким образом:
Type TGUIRect = Record Left,Top, Right, Bottom: TAffineVector; end;
var Rectangles: array[0..RectCount] of TGUIRect;
...
procedure UpdateVBO(Count: integer);
begin
glBindBuffer(GL_ARRAY_BUFFER, vId);
glBufferSubData(GL_ARRAY_BUFFER, 0, Count*sizeof(TGUIRect), @Rectangles);
glBindBuffer(GL_ARRAY_BUFFER,0);
end;
...
UpdateVBO(Count);Процедура обновления буфера VBO аналогична предыдущей, с той разницей, что теперь мы обновляем не один прямоугольник а целую серию. Ключевым изменением тут является тот факт, что мы единственный раз вызываем процедуру glDraw*!
glBindVertexArray(VAO);
glDrawArrays(GL_QUADS, 0, Count*4);
glBindVertexArray(0);
Таким образом мы одним выстрелом убили сразу целый косяк зайцев - избавились от необходимости дважды биндить буферы VBO, избавились от необходимости многократного вызова процедуры glBufferSubData и избавились от множественного вызова команд рисования, обсуждаемого в предыдущей статье. Давайте проверим, так ли это и что нам это дало:
Для более честной оценки, я для первого варианта (glTranslate) вынес бинд VAO за цикл, чтоб исключить из теста время, затрачиваемое на бинд буферов.
Как видно из графика - за счет такой оптимизации, используя обновление буфера VBO вместо конвеера трансформаций OGL, нам удалось сократить время отрисовки 8192 полигонов в сто раз!
Обращаю внимание на еще один момент - в случае использования первого подхода (glTranslate) мы вынуждены повторять эту процедуру на каждом кадре, но как часто и сколько элементов GUI на самом деле "движутся"? Правильно, в большинстве случаев GUI статично, обычно изменения происходят эпизодически, по событию таймеру или клику мыши и таких элементов GUI очень мало. Это позволяет рассматривать данных подход как вывод статичной модели на 8192 полигонов. Давайте сравним производительность при обновлении на каждом кадре и единоразовое обновление:
Зеленая линия соответствует случаю обновления всего массива данных на каждом кадре, синяя - единоразовому обновлению. Разница не столь существенна как для предыдущего теста, но тем не менее позволяет еще на 20% повысить производительность при обновлении большого количества полигонов.
На этом тесты подошли к концу, но читатели, знакомые с проблемой GUI, наверняка уже приметили у этого подхода существенный недостаток и готовы заявить - мы выводим все элементы GUI, а как же быть с невидимыми? Что делать если у контрола есть несколько разных состояний, а одновременно нужно отображать только одно, как скрыть остальные?
Но об этом в следующей части ;)
Комментариев нет:
Отправить комментарий