Полу распространенным подходом является создание того, что я называю компонентами шейдеров , аналогично тому, что я называю модулями.
Идея похожа на график пост-обработки. Вы пишете куски шейдерного кода, который включает в себя как необходимые входные данные, сгенерированные выходные данные, так и код, который фактически работает с ними. У вас есть список, в котором указано, какие шейдеры следует применять в любой ситуации (нужен ли этому материалу компонент рельефного отображения, включен ли компонент отложенного или прямого действия и т. Д.).
Теперь вы можете взять этот график и сгенерировать из него код шейдера. В основном это означает «вставку» кода чанков на место, с графиком, который гарантирует, что они уже находятся в нужном порядке, а затем вставку в шейдерные входы / выходы соответствующим образом (в 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 вы должны иметь возможность правильно генерировать ключ всех используемых директив препроцессора, чтобы избежать перекомпиляции / кэширования идентичных шейдеров. Использование униформ может снизить сложность, но, в отличие от униформы препроцессора, не уменьшает количество команд и все же может оказывать незначительное влияние на производительность.
Просто чтобы прояснить, оба подхода (а также просто ручная запись тонны вариаций шейдеров) все используются в пространстве ААА. Используйте тот, который работает лучше для вас.