четверг, 3 ноября 2011 г.

Что внутри. VBO, часть первая, обзорная


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


С выходом OpenGL 3.0/ES VBO стало стандартом вывода геометрии, появилось множество новых статей, обзоров, тестов, рекомендаций, но так было не всегда. В "далеком" 2008 году на весь интернет можно было найти всего 3 примера работы с VBO, которые усердно копировались по всему интернету. Можно было найти публикацию 2004 года, где рассказывалось об оптимизациях вывода графики и все... Все остальное - спецификация ОГЛ и форумы, где приводились порой противоречивые и иногда ошибочные данные.

В детали реализации VBO я вдаваться не буду, на эту тему нынче написано много статей, в том числе и мной, просто расскажу об особенностях. Так как материал объемный, то я его разбил на несколько частей.

Часть первая: Представление и импорт
 
Идея VBO в том, чтоб перенести всю геометрию в видеопамять и в дальнейшем, при необходимости вывода объекта, рисовать ссылаясь на данные в видеопамяти, вместо того, чтоб каждый раз передавать их из системной памяти по достаточно медленной шине. Можно спорить на тему было ли это прорывом или шагом назад по отношению к дисплейным спискам, но все уже решено за нас, потому сосредоточимся на возможностях и ограничениях этой технологии.

Возможности:
  • Ускорение вывода графики за счет отсутствия необходимости передачи данных в видеопамять
  • Возможность изменять данные в видеопамяти
  • Возможность менять наборы вершинных атрибутов модели
  • Возможность представлять геометрию в виде текстуры и наоборот
Недостатки:
  • Необходимость резервирования места под объект
  • Невозможность добавления/удаления полигонов у существующей модели
  • Невозможность комбинирования разных типов полигонов в пределах одного набора геометрии
  • Единый индексный буфер для всех атрибутов
В начале о недостатках и проблемами, с которыми пришлось столкнуться при переходе на VBO. Ключевым недостатком является невозможность дополнить существующий буфер VBO новой геометрией, или удалить часть существующей. Это сразу же привело к проблеме работы с динамическими объектами, такими как линии, точки, частицы, буквы, отладочная информация... По новой идеологии все должно быть заранее заготовлено, что далеко не всегда удобно сделать.
Конкретные проблемы и решения названных проблем будут рассмотрены в будущем, в темах посвященных системам частиц, элементам GUI и т.д.

Вторая неприятная особенность - единый индексный буфер для всех вершинных атрибутов. На этой части я остановлюсь подробнее, так как этот вопрос мне задают очень часто. Сам индексный буфер позволяет повысить производительность за счет использования T&L-кэша, и единственный способ получить сглаженные нормали, потому его использование является обязательным для всех объектов сложнее квада.
Проявляется проблема при попытке загрузить модельку, экспортированную из существующих графических пакетов, таких как 3ds Max и Maya. Будучи разработанными еще во времена OpenGL 1.0 эти пакеты насквозь пропитаны технологиями того времени, как и самые ходовые графические форматы того времени (obj, 3ds, smd). Для экономии памяти/места на диске там так же использовалось индексирование геометрии, но в отличии от VBO там существовало несколько списков индексов, по одному на каждый вершинный атрибут.

Лучше всего это видно на примере текстурированного куба. У куба 6 граней, допустим каждая грань состоит из двух треугольников, таким образом мы получаем 12 треугольников или 36 вершин по 3 вершинных атрибута у каждой (координата, нормаль, текстурная координата). В сумме получается 1152 байта для хранения информации о кубе.
Но любой кто хоть раз видел кубик знает что у него только 8 вершин, все 6 сторон плоские и квадратные. Спрашивается - зачем нам хранить целых 36 вершин с кучей информации для каждой, если достаточно хранить всего 8 вершин, 6 нормалей и 4 текстурные координаты, а уже для 12-ти треугольников указать из какого элемента массива нужно брать информацию для конкретной вершины. Таким образом размер необходимой памяти уменьшился с 1152 байт до 308 байт, тоесть почти в 4 раза. Такую экономию просто не могли обойти стороной, к тому же у нее не было никаких негативных моментов и такие буферы очень легко выводились через glBegin/glEnd.

