вторник, 17 июля 2012 г.

Частицы. Часть первая, и снова тесты.


Часть первая, и снова тесты
Часть вторая, если очень хочется
Часть третья, миллионы частиц
Часть четвертая, полевые испытания


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


Для начала определимся с терминологией - спрайтом я называю один большой объект с хорошо различимой текстурой, ориентированный на камеру (сферический спрайт) или с фиксированной осью (цилиндрический спрайт). Примером спрайта может быть полоска здоровья над головой юнита, указатели, некоторые эффекты, облака, или огонь созданный из анимированной текстуры,  импостеры (Imposters, о них мы поговорим позже) и прочие крупные одиночные или находящиеся в небольшой группе объекты. Примеры некоторых спрайтов можно посмотреть на рисунках ниже. Более подробно о типах спрайтов и формированию их матриц трансформации  можно прочесть к примеру тут:
http://www.lighthouse3d.com/opengl/billboarding/



Рис.1. Примеры спрайтов

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


Рис.2. Примеры частиц из ParticleIllusion 3.0

Второй тип подобных объектов это частицы (Рис.2.). Основное отличие от спрайтов заключается в том, что они мелкие и их много - от десятков до миллионов. Примерами таких объектов могут быть пыль, объемные облака, искры, дым, огонь, вода, гильзы, осколки, огоньки и т.д. и т.п. Тоесть куча однотипных объектов, обычно имеющих одинаковую текстуру (или текстуру из атласа), часто полупрозрачные, за счет чего создаются объемные эффекты, обычно очень динамичные, иногда для них реализована проверка коллизий. Работа с такими объектами принципиально отличается от работы со спрайтами, в первую очередь из-за необходимости на каждом кадре обновлять координаты огромного числа частиц и вычислять для каждой из них новую матрицу трансформации. Вдобавок может потребоваться еще и отсортировать эти частицы по глубине сцены, для решения проблемы с прозрачностью. Во что именно выливаются эти отличия, а так же способы решения возникающих проблем, мы и разберем в этой статье.

Существует минимум два разных подхода к рендерингу частиц, первый предполагает весь расчет частиц на CPU, второй - на GPU. У каждого подхода есть свои достоинства и недостатки, в частности, расчет на CPU позволяет боле гибко управлять параметрами каждой отдельной частицы, но из-за этого мы проигрываем в скорости отрисовки. При использовании GPU мы работаем уже с массивами однотипных частиц, за счет чего существенно повышается производительность, но усложняется контроль параметров отдельных частиц. Но использование только CPU или только GPU это крайние случаи, между которыми существует множество гибридных решений, позволяющих найти баланс между производительностью и гибкостью настроек, но обо всем по-порядку.

Начнем мы рассмотрение с "левого" крайнего варианта - расчет частиц целиком на CPU, используя методику, описанную в предыдущей статье для элементов GUI. Для этого проведем небольшой тест - выведем пару миллионов частиц через обновление координат в видеопамяти. Методика тестирования аналогична предыдущим статьям - используется прецизионный таймер для замера времени выполнения команды, отключение записи в буфер глубины и буфер цвета, отключение теста глубины. Рисование производим без использования индексного буфера, чтоб отключить какое либо кеширование. Тоесть проверим как себя ведет обновление большого объема геометрии без растеризации:
Рис.3. Рендеринг квадов с обновлением координат на каждом кадре

На левом графике хорошо заметен излом -  при увеличения количества полигонов до 65536 штук мы столкнулись с первым узким местом (Bottleneck) - пропускной способностью шины PCIEx. Что тут происходит - видеокарта быстро отрисовала имеющуюся у нее геометрию и ожидает новую порцию данных, передаваемую по медленной шине. На графике справа видно что дальше между объемом данных и временем их обработки (загрузка + отрисовка) зависимость линейная.

На графиках ниже раздельно представлено время загрузки данных в видеопамять и время отрисовки этих данных:
Рис.4. Время загрузки и отрисовки данных

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

Таким образом, при попытке обновить большое количество геометрии, вы упретесь в пропускную способность шины, что приведет к простою видеокарты и существенному падению общей производительности. Чтоб сгладить эти простои в OpenGL отказались от использования команд незамедлительного исполнения, таких как glBegin и дисплейные списки, реализовав работу с VBO в асинхронном режиме. Что это значит - это значит, что пока данные грузятся в видеопамять мы можем отрисовывать уже загруженные данные, причем загрузка данных может происходить на протяжении нескольких кадров рендера, единственное условие - нельзя использовать команды незамедлительного исполнения, которые приведут к принудительной синхронизации (приостановки основного потока CPU до выполнению всей очереди команд на GPU). К этим командам относятся все команды начинающиеся с glGet*, команды glFinish/glFlush, команды построения дисплейных списков, команды рисования glBegin/glEnd, загрузка текстур и еще ряд команд. Для отладки приложений рекомендую использовать gDEBugger, кроме всего прочего он так же предупреждает об использовании таких команд:


