понедельник, 3 июля 2017 г.

Shader Subroutines. Часть 2. Материалы на подпрограммах.

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

В качестве примера я решил взять что-то сложнее шариков и кубиков, выбор остановился на Crytek Sponza. В этой моделе 394 объекта в которых в сумме 280к треугольников и 25 материалов использующих 54 текстуры высокого разрешения. Наша задача - вывести эту модель за один DrawCall:


Пришлось немного пошаманить с загрузкой модели, из-за чего пример немного усложнился. В конечном счете модель из формата obj была переиндексирована и сконвертирована в бинарный формат (.ivnt), удобоваримый для OpenGL. Для единообразия (и чтоб не писать парсер mtl) материалы (.mat) и информация об объектах (.objs) была сохранена в формате ini-файла. 

Основная сложность заключалась в том, что нам нужно было подружить несколько различных структур данных:
Структура Materials у нас содержит загруженные из .mat данные материалов, структура Objects - данный из .obj. Но наша задача поместить эти данные в SSBO для доступа к ним из шейдера. Для этого было создано две дополнительные структуры данных - MatInfo и ObjInfo соответственно. Разберем их чуть более детально.

Структура MatInfo содержит кроме значений векторов (Ka,Kd,Ks) еще и хэндлы соответствующих текстурных карт (*_handle) если они имеются. Используя эти хэндлы мы можем затекстурировать объект используя стандартный Bindless Texturing. Однако здесь возникает заминка, описанная в предыдущей части статьи - не у всех материалов могут быть текстурные карты и тем более не гарантирован их полных набор. В частности у модели Sponza имеются только Diffuse, Normal, Specular и Mask карты, причем в разном составе у разных объектов, есть так же объекты вообще без текстурных карт. Текстурирование таких объектов в шейдере привело бы к брэнчингу или генерации целой пачки шейдеров на каждый возможный набор свойств и карт. 

Чтоб решить эту проблему я заменил прямое обращение к свойстве/текстуре объекта на вызов одной из трех предопределенных функций - get*FromMap, get*FromValue, doNothing. Первая функция - читает значение из текстуры, вторая - из свойства, третья - просто сохраняет дефолтное значение. 
Это лишь один из возможных вариантов представления материалов, на практике, используя этот же принцип подмены прямых значений на функции, их возвращающие, можно добиться сколько угодно сложной системы материалов, но описание такой системы слишком сложно для статьи.
Для примера я реализовал освещение по Фонгу (с ошибками, но надеюсь копипастеры меня простят)). Для этого я разделил код освещения на 4 независимые функции - вычисление нормали (norm_func), вычисление альбедо (albedo_func), вычисление прозрачности (opac_func) и вычисление блика (spec_func). Соответственно каждая из этих функций может быть одной из трех - get*FromMap, get*FromValue и doNothing, в зависимости от того, какие свойства определены у материала. Получился такой вот набор функций:

Как можно видеть из объявления - используется три разных сигнатуры для каждого из возвращаемых типов:
   subroutine float floatMaterial(in uint, in float);
   subroutine vec3 vec3Material(in uint, in vec3);
   subroutine vec4 vec4Material(in uint, in vec4);
Всего у меня получилось 10 разных функций. Теперь необходимо объявить соответствующие юниформы:
    subroutine uniform floatMaterial floatMaterials[3];
    subroutine uniform vec3Material vec3Materials[2];
    subroutine uniform vec4Material vec4Materials[5];
Каждая из этих юниформ будет содержать индексы соответствующих функций, в частности, я для себя определил их так:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    const GLuint DO_NOTHING = 0;
    const GLuint OPACITY_FROM_MAP = 1;
    const GLuint OPACITY_FROM_VALUE = 2;

    const GLuint NORMAL_FROM_MAP = 1;

    const GLuint ALBEDO_FROM_MAP = 1;
    const GLuint ALBEDO_FROM_VALUE = 2;

    const GLuint SPECULAR_FROM_MAP = 3;
    const GLuint SPECULAR_FROM_VALUE = 4;

Строки 1-3 задают элементы  floatMaterials[3], строка 5 задает первый элемент vec3Materials[2] (0-й элемент всех структур это DO_NOTHING), и строки 7-11 задают 5 элементов vec4Materials[5]. За каждым из этих номеров будет закреплена соответствующая подпрограмма:


