воскресенье, 22 июля 2012 г.

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

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

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

http://http.developer.nvidia.com/GPUGems3/elementLinks/02fig01.jpg
Рис.1. Примеры инстансинга.
Таких объектов может быть огромное количество, как на Рис.1., но вместе с ростом количества полигонов меняются и правила игры. Как всегда начнем с небольшого теста, в котором выводится 1024 копии объекта:

 Рис.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" и в начале статьи я не зря привел пример с викингами, у которых отличаются по цвету перчатки и кирасы, но всему свое время ;)

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

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