Рассчитывая загрузку шины нужно принимать во внимание этот факт асинхронной передачи данных и тот факт, что кроме обновления геометрии шина используется для передачи команд, для передачи юниформ, для загрузки/выгрузки текстур и прочих операций. Таким образом, перегруженная шина снизит вам общую производительность приложения, даже если вы растянете процесс загрузки данных на несколько кадров рендера. Именно потому категорически не рекомендую использовать в GLScene компоненты GLTerrainRender и GLActor, в которых кроме того что вся геометрия передается на каждом кадре, эта геометрия еще и вычисляется на стороне CPU, что приводит к постоянному простою видеокарты. Тоесть, увеличение мощности видеокарты не приведет к увеличению фпс (доли процента на вывод уже загруженной геометрии). К слову, вывод частиц в GLScene осуществляется через glBegin/glEnd...

Это вызвано историческими причинами - на заре 3D индустрии, когда видеопамяти было всего несколько мегабайт а производительность CPU была в 10 раз выше производительности GPU, расчет на CPU, с передачей на каждом кадре рендера оптимизированной геометрии на отрисовку, был вполне оправдан (и часто был единственно возможным). Так как ядро GLScene разрабатывалось в то время и в тех условиях, то такая архитектура вполне понятна.

Сейчас ситуация кардинально изменилась, во-первых существенно увеличился объем видеопамяти, во-вторых - пропускная способность видеопамяти даже бюджетных видеокарт превосходит пропускную способность системной памяти (топовые видеокарты имеют пропускную способность памяти порядка 150Гб/сек, в то время как топовая системная память имеет пропускную способность всего 20Гб/сек), при этом так же изменилась и архитектура видеокарт, перейдя от одноядерной до многоядерной. К примеру, реальная частота 8-ми ядерного процессора Phenom II X8 3020 - 3ГГц, в то время как видеокарта GTX 580, соизмеримая с ним по цене, оснащена 512 ядрами, с частотой 1.5ГГц каждое. Не сложно посчитать, что суммарная производительность видеокарты будет выше в 32 раза. Безусловно, не все задачи можно эффективно распараллелить, чтоб на 100% загрузить видеосистему, но это же относится и к CPU. Так же не все задачи можно одинаково эффективно решать на многопроцессорных системах, к примеру задачу сортировки или вычисление рекуррентных соотношений. Одно очевидно - 512 процессоров справятся с расчетом 512 частиц быстрее чем 8 процессоров, но это не решает нашу главную проблему - как же эффективно доставить эти данные в видеопамять, потому продолжим исследование.

Не смотря на все описанные ограничения, иногда передача данных в видеопамять все же  необходима, примером служит все та же система частиц или физическая симуляция (деформация объектов, ткань и прочее). Как тут быть? Вариантов тут несколько, в первую очередь это снижение частоты обновления данных в видеопамяти (рендер идет с частотой 60 к/с а обновление данных с частотой 15к/с), если это позволяет логика работы программы, то можно разбить геометрию на несколько пакетов загрузка+рендеринг, в этом случае, пока будет рисоваться первый пакет будет загружаться геометрия второго пакета. Именно так было сделано в первом тесте (Рис.3.), там геометрия выводилась 8 пакетами. Если теперь посмотреть на эти графики, к примеру на отметку 65536, то видно, что при таком подходе нам потребовалось на загрузку и отрисовку 65536 полигонов в первом случае (в 8 пакетах) примерно 2.5мс, во втором случае (Рис.4.), когда мы эти данные грузили в видеопамять одним пакетом, то нам потребовалось уже 2+1.1=3.1мс, тоесть разбитие на пакеты дало нам 24% прирост производительности.


Второе очевидно решение - уменьшение объема передаваемой геометрии. Количество вершин четырехугольника мы уменьшить не можем, но мы можем использовать для типа координат вместо 32-битного GL_FLOAT (single) 8 битный GL_BYTE или 16-битный GL_SHORT, таким образом мы сразу в 4 раза (для GL_BYTE) снизим объем передаваемых данных. Но что делать, если точность критична? Или даже снижения объема передаваемых данных в 4 раза нам недостаточно? А вот об этом мы поговорим в следующей части статьи :)

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

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