С приходом к власти VBO это стало серьезной проблемой, так как спецификация требовала чтоб был только один индексный буфер, что означало что количество вершинных атрибутов должно совпадать и что у одной вершины не может быть разных атрибутов.
Вернемся к нашему кубику. Первое положение гласит что размер массивов должен совпадать, тоесть если у нас 8 вершин, то должно быть 8 нормалей и 8 текстурных координат, но тогда возникает проблема - у куба одна вершина принадлежит сразу трем граням, а каждая грань имеет свою нормаль и свою текстурную координату! Потому, чтоб правильно вывести такой кубик, нужно запомнить что все вершины с отличающимися атрибутами должны быть продублированными. Это приводит к тому что вместо 8 вершин на куб нам нужно сохранять по 4 вершины на грань, таким образом у нас получается массив на 24 вершины, где для каждой вершины сохраняется ее координата, нормаль и текстурная координата + единый массив из 36 индексов (по 3 индекса на каждую вершину 12 треугольников).

Таким образом, после загрузки модельки из obj/3ds/ase/lwo и прочих форматов необходимо выполнить "переиндексацию" вершинных атрибутов. На практике это сводится к процедуре "извлечения треугольников", когда по имеющимся индексам вершинных атрибутов создается массив треугольников (каждый элемент массива содержит все вершинные атрибуты), и с последующим поиском в этом массиве вершин с совпадающими атрибутами и замены их на индексы.
"В картинках" об индексации можно почитать здесь:
http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-9-vbo-indexing/

На этом проблемы импорта геометрии не заканчиваются. В OpenGL существует несколько типов примитивов: Triangles, TriangleStrip, TriangleFan, Quads, QuadStrip и Polygons. Все эти примитивы разрабатывались для уменьшения количества обрабатываемых вершин, что повышало скорость вывода и уменьшало объем занимаемой памяти.
Во времена glBegin/glEnd это не представляло никакой проблемы так как каждый полигон выводился отдельно и для каждого полигона можно было указывать свой тип. С приходом VBO эта ситуация так же изменилась, теперь за один раз мы выводим целый пакет полигонов (batch), при этом стараемся максимально уменьшить количество таких пакетов, так как это является одним из узких мест (BottleNeck) конвеера ОГЛ. Вот тут-то и возникает новая проблема - теперь при импорте геометрии из obj/3ds мы должны не только провести переиндексацию геометрии, но еще и привести ее к единому виду (или преобразовать все типы примитивов в треугольники или создать несколько буферов VBO, по штуке на каждый тип примитива). К слову, в ОГЛ 3.х+ удалили поддержу квадов и полигонов, так что лучше сразу все преобразовывать в треугольники или полоски треугольников.

Вот мы и подошли к еще одной проблеме - полоски (strips) треугольников/квадов. Когда вывод осуществляется посредством glBegin/glEnd мы сами указываем начало и конец этой полоски, когда это все находится в одном буфере VBO, то такой возможности у нас уже нет (вернее есть, но мы стремимся уменьшить количество батчей), в результате если выводить все одним куском, то мы получим лишние полигоны, соединяющие куски модели.
В свое время для этих целей NVidia придумала специальный "терминатор", вершину со специальным индексом, которая сигнализировала драйверу что нужно разорвать текущую полоску треугольников и начать новую со следующего индекса. К сожалению ATI это не поддержала, а нарушать и без того хрупкую совместимость никто не решался. К счастью был еще один способ, так называемые "вырожденные треугольники", это такие треугольники у которых две вершины совпадают, таким образом их площадь равна нулю и при выводе мы их не видим. Для этого достаточно было между полосками треугольников продублировать две вершины - конец предыдущей полосы и начало следующей.

Вот с такими проблемами пришлось столкнуться при переходе на VBO, это даст представление о том, почему в GLScene, заточенную под OGL 1.1 с его glBegin/glEnd и дисплейными списками так сложно перейти на VBO и почему появился VBOMesh как надстройка над существующим функционалом GLScene.

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

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