WebGL2: одна и та же функция в разных шейдерах

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


person eruditor    schedule 27.06.2020    source источник
comment
HLSL позволяет #include использовать общий код. Для webgl хак, который приходит на ум, — это включение PHP.   -  person mukunda    schedule 27.06.2020


Ответы (2)


Способ обмена кодом в шейдерах в WebGL — это манипулирование строками. Пример

const hsv2rgb = `

vec3 hsv2rgb(vec3 c) {
  c = vec3(c.x, clamp(c.yz, 0.0, 1.0));
  vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
  vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
  return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
`;

const fragmentShader1 = `#version 300 es

${hsv2rgb}

in float hue;
out vec4 color;

void main() {
  color = vec4(hsv2Rgb(vec3(hue, 1.0, 0.8)), 1);
}

const fragmentShader2 = `#version 300 es

${hsv2rgb}

in vec3 hsv;
out vec4 color;

void main() {
  color = vec4(hsv2Rgb(hsv), 1);
}
`;

нет необходимости в библиотеке, поскольку это тривиально. Пример

Пример

const snippets = {
  hsv2rgb: `...code-from-above--...`,
  rgb2hsv: `...some code ...`,
};

теперь просто используйте фрагменты

const fragmentShader2 = `#version 300 es

${snippets.hsv2rgb}
${snippets.rgb2hsv}

in vec3 v_color;
out vec4 color;

void main() {
  vec3 hsv = rgb2hsv(v_color);
  color = vec4(hsv2Rgb(hsv + vec3(0.5, 0, 0), 1);
}
`;

Хотя я бы рекомендовал не использовать объект для сбора строк, поскольку любой используемый вами конструктор может не иметь возможности отбрасывать неиспользуемые фрагменты.

Для организации вы можете использовать импорт es6

/* hsv2rgb.glsl.js */
export default `

vec3 hsv2rgb(vec3 c) {
  c = vec3(c.x, clamp(c.yz, 0.0, 1.0));
  vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
  vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
  return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
`; 

И тогда вы можете импортировать

/* somefragshader.glsl.js */
import hsv2rgb from './hsv2rgb.glsl.js';
export default `#version 300 es

${hsv2rgb}

in vec3 hsv;
out vec4 color;

void main() {
  color = vec4(hsv2Rgb(hsv), 1);
}
`;

А потом использовать в какой-нибудь программе

import someFragmentShaderSource from './somefragmentshader.glsl.js';
...
...compile shader using someFragmentShaderSource ...

Если вам не нравится использовать подстановку строки шаблона, тривиально сделать свой собственный

const subs = {
  hsv2rgb: `...code-from-above--...`,
  rgb2hsv: `...some code ...`,
};

// replace `#include <name>` with named sub
function replaceSubs(str, subs) {
  return str.replace(/#include\s+<(\w+)>/g, (m, key) => {
    return subs[key];
  });
}

а потом

const fragmentShader2 = replaceSubs(`#version 300 es

#include <hsv2rgb>

in vec3 hsv;
out vec4 color;

void main() {
  color = vec4(hsv2Rgb(hsv), 1);
}
`, snippets);
person gman    schedule 29.06.2020

В OpenGL / WebGL код GLSL передается просто как текст. Так что, если у вас есть функция, которую можно повторно использовать в нескольких программах GLSL, вы можете написать менеджер шейдеров, который будет объединять различные блоки шейдеров.

Существует несколько распространенных подходов:

  • Мегашейдер, используемый всеми вашими программами GLSL, с #ifdefs в коде для активации/деактивации определенных блоков. Может стать очень грязным.
  • Диспетчер шейдеров динамически создает программы GLSL из строковых констант (например, генерация кода).
  • Диспетчер шейдеров заменяет подстроки предопределенным списком стандартных функций. Базовый синтаксис GLSL не поддерживает директивы #include, но Shader Manager может реализовать их или использовать другой синтаксис для определения подстрок для замены, например %ColorLighting% (или просто использовать ${theVariable} в случае JavaScript).

Чтобы пример на JavaScript мог выглядеть так:

// reusable GLSL functions
var getColor_Red = "vec4 getColor() { return vec4(1.0, 0.0, 0.0, 1.0); }\n"
// fragment shader generator
function getFragShaderRed() {
  return "precision highp float;\n"
        + getColor_Red
        + "void main() { gl_FragColor = getColor(); }";
}

Подробный ответ, охватывающий также случай, отличный от WebGL, приведен ниже.

Desktop OpenGL обеспечивает большую гибкость в этом контексте — он позволяет подключать несколько шейдеров для одной и той же сцены к одной программе GLSL. Это означает, что выделенная функция может быть перемещена в выделенный шейдер, повторно использована в других шейдерах с помощью прямого объявления функции без тела и связана в нескольких программах GLSL - аналогично тому, как обычно компилируются и линкуются программы C++.

const GLchar* aShader1Text =
  "vec4 getColor() { return vec4(1.0, 0.0, 0.0, 1.0); }";
GLuint aShader1Id = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(aShader1Id, 1, &aShader1Text, NULL);
glCompileShader(aShader1Id);

const GLchar* aShader2Text =
  "vec4 getColor();" // forward declaration
  "void main() { gl_FragColor = getColor(); }"
GLuint aShader2Id = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(aShader2Id, 1, &aShader2Text, NULL);
glCompileShader(aShader2Id);

GLuint aProgramID = glCreateProgram();
glAttachShader (aProgramID, aShader0Id); // some vertex shader
glAttachShader (aProgramID, aShader1Id); // fragment shader block 1
glAttachShader (aProgramID, aShader2Id); // fragment shader block 2
glLinkProgram (aProgramID);

У этой функции есть две проблемы:

  • В отличие от программ C++, драйверы OpenGL обычно не компилируют отдельные объекты шейдера, а скорее проверяют их синтаксис, в то время как реальная компиляция выполняется на этапе компоновки всей программы GLSL. Это практически сводит на нет все преимущества компиляции отдельных блоков GLSL по сравнению с конкатенацией строк и повторной компиляцией всего исходного кода программы GLSL (например, с точки зрения производительности).
  • OpenGL ES и WebGL просто удалили эту функцию из своей спецификации, так что переносимая программа не может полагаться на эту функцию, доступную в десктопном OpenGL (с самого начала появления GLSL). Сам API такой же, но драйвер OpenGL не сможет скомпилировать шейдер GLSL без функции main().

Desktop OpenGL 4.0 представил еще одну функциональность подпрограммы шейдера, которая дает большую гибкость в определении программы GLSL, делая его настраиваемым во время выполнения. Этот довольно сложный функционал вряд ли разумен для статических GLSL-программ, а также недоступен в OpenGL ES/WebGL.

person gkv311    schedule 27.06.2020
comment
Этот ответ не имеет отношения к WebGL. - person gman; 29.06.2020