1
2
3
4
5
6
  GLuint func[10] = {
      doNothingFloat, getOpacityFromMap,getOpacityValue,
      doNothingVec3, getNormalFromMap,
      doNothingVec4, getAlbedoFromMap, getAlbedoValue, getSpecularFromMap, getSpecularValue
  };
  glUniformSubroutinesuiv(GL_FRAGMENT_SHADER, 10, &func[0]);

Как мы помним из предыдущей части статьи - индексы всех подпрограмм должны передаваться за один раз, даже если они разбиты по массивам. В нашем случае они разбиты на группы по 3, 2 и 5 элементов. 
Про порядок следования юниформ не сказано ничего, помимо того, что элементы массива идут последовательно, так что в идеале, для каждой группы юниформ, мы должны были бы получить смещение через glGetSubroutineUniformLocation, но я поленился, подсмотрел что их порядок соответствует порядку объявления на том и остановился.
P.S. Лень до добра не доводит, как оказалось, порядок следования групп юниформ на AMD категорически отличается от такового на NVidia (7,0,2 против 0,3,5 на NVidia), как следствие, на AMD демка падала при попытке установить индексы подпрограмм, о чем мне за 7 месяцев никто не пожелал сообщить :( 
Дальше идет рутина - грузим модельку, грузим библиотеку материалов, грузим объекты, создаем на основе этих данных структуры ObjInfo и MatInfo и заполняем их данными согласно информации о материале. В зависимости от наличия текстуры - подставляем индекс нужной подпрограммы и не забываем указать у объекта индекс материала в буфере SSBO.
Потом останется сформировать командный буфер для glMultiDrawElementsIndirect на основе информации об объектах и непосредственно вывести всю сцену за один DrawCall. Весь "рендер" у нас будет выглядеть так:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  glUseProgram(shader->Id);
  gl_helpers::setUniform(shader,std::string("Proj"),ProjMatrix);
  gl_helpers::setUniform(shader,std::string("View"),ViewMatrix);

  objects->Bind(1);
  materials->Bind(2);

  GLuint func[10] = {
      doNothingFloat, getOpacityFromMap,getOpacityValue,
      doNothingVec3, getNormalFromMap,
      doNothingVec4, getAlbedoFromMap, getAlbedoValue, getSpecularFromMap, getSpecularValue
  };
  glUniformSubroutinesuiv(GL_FRAGMENT_SHADER, 10, &func[0]);


  obj->Bind(true);
    glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, cmd->data(), cmd->size(), 0);
  obj->Bind(false);

  glUseProgram(0);

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

Что можно сказать напоследок - технология имеет право на жизнь, в связке с удобным инструментом построения графов можно создавать эффекты любой сложности и динамически менять их непосредственно в момент рендеринга, к примеру создавая LODы, или подменяя текстуры на процедурные материалы (трансформации, скины и прочее), привязав эффекты к кадрам анимации или вычисляя их. Возможности поистине безграничные.

Ну и про ложку дегтя - увы, использование шейдерных подпрограмм не бесплатное, полноценный тест я не проводил, но некоторые замеры fps в процессе разработки я сделал. В частности получилось что сцена, отрисованная только с использованием биндлесс текстур и без освещения показала 1300фпс в то время как полноценная сцена с шейдерными подпрограммами и освещением уже выдала всего 685фпс, тоесть эффективность снижается на 50%. Если отключить нормал-маппинг и спек - можно выжать 870фпс, это же значение фпс получается если заменить алгоритм рисования на классический, со сменой шейдеров и отрисовкой каждого объекта по отдельности.

Итого:

Плюсы
Минусы
Отсутствие необходимости в
генерации множества шейдеров
на каждый возможный случай
Сложность описания взаимосвязей
объектов и путей вызова подпрограмм
Возможность менять эффекты
«на лету» для каждого пикселя
Существенное (50%) снижение
производительности относительно
сцены с одним эффектом
(без переключения шейдеров)
Производительность соизмерима
с затратами на переключение шейдеров
Производительность соизмерима
с затратами на переключение шейдеров
Возможность «разукрасить» попиксельно сложную сцену при отложенном шейдинге


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

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