Подтвердить что ты не робот

Практическое использование GLSL-шейдеров с помощью программного обеспечения на С++

Во время инициализации OpenGL программа должна делать что-то вроде:

<Get Shader Source Code>
<Create Shader>
<Attach Source Code To Shader>
<Compile Shader>

Получение исходного кода может быть таким же простым, как положить его в строку типа: (Пример из SuperBible, 6-е издание)

static const char * vs_source[] =
{
    "#version 420 core                             \n"
    "                                              \n"
    "void main(void)                               \n"
    "{                                             \n"
    "    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);   \n"
    "}                                             \n"
};

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

std::ifstream vertexShaderFile("vertex.glsl");
std::ostringstream vertexBuffer;
vertexBuffer << vertexShaderFile.rdbuf();
std::string vertexBufferStr = vertexBuffer.str();
// Warning: safe only until vertexBufferStr is destroyed or modified
const GLchar *vertexSource = vertexBufferStr.c_str();

Теперь проблема заключается в том, как отправить шейдеры с вашей программой? Действительно, исходный код доставки с вашим приложением может быть проблемой. OpenGL поддерживает "предварительно скомпилированные двоичные шейдеры", но Open Wiki утверждает, что:

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

Как практически поставлять шейдеры GLSL с вашим программным обеспечением на С++?

4b9b3361

Ответ 1

Существует только "хранить их непосредственно в исполняемом файле" или "хранить их в (a) отдельном файле (файлах)", при этом ничего не происходит между ними. Если вам нужен автономный исполняемый файл, то вставить его в двоичный файл - хорошая идея. Обратите внимание, что вы можете добавлять их в качестве ресурсов или настраивать свою систему сборки, чтобы вставлять шейдерные строки из отдельных файлов разработки в исходные файлы, чтобы упростить разработку (с возможностью добавления возможности напрямую загружать отдельные файлы в сборках разработки).

Почему вы думаете, что доставка источников шейдеров будет проблемой? Другого пути в GL нет. Предварительно скомпилированные двоичные файлы полезны только для кэширования результатов компиляции на целевой машине. Благодаря быстрым достижениям технологии GPU и изменениям архитектуры GPU, а также различным поставщикам с полностью несовместимыми ISA, предварительно скомпилированные двоичные файлы шейдеров не имеют никакого смысла.

Обратите внимание, что размещение ваших шейдерных источников в исполняемом файле не "защищает" их, даже если вы их зашифруете. Пользователь все еще может подключиться к библиотеке GL и перехватить источники, которые вы указываете GL. И отладчики GL там делают именно это.

ОБНОВЛЕНИЕ 2016

На SIGGRAPH 2016 в обзоре по обзору архитектуры OpenGL было выпущено расширение GL_ARB_gl_spirv. Это позволит использовать GL для использования SPIRV двоичного промежуточного языка. Это имеет некоторые потенциальные преимущества:

  • Шейдеры могут быть предварительно "скомпилированы" в автономном режиме (окончательная компиляция для целевого GPU по-прежнему происходит с драйвером позже). Вам не нужно отправлять исходный код шейдера, но только двоичное промежуточное представление.
  • Существует один стандартный интерфейс компилятора (glslang), который выполняет синтаксический разбор, поэтому различия между анализаторами различных реализаций могут быть устранены.
  • Можно добавить дополнительные шейдерные языки без необходимости изменения реализаций GL.
  • Это несколько увеличивает переносимость к вулкану.

С этой схемой GL становится более похожим на D3D и Vulkan в этом отношении. Однако он не меняет большую картину. Байт-код SPIRV все еще может быть перехвачен, разобран и декомпилирован. Это делает обратное проектирование немного сложнее, но не на самом деле. В шейдере вы обычно не можете позволить себе обширные меры по запуску, так как это значительно снижает производительность, что противоречит тому, для чего нужны шейдеры.

Также имейте в виду, что это расширение широко не доступно сейчас (осень 2016 года). И Apple перестала поддерживать GL после 4.1, поэтому это расширение, вероятно, никогда не придет в OSX.

MINOR UPDATE 2017

