我经常发现自己在多个着色器之间粘贴粘贴代码。这包括在单个管道中的所有着色器之间共享的某些计算或数据,以及我所有的顶点着色器(或任何其他阶段)都需要的通用计算。
当然,这是可怕的做法:如果我需要在任何地方更改代码,则需要确保在其他地方进行更改。
有保持DRY的公认最佳实践吗?人们是否只是将一个通用文件添加到所有着色器?他们是否编写自己的基本C样式预处理程序来解析#include
指令?如果行业中存在可接受的模式,我想遵循它们。
我经常发现自己在多个着色器之间粘贴粘贴代码。这包括在单个管道中的所有着色器之间共享的某些计算或数据,以及我所有的顶点着色器(或任何其他阶段)都需要的通用计算。
当然,这是可怕的做法:如果我需要在任何地方更改代码,则需要确保在其他地方进行更改。
有保持DRY的公认最佳实践吗?人们是否只是将一个通用文件添加到所有着色器?他们是否编写自己的基本C样式预处理程序来解析#include
指令?如果行业中存在可接受的模式,我想遵循它们。
Answers:
有很多方法,但都不是完美的。
可以通过使用glAttachShader
合并着色器来共享代码,但这不能共享诸如struct声明或#define
-d常量之类的东西。它确实可以共享功能。
有些人喜欢使用传递给 glShaderSource
它作为在代码之前添加通用定义的一种方式,但这有一些缺点:
#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
扩展名,但有一些缺点:
"/buffers.glsl"
(在中使用#include "/buffers.glsl"
)对应于文件的内容buffer.glsl
(之前已加载)相对应。"/"
,例如Linux风格的绝对路径。这种表示法通常对C程序员并不熟悉,意味着您无法指定相对路径。常见的设计是实现自己的#include
机制,但这可能很棘手,因为您还需要解析(和评估)其他预处理器指令,例如#if
以便正确处理条件编译(例如标头保护)。
如果您自己实现#include
,则在实现方式上也有一些自由:
GL_ARB_shading_language_include
)。为简化起见,您可以为预处理层中的每个include自动插入标头保护,因此您的处理器层如下所示:
if (#include and not_included_yet) include_file();
(感谢Trent Reed向我展示了上述技术。)
总之,没有自动,标准和简单的解决方案。在将来的解决方案中,您可以使用一些SPIR-V OpenGL接口,在这种情况下,GLSL到SPIR-V的编译器可能不在GL API的范围内。将编译器置于OpenGL运行时之外可以极大地简化实现过程,#include
因为它是与文件系统进行连接的更合适的位置。我相信当前广泛使用的方法是仅实现一个自定义预处理器,该预处理器以任何C程序员都应该熟悉的方式工作。
我通常只是使用glShaderSource(...)接受字符串数组作为输入的事实。
我使用了一个基于json的着色器定义文件,该文件指定了着色器(或更正确的程序)的组成方式,并在其中指定了我可能需要的预处理器定义,使用的制服,顶点/片段着色器文件,以及所有其他“依赖项”文件。这些只是在实际着色器源之前附加到源的函数集合。
仅添加AFAIK,虚幻引擎4使用了一个#include指令,该指令将被解析并按照您的建议在编译之前附加所有相关文件。
我不认为有一个通用约定,但是如果我猜想的话,我想说几乎每个人都将某种形式的文本包含作为预处理步骤(#include
扩展)来实现,因为它很容易做到所以。(例如,在JavaScript / WebGL中,您可以使用简单的正则表达式来完成此操作)。这样做的好处是,当不再需要更改着色器代码时,可以在脱机步骤中对“发布”版本执行预处理。
实际上,这种方法很常见的迹象是为此引入了ARB扩展:GL_ARB_shading_language_include
。我不确定这是否现在成为核心功能,但是扩展是针对OpenGL 3.2编写的。
有人已经指出glShaderSource
可以采用字符串数组。
最重要的是,在GLSL中,着色器的编译(glShaderSource
,glCompileShader
)和链接(glAttachShader
,glLinkProgram
)是分开的。
在某些项目中,我曾使用它在特定部分和大多数着色器共有的部分之间划分着色器,然后将其编译并与所有着色器程序共享。这行得通,而且并不难实现:您只需维护一个依赖项列表。
不过,就可维护性而言,我不确定这是不是胜利。观察结果相同,让我们尝试分解。尽管确实避免了重复,但是该技术的开销让人感觉很重要。而且,最终的着色器更难以提取:您不能仅连接着色器源,因为声明以某些编译器将拒绝或重复的顺序结束。因此,在单独的工具中进行快速着色器测试变得更加困难。
最后,该技术解决了一些DRY问题,但远非理想。
在一个副题上,我不确定这种方法对编译时间是否有影响?我读过一些驱动程序仅在链接时才真正编译着色器程序,但我没有测量。