I’ve been toying with the idea of creating a “shader compositor”, but, given my relative inexperience with shaders, I’m not sure of whether it’s a good idea or not, and whether easier or better alternatives exist.
The idea is to address two issues: first, that GLSL lacks a built-in “#include” function, and second, the question of how to adjust shaders and their inputs for when in-game quality settings are adjusted.
The first issue can be dealt with, I gather, via a “#pragma include” statement that Panda provides; my idea thus leans more on the second point, with the “#include” functionality as a bonus.
With regards to in-game settings, then, my thought is that in some cases, one might want to swap out shaders when certain settings are adjusted. For example, if shadows are enabled when the game is started, but disabled during play, one might want to switch one’s object shaders from ones that include shadow-map projection to ones that don’t.
So, all that said, the idea is this:
The “shader compositor” would be an object that takes in shader “meta-files” that outline a shader by referring to shader-chunk files–essentially being a sequence of function-calls. The “meta-files” would then be used to load the relevant shader-chunk files and concatenate them into complete shaders, which would finally be loaded via Panda’s string-based version of “Shader.loadShader”. The compositor would keep a record of which shaders use which shader-chunks, in order to more easily update shaders when called for.
In addition, calls to “setShader” and “setShaderInput” would be routed through the compositor, allowing it to keep track of which objects use which shaders and inputs, and thus which should be updated when something is changed.
When a shader is changed–such as specifying that it should no longer use shadow-projection–the compositor would check which shaders are affected and then re-build them from their “meta-files” with the changes applied.
In terms of usage, it would replace calls to “Shader.loadShader”, “NodePath.setShader”, and “NodePath.setShaderInput”. Changing a shader or input might be done through a method-call; something along the lines of “ShaderCompositor.replaceSubShader(‘chunk-name’, ‘newShaderChunk’)” and “ShaderCompositor.replaceInput(‘inputToReplace’, newValue)”.
For example:
self.compositor = ShaderCompositor()
self.compositor.loadShader("nameForShader", "myVertex", "myFragment")
self.compositor.setShader(someNodePath, "nameForShader")
self.compositor.replaceSubShader("normalMapping", "noNormalMapping")
# This should automatically update "someNodePath" to use the new shader
“myFragment” might look something like this:
uniform sampler2D p3d_Texture0;
in vec3 normal;
in vec3 binormal;
// Further parameters omitted for (relative) brevity...
// Syntax: result, method name, default chunk-file
vec3 normal = normalMapping() defaultNormalMappingChunk
vec3 colour = colourMapping() defaultColourMappingChunk
float lightLevel = lighting(normal) defaultLightingChunk
gl_FragColor = finalOutput(colour, lightLevel) defaultColourCalculationChunk
There might then be a shader-chunk file named “defaultNormalMappingChunk” that looks something like this:
vec3 normalMapping(normalTex, texCoords, normal, tangent, binormal)
{
vec3 normalMapVec = (texture2D(normalTex, texCoords.st)*2.0) - 1.0;
vec3 right = binormal*normalMapVec.y;
vec3 up = tangent*normalMapVec.x;
vec3 norm = normal*normalMapVec.z;
return normalize(up + right + norm);
}
Any shader inputs required by a chunk–as the lighting chunk might want a “light-direction” input–might be specified in the chunk, and then moved to the start of the resultant shader-string as part of the composition process.
So, is this a good idea? Are there any easier or better ways to do what I have in mind? Are there any pitfalls or caveats that I should be aware of? (For example, when replacing a shader, should the old version be “unloaded” somehow?)