Skip to content

Shader Variants

Antonio Caggiano edited this page Dec 20, 2021 · 8 revisions

I started writing shaders just to put some pixels on the screens and to get confident with some graphics techniques, but as soon as I decided to support glTF models and the PBR lighting model in RustSpot, the number of shaders started to increase almost exponentially. The most annoying thing was the repetition of some code in each shader.

The first idea was just to use the well-known mechanism of inserting code with a C-like #include directive but GLSL does not know what it means, unless you use an extension, but it may not be available everywhere, and I tough it would not be too difficult to write a system to resolve the include directives upfront, and generate the shader code that would be actually used by the renderer.

Indeed that part was easy and I refactored out some common code but it was not enough for the idea of flexibility I had in mind. The problem arises from the fact that you might have multiple ways to do the same thing, indeed if you look at a glTF model you would find metallic-roughness values either as values embedded in the material structure or as a texture to sample. Two strategies, two different include files to do the same thing, still required me to write two identical shaders differing only by that include line to include each of the strategies.

At that point I had this idea of include variants. One single base shader with an #include directive that could be provided by multiple glsl files, as long as they start with the same prefix, would generate just as many shaders variants; and, here is where it explodes, multiple #include directives in the same base shader would generate all possible combinations of shaders you could get from the various include variants at our disposal.

OK, let us have a look at a practical example. As I mentioned, we can retrieve metallic-roughness values either directly, which would be our default include option, or by sampling a texture. This means we have two include variants, aka two files starting with the same prefix, (metallic-roughness.glsl and metallic-roughness.texture.glsl), and a base shader just needs the line #include "metallic-roughness.glsl" to generate two shader variants, one with the default include and the other with the texture option.

Shader Variants

Click on the image to play the video on YouTube

Code samples

Base shader

// This will generate two shader variants:
// - One with the content of `metallic-roughness.glsl`
// - Another with the content of `metallic-roughness.texture.glsl`
#include "metallic-roughness.glsl"

uniform vec2 uv;

void main() {
    vec3 MR = get_metallic_roughness(uv);
    // .. use metallic roughness values
}

Default Metallic Roughness

// metallic-roughness.glsl
uniform float metallic;
uniform float roughness;

vec3 get_metallic_roughness(vec2 uv) {
  return vec3(roughness, metallic)
}

Texture Metallic Roughness

// metallic-roughness.texture.glsl
uniform sampler2D mr_sampler;

vec3 get_metallic_roughness(vec2 uv) {
  return texture(mr_sampler, uv).gb;
}
Clone this wiki locally