Game Engine Design - Ubershader - Дизайн управления шейдерами [закрыто]

18

Я хочу реализовать гибкую систему Ubershader с отложенным затенением. Моя текущая идея состоит в том, чтобы создавать шейдеры из модулей, которые имеют дело с определенными функциями, такими как FlatTexture, BumpTexture, Displacement Mapping и т. Д. Есть также небольшие модули, которые декодируют цвет, выполняют тональное отображение и т. Д. Это имеет то преимущество, что я могу заменить определенные типы модулей, если графический процессор не поддерживает их, поэтому я могу адаптироваться к текущим возможностям графического процессора. Я не уверен, что этот дизайн хорош. Боюсь, что сейчас я могу сделать плохой выбор дизайна, а потом заплатить за него.

Мой вопрос: где я могу найти ресурсы, примеры, статьи о том, как эффективно внедрить систему управления шейдерами? Кто-нибудь знает, как движки большой игры делают это?

Майкл Стауд
источник
3
Недостаточно времени для реального ответа: с этим подходом у вас все получится, если вы начнете с малого и позволите ему органично развиваться в соответствии с вашими потребностями, а не пытаться создать шейдеры MegaCity-One. Во-первых, вы меньше всего беспокоитесь о том, чтобы слишком много делать заранее и платить за это позже, если это не сработает, во-вторых, вы избегаете дополнительной работы, которая никогда не используется.
Патрик Хьюз
К сожалению, мы больше не принимаем вопросы «запроса ресурсов».
Гнемлок

Ответы:

23

Полу распространенным подходом является создание того, что я называю компонентами шейдеров , аналогично тому, что я называю модулями.

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

Теперь вы можете взять этот график и сгенерировать из него код шейдера. В основном это означает «вставку» кода чанков на место, с графиком, который гарантирует, что они уже находятся в нужном порядке, а затем вставку в шейдерные входы / выходы соответствующим образом (в GLSL это означает определение вашего «глобального» в , и равномерные переменные).

Это не то же самое, что подход Ubershader. Ubershaders - это место, где вы помещаете весь код, необходимый для всего, в один набор шейдеров, возможно, используя #ifdefs и uniforms и т.п., чтобы включать и выключать функции при их компиляции или запуске. Я лично презираю подход Ubershader, но некоторые довольно впечатляющие движки AAA используют их (в частности, Crytek приходит на ум).

Вы можете обрабатывать блоки шейдера несколькими способами. Самый продвинутый способ - и полезный, если вы планируете поддерживать GLSL, HLSL и консоли - это написать синтаксический анализатор для языка шейдеров (возможно, настолько близкий к HLSL / Cg или GLSL, насколько это возможно для максимальной «понятности» ваших разработчиков). ), который затем может быть использован для переводов от источника к источнику. Другой подход заключается в том, чтобы просто обернуть блоки шейдеров в файлы XML или тому подобное, например,

<shader name="example" type="pixel">
  <input name="color" type="float4" source="vertex" />
  <output name="color" type="float4" target="output" index="0" />
  <glsl><![CDATA[
     output.color = vec4(input.color.r, 0, 0, 1);
  ]]></glsl>
</shader>

Обратите внимание, что при таком подходе вы можете создать несколько разделов кода для разных API или даже создать версию раздела кода (так что вы можете иметь версию GLSL 1.20 и версию GLSL 3.20). Ваш график может даже автоматически исключать фрагменты шейдера, у которых нет совместимого раздела кода, так что вы можете получить незначительную деградацию на старом оборудовании (например, обычное отображение или что-то еще, что просто исключено на старом оборудовании, которое не может его поддерживать, без необходимости программиста) сделать кучу явных проверок).

Затем образец XMl может сгенерировать что-то похожее (извините, если это недопустимый GLSL, прошло много времени с тех пор, как я подверг себя этому API):

layout (location=0) in vec4 input_color;
layout (location=0) out vec4 output_color;

struct Input {
  vec4 color;
};
struct Output {
  vec4 color;
}

void main() {
  Input input;
  input.color = input_color;
  Output output;

  // Source: example.shader
#line 5
  output.color = vec4(input.color.r, 0, 0, 1);

  output_color = output.color;
}

Вы могли бы быть немного умнее и генерировать более «эффективный» код, но, честно говоря, любой шейдерный компилятор, который не является полным дерьмом, собирается удалить излишки из этого сгенерированного кода для вас. Может быть, более новый GLSL #lineтеперь позволяет вводить имя файла в команды, но я знаю, что более старые версии очень несовершенны и не поддерживают это.

Если у вас есть несколько чанков, их входы (которые не предоставляются как выход чанком предка в дереве) объединяются во входной блок, как и выходы, а код просто соединяется. Проделана небольшая дополнительная работа, чтобы убедиться, что этапы совпадают (вершина против фрагмента) и что входные макеты атрибута вершины «просто работают». Еще одним приятным преимуществом этого подхода является то, что вы можете написать явные унифицированные индексы и индексы привязки входных атрибутов, которые не поддерживаются в более старых версиях GLSL, и обрабатывать их в своей библиотеке генерации / связывания шейдеров. Точно так же вы можете использовать метаданные при настройке ваших VBO и glVertexAttribPointerвызовов для обеспечения совместимости и того, что все «просто работает».

К сожалению, такой хорошей библиотеки кросс-API уже нет. Cg довольно близок, но он поддерживает дерьмовую поддержку OpenGL на картах AMD и может быть очень медленным, если вы используете какие-либо, кроме самых основных функций генерации кода. Платформа эффектов DirectX также работает, но, конечно, не поддерживает ни один язык, кроме HLSL. Для GLSL есть несколько неполных / ошибочных библиотек, которые имитируют библиотеки DirectX, но, учитывая их состояние, когда я проверял в прошлый раз, я просто написал свою собственную.

Подход Ubershader просто означает определение «хорошо известных» директив препроцессора для определенных функций, а затем перекомпиляцию для разных материалов с различной конфигурацией. Например, для любого материала с картой нормалей, которую вы можете определить, USE_NORMAL_MAPPING=1а затем в своем пиксельном этапе ubershader просто:

#if USE_NORMAL_MAPPING
  vec4 normal;
  // all your normal mapping code
#else
  vec4 normal = normalize(in_normal);
#endif

Большая проблема здесь заключается в обработке этого для предварительно скомпилированного HLSL, где вам нужно предварительно скомпилировать все используемые комбинации. Даже с GLSL вы должны иметь возможность правильно генерировать ключ всех используемых директив препроцессора, чтобы избежать перекомпиляции / кэширования идентичных шейдеров. Использование униформ может снизить сложность, но, в отличие от униформы препроцессора, не уменьшает количество команд и все же может оказывать незначительное влияние на производительность.

Просто чтобы прояснить, оба подхода (а также просто ручная запись тонны вариаций шейдеров) все используются в пространстве ААА. Используйте тот, который работает лучше для вас.

Шон Миддледич
источник