Совместное использование кода между несколькими шейдерами GLSL

30

Я часто сталкиваюсь с копированием кода между несколькими шейдерами. Это включает в себя как определенные вычисления или данные, используемые всеми шейдерами в одном конвейере, так и общие вычисления, которые нужны всем моим вершинным шейдерам (или любой другой стадии).

Конечно, это ужасная практика: если мне нужно где-то изменить код, я должен убедиться, что я изменю его везде.

Есть ли лучшая практика для сохранения СУХОГО ? Люди просто добавляют один общий файл ко всем своим шейдерам? Они пишут свой собственный элементарный препроцессор в стиле C, который анализирует #includeдирективы? Если в отрасли есть общепринятые модели, я бы хотел следовать им.

Мартин Эндер
источник
4
Этот вопрос может быть немного спорным, потому что некоторые другие сайты SE не хотят вопросов о передовой практике. Это намеренно, чтобы увидеть, как это сообщество относится к таким вопросам.
Мартин Эндер
2
Хм, выглядит хорошо для меня. Я бы сказал, что мы в большей степени немного «шире» / «более общие» в наших вопросах, чем, скажем, StackOverflow.
Крис говорит восстановить Монику
2
StackOverflow превратился из форума «спросите нас» в раздел «не спрашивайте нас, если вам не нужно, пожалуйста».
insidesin
Если это предназначено для определения предметности, то как насчет связанного вопроса Мета?
SL Barth - Восстановить Монику
2
Обсуждение мета.
Мартин Эндер

Ответы:

18

Есть куча подходов, но ни один не идеален.

Можно совместно использовать код, используя glAttachShaderдля объединения шейдеров, но это не позволяет совместно использовать такие вещи, как объявления структуры или #defineконстанты -d. Это работает для обмена функциями.

Некоторым людям нравится использовать массив строк, переданных glShaderSourceкак способ добавления общих определений перед вашим кодом, но у этого есть некоторые недостатки:

  1. Сложнее контролировать то, что должно быть включено из шейдера (для этого вам нужна отдельная система).
  2. Это означает, что автор шейдера не может указать GLSL #versionиз-за следующего утверждения в спецификации GLSL:

#Version директива должна произойти в шейдере , прежде чем что -то еще, для комментариев и белого пространства , за исключением.

Из-за этого заявления, glShaderSourceне может использоваться для добавления текста перед #versionобъявлениями. Это означает, что #versionв ваши glShaderSourceаргументы должна быть включена строка , что означает, что интерфейсу компилятора GLSL нужно как-то сказать, какую версию GLSL предполагается использовать. Кроме того, отсутствие указания #versionсделает компилятор GLSL по умолчанию для использования GLSL версии 1.10. Если вы хотите, чтобы авторы шейдеров указывали #versionвнутри скрипта стандартным способом, то вам нужно как-то вставить #include-s после #versionоператора. Это можно сделать, явно проанализировав шейдер GLSL, чтобы найти #versionстроку (если она есть) и сделать ваши включения после нее, но имея доступ к#includeДиректива может быть предпочтительнее контролировать легче, когда эти включения должны быть сделаны. С другой стороны, поскольку GLSL игнорирует комментарии перед #versionстрокой, вы можете добавить метаданные для включений в комментарии в верхней части вашего файла (черт.)

Теперь возникает вопрос: есть ли стандартное решение для #includeили вам нужно накатить собственное расширение препроцессора?