GL_ARB_gl_spirv теперь является официальной основной особенностью OpenGL 4.6, так что мы можем ожидать роста скорости принятия этой функции, но она не меняет большую картину.

Ответ 2

С помощью С++ 11 вы также можете использовать новую функцию для строковых литералов. Поместите этот исходный код в отдельный файл с именем shader.vs:

R"(
#version 420 core

void main(void)
{
    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)"

а затем импортируйте его в виде строки следующим образом:

const std::string vs_source =
#include "shader.vs"
;

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

Единственный недостаток, который я вижу, - это добавленные строки в верхней и нижней части файла (R") и )") и синтаксис, немного странный для получения строки в С++-коде.

Ответ 3

OpenGL поддерживает предварительно скомпилированные двоичные файлы, но не переносимо. В отличие от HLSL, который скомпилирован в стандартный формат bytcode компилятором Microsoft, а затем переведенный в собственную инструкцию GPU, установленную драйвером, OpenGL не имеет такого формата. Вы не можете использовать предварительно скомпилированные двоичные файлы для чего-то большего, чем кеширование скомпилированных шейдеров GLSL на одной машине, чтобы ускорить время загрузки, и даже тогда нет гарантии, что скомпилированный двоичный файл будет работать, если версия драйвера изменится... тем более фактический графический процессор на аппарате изменяется.

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

Если вы обеспокоены тем, что люди меняют ваше программное обеспечение, я предлагаю вам выполнить простой хеш-тест или контрольную сумму. Многие игры уже делают это, чтобы предотвратить мошенничество, насколько вы хотите принять его, зависит от вас. Но суть в том, что двоичные шейдеры в OpenGL предназначены для сокращения времени компиляции шейдеров, а не для переносимого перераспределения.

Ответ 4

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

#pragma once
enum ShaderResource {
    LIT_VS,
    LIT_FS,
    // ... 
    NO_SHADER
};

const std::string & getShaderPath(ShaderResource shader);

Аналогично, CMake создает CPP файл, который при задании ресурса возвращает путь к шейдеру.

const string & getShaderPath(ShaderResource res) {
  static map<ShaderResource, string> fileMap;
  static bool init = true;
  if (init) {
   init = false;
   fileMap[LIT_VS] =
    "C:/Users/bdavis/Git/OculusRiftExamples/source/common/Lit.vs";
   // ...
  }
  return fileMap[res];
}

Было бы не слишком сложно (много поворота здесь), чтобы заставить CMake script изменить его поведение, чтобы в сборке релиза вместо предоставления пути к файлу он предоставлял источник шейдера и в файле cpp хранит содержимое самих шейдеров (или в случае целевого объекта Windows или Apple делает их частью исполняемого ресурса/исполняемого пакета).

Преимущество такого подхода заключается в том, что гораздо легче модифицировать шейдеры "на лету" во время отладки, если они не запекаются в исполняемый файл. Фактически, мой код GLSL, получающий код, фактически просматривает время компиляции шейдера по сравнению с измененными временными метками исходных файлов и перезагружает шейдер, если файлы были изменены с момента последнего компиляции (это все еще находится в зачаточном состоянии, поскольку это означает, что вы потеряете любую униформу, которая ранее была привязана к шейдеру, но я над этим работаю).

Это действительно меньше проблемы с шейдером, чем общие проблемы с не-С++. Та же проблема существует со всем, что вы можете захотеть загрузить и обработать... изображения для текстур, звуковых файлов, уровней, что у вас есть.

Ответ 5

Проблема в том, что ее трудно редактировать, отлаживать и поддерживать GLSL шейдеров непосредственно в строке.

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

Ответ на упрощение их редактирования при загрузке непосредственно из строки, прост. Рассмотрим следующий строковый литерал:

    const char* gonFrag1 = R"(#version 330
// Shader code goes here
// and newlines are fine, too!)";

Все остальные комментарии верны, насколько они подходят. Действительно, как говорится, лучшая доступная безопасность - это неясность, поскольку GL можно перехватить. Но чтобы честные люди честны, и чтобы наложить какой-то блок на случай случайного повреждения программы, вы можете сделать так, как указано выше на С++, и по-прежнему легко поддерживать свой код.

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

