A "Shader Compositor": a good idea?

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?)

Hm, to be honest I am not sure if this is really neccessary - #pragma include works just fine.

If you want to permute shaders (Which btw is only really required when using forward rendering), then you could aswell toggle features on and off using defines.

Also, thats basically what the shader generator does, it links shader parts together depending on the current render state.

Indeed, as I said, the “#include” functionality is secondary to the rebuilding functionality.

Based on a quick search, I gather that “forward rendering” refers to the standard rendering model, as opposed to deferred rendering. In which case, yes, “forward rendering” is what I’m using.

Hmm… I hadn’t considered using “#define” statements. I suppose that if I want to change the code in a shader–such as by removing the shadow-mapping code when shadows are disabled by the user–I could simply change the relevant definition and reload the shader–am I correct?

[edit]
Actually, how do I set definitions for shaders without editing their text? Is there some facility in Panda or Python that I’m not aware of? A quick look at the API page for the Shader module doesn’t reveal anything obvious…
[/edit]

I guessed as much–but then, the shader generator doesn’t (to the best of my knowledge) allow one to use one’s own shader files. What I proposed above would be a little more general-purpose than the shader generator, I believe.

However, given your mention of “#define” statements above, I think that my “shader compositor” probably is superfluous indeed.

Thank you for the input! :slight_smile:

There is no method to add defines from the code yet - having a tool for that would probably be useful though.

Your code would basically look like:

#define USE_SHADOWS 1 // or 0
...
#if USE_SHADOWS
// shadowmapping code
#endif

I’m honestly not sure of how one would go about that, to be honest. ^^;

Hmm… It may be worth looking into, however.

Am I correct in assuming that one would have to reload the shader when a definition is changed? If so, should the already-loaded version be cleaned up somehow, or is it enough to simply reassign any variables referencing it and apply the newly-loaded version to all objects to which it’s been applied? (I don’t see any sort of “cleanup” or “destroy” method in the API documentation for Shader.)

I imagine that the basic usage of such a “define”-setting module would be something like the following (in which I’m referring to the module as “DefineManager”):

#  Assume that "aVertexShader.glsl" does something sensible; it's just here
# to fill the parameter in "Shader.load". See "myFragmentShader.glsl" below.

DefineManager.setDefinition("USE_TEXTURE", 1)

aShader = Shader.load(Shader.SL_GLSL, "aVertexShader.glsl", "myFragmentShader.glsl")
someNodePath.setShader(aShader)

# We now want to change the behaviour of the shader...

DefineManager.setDefinition("USE_TEXTURE", 0)

aShader = Shader.load(Shader.SL_GLSL, "aVertexShader.glsl", "myFragmentShader.glsl")
someNodePath,setShader(aShader)

Where “myFragmentShader.glsl” looks something like this (a short and somewhat silly shader simply intended to show the defines mentioned above):

#version 130

#if USE_TEXTURE

in vec2 texCoord;
uniform sampler2D p3d_Texture0; // Diffuse colour

#endif

void main()
{
#if USE_TEXTURE
    gl_FragColor = texture2D(p3d_Texture0, texCoord.st);
#else
    gl_FragColor = vec4(1, 1, 1, 1);
#endif
}

You have to reload the shader when a definition is changed. I am pretty sure the shader gets cleaned up when its not used anymore - however, be sure to call base.camNode.clear_tag_states() (actually for every camera with initial states) in case you have any initial states set - since panda cannot know whether you still need them or not.

I am also not sure if your syntax is possible without modifying the panda3d sourcecode. A much easier solution would be something like:

shader = FancyShaderGenerator.load(Shader.SL_GLSL, "vtx.glsl", "frag.glsl", {
   "USE_TEXURE": True
})
mynp.set_shader(shader)

This way the class could just load the shader from disk, append the defines at the top (after the version string, though), and then return a new shader using Shader.make(…).

This also makes it very easy to use different defines for different objects

My apologies for the delay in response! ^^;;

So… More or less a much lighter version of what I described above, then? Instead of using a meta-file and compositing a final shader-string, we just read in a single shader file (that uses #pragma includes in the background) and insert the relevant definitions as appropriate.

Fair enough, and thanks for your help! :slight_smile: