WebGL2:不同着色器中的相同功能

WebGL2: same function in different shaders

我有一个包含复杂数学的大函数,我想在多个片段着色器中调用同一个函数。我是否必须将函数代码复制粘贴到每个着色器?或者有什么办法可以避免这种情况,在着色器之间共享代码?我可以拥有任何类型的通用着色器函数的“库”吗?

OpenGL / WebGL 中,GLSL 代码仅作为文本传递。因此,如果您有一个可在多个 GLSL 程序中重用的函数 - 您可以编写一个着色器管理器来连接各种着色器块。

有几种常见的方法:

  • Mega Shader 由所有 GLSL 程序共享,#ifdefs 在代码中 activate/deactivate 特定块。可能会变得很乱。
  • Shader Manager 从字符串常量动态构造 GLSL 程序(如代码生成)。
  • Shader Manager 用预定义的标准函数列表替换子字符串。核心 GLSL 语法不支持 #include 指令,但着色器管理器可能会实现它们或使用另一种语法来识别要替换的子字符串,例如 %ColorLighting%(或者在 [= 的情况下仅使用 ${theVariable} 68=]).

因此 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 ESWebGL 刚刚从其规范中删除了此功能,因此可移植程序无法依赖桌面 OpenGL 中提供的此功能(从 GLSL 介绍的一开始)。 API 本身是相同的,但是如果没有 main() 函数,OpenGL 驱动程序将无法编译 GLSL 着色器。

Desktop OpenGL 4.0 引入了另一个功能 shader subroutines,它为 GLSL 程序定义提供了更大的灵活性,使其在运行时可配置。这是相当复杂的功能,对于静态 GLSL 程序来说不太合理,而且在 OpenGL ES / WebGL 中也不可用。

在 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);