У меня есть большая функция со сложной математикой, и я хочу вызвать эту же функцию в нескольких фрагментных шейдерах. Нужно ли копировать-вставлять код функции в каждый шейдер? Или есть способ избежать этого, разделить код между шейдерами? Могу ли я иметь какую-либо библиотеку для общих функций шейдера?
WebGL2: одна и та же функция в разных шейдерах
Ответы (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);
В 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.