Существует GL_ARB_shading_language_includeрасширение, но она имеет некоторые недостатки:

  1. Он поддерживается только NVIDIA ( http://delphigl.de/glcapsviewer/listreports2.php?listreportsbyextension=GL_ARB_shading_language_include )
  2. Это работает, определяя строки включения заранее. Поэтому перед компиляцией необходимо указать, что строка "/buffers.glsl"(как используется в #include "/buffers.glsl") соответствует содержимому файла buffer.glsl(который вы загрузили ранее).
  3. Как вы, возможно, заметили в пункте (2), ваши пути должны начинаться с "/"абсолютных путей в стиле Linux. Эта нотация обычно незнакома программистам на Си и означает, что вы не можете указать относительные пути.

Обычный дизайн заключается в реализации собственного #includeмеханизма, но это может быть непросто, поскольку вам также необходимо проанализировать (и оценить) другие инструкции препроцессора, например #if, для правильной обработки условной компиляции (например, защиты заголовка).

Если вы реализуете свой собственный #include, у вас также есть некоторые свободы в том, как вы хотите его реализовать:

  • Вы можете передать строки раньше времени (как GL_ARB_shading_language_include).
  • Вы можете указать обратный вызов включения (это делается библиотекой DirectX D3DCompiler).
  • Вы могли бы реализовать систему, которая всегда читает непосредственно из файловой системы, как это делается в типичных приложениях Си.

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

if (#include and not_included_yet) include_file();

(Благодарим Трента Рида за показ вышеописанной техники.)

В заключение , не существует автоматического, стандартного и простого решения. В будущем решении вы могли бы использовать некоторый интерфейс SPIR-V OpenGL, и в этом случае компилятор GLSL-SPIR-V может находиться за пределами GL API. Наличие компилятора вне среды выполнения OpenGL значительно упрощает реализацию таких вещей, как, #includeпоскольку это более подходящее место для взаимодействия с файловой системой. Я полагаю, что в настоящее время широко распространенный метод состоит в том, чтобы просто реализовать собственный препроцессор, который работает так, как должен быть знаком любой программист на Си.

Николя Луи Гийемот
источник
Шейдеры также можно разделить на модули с помощью glslify , хотя он работает только с node.js.
Андерсон Грин
9

Обычно я просто использую тот факт, что glShaderSource (...) принимает массив строк в качестве входных данных.

Я использую основанный на json файл определения шейдера, который определяет, как составляется шейдер (или программа, если быть более точным), и там я указываю, что препроцессор определяет, какие мне могут понадобиться, формы, которые он использует, файл шейдеров вершин / фрагментов, и все дополнительные файлы "зависимости". Это просто наборы функций, которые добавляются к источнику перед фактическим источником шейдера.

Просто добавьте, AFAIK, что Unreal Engine 4 использует директиву #include, которая анализируется и добавляет все соответствующие файлы перед компиляцией, как вы и предлагали.

Маттео Бертелло
источник
4

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

На самом деле, признак того, что этот подход является общим является тот факт , что расширение АРБ было введено для этого: GL_ARB_shading_language_include. Я не уверен, стало ли это основной функцией на данный момент или нет, но расширение было написано для OpenGL 3.2.

glampert
источник
2
GL_ARB_shading_language_include не является основной функцией. Фактически, только NVIDIA поддерживает это. ( delphigl.de/glcapsviewer/… )
Николас Луи Гийемот
4

Некоторые люди уже указали, что glShaderSourceмогут принимать массив строк.

Кроме того, в GLSL компиляция ( glShaderSource, glCompileShader) и компоновка ( glAttachShader, glLinkProgram) шейдера разделены.

Я использовал это в некоторых проектах для разделения шейдеров между определенной частью и частями, общими для большинства шейдеров, которые затем компилируются и передаются всем программам шейдеров. Это работает и не сложно реализовать: вам просто нужно поддерживать список зависимостей.

Что касается ремонтопригодности, я не уверен, что это победа. Наблюдение было то же самое, давайте попробуем факторизовать. Хотя это действительно позволяет избежать повторений, накладные расходы на технику кажутся значительными. Более того, последний шейдер сложнее извлечь: вы не можете просто объединить источники шейдеров, так как объявления заканчиваются в порядке, который некоторые компиляторы отклонят или будут дублированы. Таким образом, это затрудняет проведение быстрого теста шейдера в отдельном инструменте.

В конце концов, эта техника решает некоторые проблемы сушки, но она далека от идеала.

Что касается побочной темы, я не уверен, оказывает ли этот подход какое-либо влияние с точки зрения времени компиляции; Я читал, что некоторые драйверы действительно компилируют шейдерную программу только при линковке, но я не измерял.

Жюльен Геро
источник
Насколько я понимаю, я думаю, что это не решает проблему разделения структурных определений.
Николя Луи Гийемот
@NicolasLouisGuillemot: да, вы правы, таким образом передается только код инструкции, а не объявления.
Жюльен Геро,