пятница, 10 февраля 2012 г.

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

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

Первый раз о проблеме вывода низкополигональных объектов мы заговорили в статье "Рисуем "Hello World!", где было решено что множество однотипных низкополигональных объектов (отдельных букв текста) лучше объединять в один буфер или текстуру, причину этого мы разобрали в статье "Сколько стоит полигон", где на практике убедились в том, что вывод лишнего полигона без растеризации практически ничего не стоит и существенно дешевле лишнего бинда буфера, а если и приходится это делать, то лучше группировать их в пакеты по сотне полигонов. Ну и в последней статье, "Рисуем GUI. Часть первая, немного тестов", мы в очередной раз убедились что архитектура современных видеокарт ужасно плохо стыкуется с низкополигональной графикой, даже в плане задания трансформаций. Так мы выяснили, что при работе с отдельными квадами можно получить стократный прирост производительности, просто отказавшись от конвеера трансформаций OpenGL, заменив его прямой модификацией вершинных данных.

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




Какие есть способы - их несколько, частично они уже упоминались в предыдущих статьях:
  1. Один бинд буфера + множественный DrawCall;
  2. Один бинд буфера + массив указателей на индексы отдельных элементов с использованием команд glMultiDraw*;
  3. Коллапс полигонов.
Первый вариант мы уже хорошо рассмотрели и знаем к чему это может привести, второй способ - есть сложности с поддерживаемым железом и драйверами, на практике производительность выше не намного, но сам алгоритм рендера оказывается несколько  сложнее. И последний вариант - о нем я раньше не упоминал, но он является ключевым при работе с low-poly объектами - коллапс полигонов.

Коллапс, она же свертка, или сжатие объекта в материальную точку, он же "вырожденный полигон", он же "полигон с нулевой площадью" - процесс, при котором на выходе мы получим полигон без растеризируемых пикселей. Теперь вспоминаем - не встречалось ли нам это определение раньше? Встречалось, и даже выделено в начале этой статьи. Другими словами, если мы "свернем" полигон, то наша задача превратится в задачу, описанную в статье "Сколько стоит полигон", а из тестов мы уже знаем, что это очень дешевая процедура.

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

Подытожим сказанное - для эффективного рендеринга GUI нужно выполнить несколько условий:
  • Объединить всю геометрию в один буфер, для исключения переключения между буферами VBO;
  • Вместо стандартного конвеера трансформаций OpenGL использовать прямое обновление буфера VBO в видеопамяти, причем выполнять такое обновление только для нужной части буфера и только по требованию;
  • Для управления видимости использовать "свертку" полигонов;
  • Рисовать всю геометрию одним пакетом.
В этом случае, на любое количество элементов GUI, потребуется всего один вызов команды рисования!

В приведенном примере выводится примерно 700 элементов управления, что соответствует примерно 4к полигонов. На вывод этого множества потребовалось всего 2 команды рисования, причем только потому, что я решил сгруппировать контролы.

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

Основываясь на этом предположении я решил что в основе должно быть именно статичное меню, это существенно упростило менеджер квадов. Для динамических элементов управления осталась возможность создавать/удалять их рантайм, не привязываясь к "статичным" контролам.

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

Этот же подход может быть с успехом применен при выводе спрайтов, системы частиц, импостеров, травы и многого другого, состоящего из малого числа полигонов.

Так же хочу отметить что осталась еще одна нерешенная проблема, над которой еще предстоит поработать - вывод текста. Да, я помню что я об этом написал целую статью :) Собственно даже на картинке выше вы можете видеть разные надписи, так о чем же речь? Речь, как всегда, об оптимизации. Этот цикл статей был посвящен оптимизации работы с геометрией, а текст предоставляет нам новую проблему, связанную с оптимизацией переключения текстур.

Дело в том, что каждая надпись так или иначе представляет из себя отдельную текстуру (методика описана в статье "Рисуем "Hello World!"), при этом, нам необходимо выполнить минимум два действия: 1) - забиндить новую текстуру текста, 2) - отрисовать геометрию текста с новой текстурой. Таким образом мы не можем выводить текст в одном пакете с геометрией контролов и вынуждены рисовать его отдельно (так сделано в моем модуле uGUI), для каждого биндя свою геометрию и свою текстуру (геометрия всех надписей одной группы хранится в одном отдельном буфере VBO). Какие есть пути опимизации:
  1. Использовать мультиполигональный вывода текста (каждой букве соответствует свой полигон со своим фрагментом общей текстуры). Плюс такого подхода - текст можно будет рисовать в одном пакете с геометрией контрола и исчезнет необходимость в переключении текстур. Минус - более сложный менеджер текста, который должен учитывать динамическую природу текста, который должен предусмотреть возможность менять шрифты отдельных надписей, увеличение числа полигонов и прочее (подробнее описано в статье "Рисуем "Hello World!")
  2.  Помещать надписи в специальные текстурные атласы. Плюсы в том же, минусы - сложность построения и управления таким атласом, с учетом динамичности текста.
Оба подхода имеют существенные недостатки и могут доставить проблем при реализации, потому я пока ищу альтернативный вариант, а до тех пор текст будет выводиться отдельно.

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

На этом цикл статей, посвященных GUI и оптимизации вывода low-poly, редко-меняющихся  объектов можно считать закрытым. В дальнейших статьях мы разберемся с оптимизацией вывода множества динамических низкполигональных объектов(таких как частицы) и объектов с одинаковой геометрией - инстансов.

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

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