在多个GLSL着色器之间共享代码


30

我经常发现自己在多个着色器之间粘贴粘贴代码。这包括在单个管道中的所有着色器之间共享的某些计算或数据,以及我所有的顶点着色器(或任何其他阶段)都需要的通用计算。

当然,这是可怕的做法:如果我需要在任何地方更改代码,则需要确保在其他地方进行更改。

有保持DRY的公认最佳实践吗?人们是否只是将一个通用文件添加到所有着色器?他们是否编写自己的基本C样式预处理程序来解析#include指令?如果行业中存在可接受的模式,我想遵循它们。


4
这个问题可能会引起争议,因为其他几个SE站点不想要有关最佳实践的问题。旨在了解该社区在此类问题上的立场。
马丁·恩德

2
嗯,对我来说很好。我想说我们的问题在很大程度上要比StackOverflow的“更广泛” /“更笼统”。
克里斯说,请在2015年

2
StackOverflow从“问我们”变成了“不要问我们,除非您必须请”。
Insidesin 2015年

如果要确定话题性,那么相关的元问题怎么样?
SL Barth-恢复莫妮卡2015年

Answers:


18

有很多方法,但都不是完美的。

可以通过使用glAttachShader合并着色器来共享代码,但这不能共享诸如struct声明或#define-d常量之类的东西。它确实可以共享功能。

有些人喜欢使用传递给 glShaderSource它作为在代码之前添加通用定义的一种方式,但这有一些缺点:

  1. 难以控制需要从着色器中包含什么(为此您需要一个单独的系统。)
  2. 这意味着着色器作者#version由于GLSL规范中的以下声明而无法指定GLSL:

的#Version指令必须在发生任何事情之前着色器,除了注释和空白。

由于此语句,glShaderSource不能用于在#version声明之前添加文本。这意味着该#version行需要包含在您的glShaderSource参数中,这意味着需要以某种方式告知您的GLSL编译器接口,希望使用哪个版本的GLSL。此外,不指定a #version将使GLSL编译器默认使用GLSL 1.10版。如果要让着色器作者#version以标准方式在脚本中指定,则需要以某种方式#include#version语句后插入-s 。可以通过显式解析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风格的绝对路径。这种表示法通常对C程序员并不熟悉,意味着您无法指定相对路径。

常见的设计是实现自己的#include机制,但这可能很棘手,因为您还需要解析(和评估)其他预处理器指令,例如#if以便正确处理条件编译(例如标头保护)。

如果您自己实现#include,则在实现方式上也有一些自由:

  • 您可以提前传递字符串(如GL_ARB_shading_language_include)。
  • 您可以指定一个include回调(由DirectX的D3DCompiler库完成。)
  • 您可以实现一个系统,该系统始终像典型的C应用程序一样直接从文件系统读取。

为简化起见,您可以为预处理层中的每个include自动插入标头保护,因此您的处理器层如下所示:

if (#include and not_included_yet) include_file();

(感谢Trent Reed向我展示了上述技术。)

总之,没有自动,标准和简单的解决方案。在将来的解决方案中,您可以使用一些SPIR-V OpenGL接口,在这种情况下,GLSL到SPIR-V的编译器可能不在GL API的范围内。将编译器置于OpenGL运行时之外可以极大地简化实现过程,#include因为它是与文件系统进行连接的更合适的位置。我相信当前广泛使用的方法是仅实现一个自定义预处理器,该预处理器以任何C程序员都应该熟悉的方式工作。


着色器也可以使用glslify分成模块,尽管它仅与node.js一起使用。
Anderson Green

9

我通常只是使用glShaderSource(...)接受字符串数组作为输入的事实。

我使用了一个基于json的着色器定义文件,该文件指定了着色器(或更正确的程序)的组成方式,并在其中指定了我可能需要的预处理器定义,使用的制服,顶点/片段着色器文件,以及所有其他“依赖项”文件。这些只是在实际着色器源之前附加到源的函数集合。

仅添加AFAIK,虚幻引擎4使用了一个#include指令,该指令将被解析并按照您的建议在编译之前附加所有相关文件。


4

我不认为有一个通用约定,但是如果我猜想的话,我想说几乎每个人都将某种形式的文本包含作为预处理步骤(#include扩展)来实现,因为它很容易做到所以。(例如,在JavaScript / WebGL中,您可以使用简单的正则表达式来完成此操作)。这样做的好处是,当不再需要更改着色器代码时,可以在脱机步骤中对“发布”版本执行预处理。

实际上,这种方法很常见的迹象是为此引入了ARB扩展:GL_ARB_shading_language_include。我不确定这是否现在成为核心功能,但是扩展是针对OpenGL 3.2编写的。


2
GL_ARB_shading_language_include不是核心功能。实际上,只有NVIDIA支持它。(delphigl.de/glcapsviewer/…)–
尼古拉斯·路易斯·

4

有人已经指出glShaderSource可以采用字符串数组。

最重要的是,在GLSL中,着色器的编译(glShaderSourceglCompileShader)和链接(glAttachShaderglLinkProgram)是分开的。

在某些项目中,我曾使用它在特定部分和大多数着色器共有的部分之间划分着色器,然后将其编译并与所有着色器程序共享。这行得通,而且并不难实现:您只需维护一个依赖项列表。

不过,就可维护性而言,我不确定这是不是胜利。观察结果相同,让我们尝试分解。尽管确实避免了重复,但是该技术的开销让人感觉很重要。而且,最终的着色器更难以提取:您不能仅连接着色器源,因为声明以某些编译器将拒绝或重复的顺序结束。因此,在单独的工具中进行快速着色器测试变得更加困难。

最后,该技术解决了一些DRY问题,但远非理想。

在一个副题上,我不确定这种方法对编译时间是否有影响?我读过一些驱动程序仅在链接时才真正编译着色器程序,但我没有测量。


根据我的理解,我认为这不能解决共享结构定义的问题。
Nicolas Louis Guillemot

@NicolasLouisGuillemot:是的,您是对的,仅以这种方式共享指令代码,而不是声明。
Julien Guertault
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.