В предыдущих статьях мы с вами рассмотрели способы вывода низкополигональной геометрии и оценили производительность разных подходов. В этих статьях шла речь об объектах, составленных из нескольких полигонов, таких как частицы, буквы, спрайты, элементы GUI и прочее. Так же мы рассмотрели и более сложные случаи - системы частиц, основное отличие которых состояло в их количестве и динамичности. Но есть и другой класс объектов, в него попадают объекты с количеством полигонов более сотни, это могут быть всякие скамейки, столы, вазы, астероиды, столбы, модели в стратегиях, актеры и прочее. В некоторых случаях это могут быть примитивы (шар, тор, цилиндр и прочее), но чаще это отдельные модели. Для начала давайте определим отличительные черты таких объектов, относительно рассмотренных ранее элементов GUI.
Таких объектов может быть огромное
количество, как на Рис.1., но вместе с ростом количества полигонов меняются и
правила игры. Как всегда начнем с небольшого теста, в котором выводится 1024 копии объекта:
Жутковато не правда ли? Особенно если учесть что под каждым "Apply*/UnApply*" кроется еще по десятку команд OpenGL, при чем не важно, одинаковые это объекты или нет. Кто-то сделает замечание - "ну так ведь ты уже об этом рассказывал в статье о Машине состояний OpenGL и вроде писал что это все решено!". Действительно, это очень близко к написанному ранее, но есть и принципиальное отличие, в частности - появилось понятие сабмеша (SubMesh). Давайте разберемся зачем я его сюда ввел и почему этот момент так принципиален.
Если бы объекты были одинаковыми, то машина состояний OpenGL закешировала бы все установленные свойства и все было бы замечательно, в идеальном случае, но бывают и другие, к примеру как Листинг 1 - после рисования объект происходит принудительный сброс всех состояний ОГЛ на значения по умолчанию (строки 12-17). Это обычная ситуация для графического движка, так как мы не знаем что будет рисоваться дальше и какие из установленных свойств ему не потребуются.
Первое очевидное решение тут сгруппировать сабмеши по каким-то признакам, будь то текстура или шейдер или геометрия. В нашем случае все просто - все сабмеши совпадают, потому предыдущий код можно переписать в таком вот виде:
Что изменилось - мы практически все установки свойств вынесли за цикл, оставив внутри цикла лишь установку матриц трансформации объекта (сброс вынесен за цикл, так как мы их перезаписываем для каждого сабмеша). В результате, все уникальные свойства у нас установятся лишь один раз + отработает процедура кеширования состояний ОГЛ.
Именно такой подход и используется в VBOMesh для вывода инстансов, эффективность работы такого алгоритма можно оценить по приведенной выше демке. Так для фриформы из 366 полигонов мы уже имеем 304фпс, для кубов из 12 полигонов и сферы из 270 полигонов - 500 фпс, для сфер из 1054 полигонов - 190фпс. Это уже близко к тому, что мы ожидали получить, из ряда выбивается только куб с 12 полигонам, по логике при выводе 12 полигонов фпс должен быть выше чем при выводе шарика с 270 полигонами, ответ на эту загадку можно найти в статье Сколько стоит полигон. В двух словах - стоимость вызова процедуры рендеринга и стоимость установки матриц трансформации в сумме оказываются дороже чем вывод этих 12 полигонов, потому ограничивающим фактором тут является не количество полигонов а количество вызовов процедуры рисования. Для более сложных объектов, таких как фриформа и сфера из 1054 полигонов, стоимость вызова процедуры рисования оказывается существенно ниже стоимости самого рисования, что и отразилось на фпс.
Если вы думаете что это уже конец, то вы сильно заблуждаетесь, у нас в цикле еще осталась процедура "ApplyTransformations" и в начале статьи я не зря привел пример с викингами, у которых отличаются по цвету перчатки и кирасы, но всему свое время ;)
- Все эти объекты так же нужно расположить на сцене (менять их координаты), это может потребоваться сделать только один раз, или единоразово по событию, или обновлять их положение на каждом кадре.
- Такие объекты могут быть постоянными или подгружаемыми, могут иметь свои уровни детализации или физическую оболочку или самими быть ими.
- Инстансы это полноценные объекты, которые могут иметь свои настройки материалов, скины, анимацию.
Рис.1. Примеры инстансинга. |
Рис.2. Фриформа, 366 полигонов, 60фпс. Куб, 12 полигонов 169фпс.
Рис.3. Сфера, 270 полигонов, 169фпс. Сфера, 1054 полигона, 167фпс.
Для теста использовалась программа из комплекта демок VBOMesh: http://glsnewton.googlecode.com/files/Proxy.rar
На первый взгляд результат бредовый, можно подумать что демка глючит или не тот скриншот вставил, или при копи-пасте описания допустил ошибку в фпс, ведь как так может быть, что куб с 12 полигонами рисуется с той же скоростью что и сфера с 270 полигонов, а сфера с 270 полигонами рисуется столько же времени, сколько и сфера из 1054 полигонов, в то время как моделька из 366 полигонов рисуется в три раза медленнее этой самой сферы...
Тому кто действительно так подумал стоит перечитать мою статью Машина состояний OpenGL
Предположим вы прочли статью и знаете что корень зла в количестве переключений состояний OpenGL, и весь предыдущий цикл статей был посвящен уменьшению этого числа переключений состояний для конкретных задач (текст, GUI, частицы). Но вариант с текстом и GUI здесь не применим, так объекты большие и нельзя заранее наделать копий в видеопамяти. Вариант с системой частиц нам не подходит, так как количество полигонов очень велико (в демке выше порядка миллиона полигонов) и в большинстве случаев (кроме кубика и шарика) мы не сможем восстановить геометрию этого объекта, потому нужен принципиально новый подход, учитывающий особенности задачи инстансинга.
Чтоб понять что и как оптимизировать, давайте сначала разберем этапы рисования инстансов для штатной ситуации - объект с несколькими сабмешами, у каждого из которых свой шейдерный материал, своя текстура и свои настройки смешивания. Штатный цикл рисования таких объектов имеет вид:
Листинг 1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | for i:=0 to MeshObjects.Count-1 do begin MeshObject:=MeshObjects[i]; for j:=0 to MeshObject.SubMeshes.Count-1 do begin SubMesh:=MeshObject.SubMeshes[j]; SubMesh.ApplyShader; SubMesh.ApplyMaterial; SubMesh.ApplyTexture; SubMesh.ApplyBlending; SubMesh.ApplyTransformations; SubMesh.ApplyVBO; SubMesh.Render; SubMesh.UnApplyVBO; SubMesh.UnApplyTransformations; SubMesh.UnApplyBlending; SubMesh.UnApplyTexture; SubMesh.UnApplyMaterial; SubMesh.UnApplyShader; end; end; |
Жутковато не правда ли? Особенно если учесть что под каждым "Apply*/UnApply*" кроется еще по десятку команд OpenGL, при чем не важно, одинаковые это объекты или нет. Кто-то сделает замечание - "ну так ведь ты уже об этом рассказывал в статье о Машине состояний OpenGL и вроде писал что это все решено!". Действительно, это очень близко к написанному ранее, но есть и принципиальное отличие, в частности - появилось понятие сабмеша (SubMesh). Давайте разберемся зачем я его сюда ввел и почему этот момент так принципиален.
Если бы объекты были одинаковыми, то машина состояний OpenGL закешировала бы все установленные свойства и все было бы замечательно, в идеальном случае, но бывают и другие, к примеру как Листинг 1 - после рисования объект происходит принудительный сброс всех состояний ОГЛ на значения по умолчанию (строки 12-17). Это обычная ситуация для графического движка, так как мы не знаем что будет рисоваться дальше и какие из установленных свойств ему не потребуются.
Тут так же можно было бы делать снимки установленных свойств, искать пересечения множеств установленных и необходимых свойств и устанавливать недостающие, сбрасывая лишние, но до этого у меня еще руки не дошли. Но обычно большинство состояний не меняется, о чем рассказывалось в статье о Машине состояний OGL, потому оказывается проще убрать после себя, выполнив серию "UnApply/Reset", которые в свою очередь могут быть закешированы менеджером состояний ОГЛ.В результате, из-за сабмешей, мы даже при рисовании одинаковых объектов вынуждены устанавливать все свойства для каждого сабмеша каждого объекта. Простой пример - мы рисуем дерево, у дерева есть ствол и есть крона, у ствола текстура коры, у кроны - текстура листьев, кора не прозрачна, у листьев установлен альфатест. При рисовании двух деревьев подряд мы рисуем ствол первого, крону первого, затем ствол второго, крону второго и т.д. И как следствие - вынуждены для каждого сабмеша переключать весь список свойств.
Первое очевидное решение тут сгруппировать сабмеши по каким-то признакам, будь то текстура или шейдер или геометрия. В нашем случае все просто - все сабмеши совпадают, потому предыдущий код можно переписать в таком вот виде:
Листинг 2.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | for i:=0 to MasterProxy.SubMeshes.Count-1 do begin SubMesh:=MasterProxy.SubMeshes[i]; SubMesh.ApplyShader; SubMesh.ApplyMaterial; SubMesh.ApplyTexture; SubMesh.ApplyBlending; SubMesh.ApplyVBO; for j:=0 to MeshObjects.Count-1 do begin MeshObjects[j].ApplyTransformations; SubMesh.Render; end; SubMesh.UnApplyVBO; SubMesh.UnApplyTransformations; SubMesh.UnApplyBlending; SubMesh.UnApplyTexture; SubMesh.UnApplyMaterial; SubMesh.UnApplyShader; end; |
Что изменилось - мы практически все установки свойств вынесли за цикл, оставив внутри цикла лишь установку матриц трансформации объекта (сброс вынесен за цикл, так как мы их перезаписываем для каждого сабмеша). В результате, все уникальные свойства у нас установятся лишь один раз + отработает процедура кеширования состояний ОГЛ.
Именно такой подход и используется в VBOMesh для вывода инстансов, эффективность работы такого алгоритма можно оценить по приведенной выше демке. Так для фриформы из 366 полигонов мы уже имеем 304фпс, для кубов из 12 полигонов и сферы из 270 полигонов - 500 фпс, для сфер из 1054 полигонов - 190фпс. Это уже близко к тому, что мы ожидали получить, из ряда выбивается только куб с 12 полигонам, по логике при выводе 12 полигонов фпс должен быть выше чем при выводе шарика с 270 полигонами, ответ на эту загадку можно найти в статье Сколько стоит полигон. В двух словах - стоимость вызова процедуры рендеринга и стоимость установки матриц трансформации в сумме оказываются дороже чем вывод этих 12 полигонов, потому ограничивающим фактором тут является не количество полигонов а количество вызовов процедуры рисования. Для более сложных объектов, таких как фриформа и сфера из 1054 полигонов, стоимость вызова процедуры рисования оказывается существенно ниже стоимости самого рисования, что и отразилось на фпс.
Если вы думаете что это уже конец, то вы сильно заблуждаетесь, у нас в цикле еще осталась процедура "ApplyTransformations" и в начале статьи я не зря привел пример с викингами, у которых отличаются по цвету перчатки и кирасы, но всему свое время ;)
Комментариев нет:
Отправить комментарий