There's a bunch of a approaches, but none is perfect.
It's possible to share code by using glAttachShader
to combine shaders, but this doesn't make it possible to share things like struct declarations or #define
-d constants. It does work for sharing functions.
Some people like to use the array of strings passed to glShaderSource
as a way to prepend common definitions before your code, but this has some disadvantages:
- It's harder to control what needs to be included from within the shader (you need a separate system for this.)
- It means the shader author cannot specify the GLSL
#version
, due to the following statement in the GLSL spec:
The #version directive must occur in a shader before anything else, except for comments and white space.
Due to this statement, glShaderSource
cannot be used to prepend text before the #version
declarations. This means that the #version
line needs to be included in your glShaderSource
arguments, which means that your GLSL compiler interface needs to somehow be told what version of GLSL is expected to be used. Additionally, not specifying a #version
will make the GLSL compiler default to using GLSL version 1.10. If you want to let shader authors specify the #version
within the script in a standard way, then you need to somehow insert #include
-s after the #version
statement. This could be done by explicitly parsing the GLSL shader to find the #version
string (if present) and make your inclusions after it, but having access to an #include
directive might be preferable to control more easily when those inclusions need to be made. On the other hand, since GLSL ignores comments before the #version
line, you could add metadata for includes within comments at the top of your file (yuck.)
The question now is: Is there a standard solution for #include
, or do you need to roll your own preprocessor extension?
There is the GL_ARB_shading_language_include
extension, but it has some drawbacks:
- It is only supported by NVIDIA (http://delphigl.de/glcapsviewer/listreports2.php?listreportsbyextension=GL_ARB_shading_language_include)
- It works by specifying the include strings ahead of time. Therefore, before compiling, you need to specify that the string
"/buffers.glsl"
(as used in #include "/buffers.glsl"
) corresponds to the contents of the file buffer.glsl
(which you have loaded previously).
- As you may have noticed in point (2), your paths need to start with
"/"
, like Linux-style absolute paths. This notation is generally unfamiliar to C programmers, and means you can't specify relative paths.
A common design is to implement your own #include
mechanism, but this can be tricky since you also need to parse (and evaluate) other preprocessor instructions like #if
in order to properly handle conditional compilation (like header guards.)
If you implement your own #include
, you also have some liberties in how you want to implement it:
- You could pass strings ahead of time (like
GL_ARB_shading_language_include
).
- You could specify an include callback (this is done by DirectX's D3DCompiler library.)
- You could implement a system that always reads directly from the filesystem, as done in typical C applications.
As a simplification, you can automatically insert header guards for each include in your preprocessing layer, so your processor layer looks like:
if (#include and not_included_yet) include_file();
(Credit to Trent Reed for showing me the above technique.)
In conclusion, there exists no automatic, standard, and simple solution. In a future solution, you could use some SPIR-V OpenGL interface, in which case the GLSL to SPIR-V compiler could be outside of the GL API. Having the compiler outside the OpenGL runtime greatly simplifies implementing things like #include
since it's a more appropriate place to interface with the filesystem. I believe the current widespread method is to just implement a custom preprocessor that works in a way any C programmer should be familiar with.