четверг, 9 февраля 2012 г.

Рисуем GUI. Часть первая, немного тестов.

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

Что из себя представляют элементы GUI? Правильно, в общем случае это квад или серия квадов, из которых составляется элемент управления. Теперь подумаем как можно задать местоположение отдельного квада? Существует три основных способа:
  1. Используем стандартный стэк трансформаций (glTranslate)
  2. Передавать эти трансформации в шейдер (псевдоинстансинг)
  3. Явно задать координаты вершин в координатах окна.
О псевдоинстансинге мы поговорим позже, забегая наперед скажу, что для нашего случая результат будет мало отличаться от использования glTranslate, потому рассмотрим более подробно первый и третий вариант. С первым вариантом все понятно, это стандартный конвеер трансформаций OGL:
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);
glBindVertexArray(VAO);
glDrawArrays(GL_QUADS, 0, Count*4);
glBindVertexArray(0); 
Процедура обновления буфера VBO аналогична предыдущей, с той разницей, что теперь мы обновляем не один прямоугольник а целую серию. Ключевым изменением тут является тот факт, что мы единственный раз вызываем процедуру glDraw*!
Таким образом мы одним выстрелом убили сразу целый косяк зайцев - избавились от необходимости дважды биндить буферы VBO, избавились от необходимости многократного вызова процедуры glBufferSubData и избавились от множественного вызова команд рисования, обсуждаемого в предыдущей статье. Давайте проверим, так ли это и что нам это дало:


Для более честной оценки, я для первого варианта (glTranslate) вынес бинд VAO за цикл, чтоб исключить из теста время, затрачиваемое на бинд буферов.
Как видно из графика - за счет такой оптимизации, используя обновление буфера VBO вместо конвеера трансформаций OGL, нам удалось сократить время отрисовки 8192 полигонов в сто раз!

Обращаю внимание на еще один момент - в случае использования первого подхода (glTranslate) мы вынуждены повторять эту процедуру на каждом кадре, но как часто и сколько элементов GUI на самом деле "движутся"? Правильно, в большинстве случаев GUI статично, обычно изменения происходят эпизодически, по событию таймеру или клику мыши и таких элементов GUI очень мало. Это позволяет рассматривать данных подход как вывод статичной модели на 8192 полигонов. Давайте сравним производительность при обновлении на каждом кадре и единоразовое обновление:


 Зеленая линия соответствует случаю обновления всего массива данных  на каждом кадре, синяя - единоразовому обновлению. Разница не столь существенна как для предыдущего теста, но тем не менее позволяет еще на 20% повысить производительность при обновлении большого количества полигонов.

На этом тесты подошли к концу, но читатели, знакомые с проблемой GUI, наверняка уже приметили у этого подхода существенный недостаток и готовы заявить - мы выводим все элементы GUI, а как же быть с невидимыми? Что делать если у контрола есть несколько разных состояний, а одновременно нужно отображать только одно, как скрыть остальные?

Но об этом в следующей части ;)

Комментариев нет:

Отправить комментарий