Ответ 6

Как альтернатива хранению шейдеров GLSL непосредственно в строке, я бы предложил рассмотреть эту библиотеку, которую я разрабатываю: ShaderBoiler (Apache-2.0).

Он находится в альфа-версии и имеет некоторые ограничения, которые могут ограничить его использование.

Основная идея заключается в написании в конструкциях С++, подобных GLSL-коду, который построил бы граф вычислений, из которого генерируется окончательный код GLSL.

Например, рассмотрим следующий код С++

#include <shaderboiler.h>
#include <iostream>

void main()
{
    using namespace sb;

    context ctx;
    vec3 AlbedoColor           = ctx.uniform<vec3>("AlbedoColor");
    vec3 AmbientLightColor     = ctx.uniform<vec3>("AmbientLightColor");
    vec3 DirectLightColor      = ctx.uniform<vec3>("DirectLightColor");
    vec3 LightPosition         = ctx.uniform<vec3>("LightPosition");

    vec3 normal   = ctx.in<vec3>("normal");
    vec3 position = ctx.in<vec3>("position");
    vec4& color   = ctx.out<vec4>("color");

    vec3 normalized_normal = normalize(normal);

    vec3 fragmentToLight = LightPosition - position;

    Float squaredDistance = dot(fragmentToLight, fragmentToLight);

    vec3 normalized_fragmentToLight = fragmentToLight / sqrt(squaredDistance);

    Float NdotL = dot(normal, normalized_fragmentToLight);

    vec3 DiffuseTerm = max(NdotL, 0.0) * DirectLightColor / squaredDistance;

    color = vec4(AlbedoColor * (AmbientLightColor + DiffuseTerm), 1.0);

    std::cout << ctx.genShader();
}

Выход на консоль будет:

uniform vec3 AlbedoColor;
uniform vec3 AmbientLightColor;
uniform vec3 LightPosition;
uniform vec3 DirectLightColor;

in vec3 normal;
in vec3 position;

out vec4 color;

void main(void)
{
        vec3 sb_b = LightPosition - position;
        float sb_a = dot(sb_b, sb_b);
        color = vec4(AlbedoColor * (AmbientLightColor + max(dot(normal, sb_b / sqrt(sb_a)), 0.0000000) * DirectLightColor / sb_a), 1.000000);
}

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

Ответ 7

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

См. http://www.gamedev.net/topic/651404-shaders-glsl-in-one-file-is-it-practical/

Ответ 8

Я не знаю, будет ли это работать, но вы можете вставить файл .vs в свой исполняемый файл с помощью binutils, например, как g2bin, и вы можете объявить свои шейдерные программы как внешние, тогда вы обращаетесь к ним как к нормальным ресурсам, встроенным в исполняемый файл. См. Qrc в Qt или вы можете просмотреть мою небольшую программу для встраивания содержимого в исполняемые файлы: https://github.com/heatblazer/binutil, который вызывается как команда предварительной сборки для IDE.

Ответ 9

Предложение:

В вашей программе установите шейдер в:

const char shader_code = {
#include "shader_code.data"
, 0x00};

В файле shader_code.data должен быть исходный код шейдера в виде списка o шестнадцатеричных чисел, разделенных запятыми. Эти файлы должны быть созданы до компиляции, используя ваш шейдерный код, обычно записываемый в файл. В Linux я бы поставил инструкции в Makefile для запуска кода:

cat shader_code.glsl | xxd -i > shader_code.data

Ответ 10

Другой альтернативой хранению текстовых файлов glsl или предварительно скомпилированных файлов glsl является генератор шейдеров, который принимает дерево оттенков в качестве входных данных и выводит код glsl (или hlsl,...), который затем компилируется и привязывается во время выполнения... Следуя этому подходу, вы можете более легко адаптироваться к любым возможностям, имеющимся в оборудовании gfx. Вы также можете поддерживать hlsl, если у вас много времени, нет необходимости в языке cg shading. Если вы думаете о glsl/hlsl достаточно глубоко, вы увидите, что преобразование деревьев тени в исходный код было в глубине умов разработчиков языка.