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

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

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

评论

这个问题可能会引起争议,因为其他几个SE网站不希望遇到有关最佳做法的问题。旨在了解这个社区在此类问题上的立场。

嗯,对我来说很好。我说我们的问题在很大程度上要比StackOverflow的“更广泛” /“更笼统”。

StackOverflow从一个“问我们”变成了一个“除非您必须请别问我们”就登机了。

如果要确定主题,那么相关的元问题怎么样?

关于meta的讨论。

#1 楼

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

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

有些人喜欢使用传递给glShaderSource的字符串数组作为在代码之前添加通用定义的一种方法,但这有一些缺点: />
很难控制着色器中需要包含的内容(为此您需要一个单独的系统。)
这意味着着色器作者无法指定GLSL #version,这是因为GLSL规范:


#version指令必须先在着色器中出现,注释和空格除外。


此语句glShaderSource不能用于在#version声明之前添加文本。这意味着#version参数中需要包含glShaderSource行,这意味着需要以某种方式告知您的GLSL编译器接口将使用哪个版本的GLSL。此外,不指定#version将使GLSL编译器默认使用GLSL 1.10版。如果要让着色器作者以标准方式在脚本中指定#version,则需要以某种方式在#include语句后插入#version -s。可以通过显式解析GLSL着色器以找到#version字符串(如果存在)并在其之后进行包含,来完成此操作,但是在需要创建包含时,最好使用#include指令更容易地进行控制。另一方面,由于GLSL在#version行之前会忽略注释,因此您可以在文件顶部添加注释中的包含元数据(很糟糕)。 for #include,还是您需要推出自己的预处理程序扩展?

GL_ARB_shading_language_include扩展名,但有一些缺点:


仅NVIDIA支持(http://delphigl.de/glcapsviewer/listreports2.php?listreportsbyextension=GL_ARB_shading_language_include)
通过提前指定包含字符串来工作。因此,在编译之前,您需要指定字符串"/buffers.glsl"(在#include "/buffers.glsl"中使用)对应于文件buffer.glsl(您先前已加载)的内容。
您可能已经注意到在第(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)向我展示了上述te chnique。)

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

评论


$ \ begingroup $
也可以使用glslify将着色器分成模块,尽管它仅适用于node.js。
$ \ endgroup $
–安德森·格林(Anderson Green)
19年8月20日在18:34

#2 楼

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

我使用基于json的着色器定义文件,该文件指定了着色器(或程序如何(更正确)),然后在此处指定我可能需要的预处理器定义,其使用的制服,顶点/片段着色器文件以及所有其他“依赖”文件。
这些只是函数的集合被添加到实际着色器源之前。

只需添加AFAIK,虚幻引擎4就使用#include指令进行解析,并在编译之前将所有相关文件附加到您在暗示。

#3 楼

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

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

评论


$ \ begingroup $
GL_ARB_shading_language_include不是核心功能。实际上,只有NVIDIA支持它。 (delphigl.de/glcapsviewer / ...)
$ \ endgroup $
–尼古拉斯·路易斯·吉列莫特
15年8月8日在1:08

#4 楼

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

最重要的是,在GLSL中,着色器的编译(glShaderSourceglCompileShader)和链接(glAttachShaderglLinkProgram)是分开的。 br />
在某些项目中,我曾使用该方法在特定部分和大多数着色器共有的部分之间划分着色器,然后将其编译并与所有着色器程序共享。这行之有效,而且实施起来并不难:您只需要维护一个依赖项列表即可。观察结果相同,让我们尝试分解。尽管确实避免了重复,但该技术的开销却让人感到很大。而且,最终的着色器更难以提取:您不能仅将着色器源连接起来,因为声明以某些编译器将拒绝或重复的顺序结束。因此,在单独的工具中进行快速着色器测试变得更加困难。另外,我不确定这种方法是否会对编译时间产生影响;我已经读到一些驱动程序只能在链接时真正编译着色器程序,但是我还没有测量。

评论


$ \ begingroup $
根据我的理解,我认为这不能解决共享结构定义的问题。
$ \ endgroup $
–尼古拉斯·路易斯·吉列莫特
15年8月8日在17:35

$ \ begingroup $
@NicolasLouisGuillemot:是的,您是对的,仅以这种方式共享指令代码,而不是声明。
$ \ endgroup $
–朱利安·盖尔特(Julien Guertault)
2015年8月8日17:39