Shaders are combination of different effects, you may need skinning and shadows to render your character, but just diffuse textures for skymap, while doing a depth pass only, you do not need to render any textures but deformation is still needed for skinned meshes. More effects you have, more combinations you end up with.

One solution to this problem is writing a mega-shader, a shader combines multiple effects but choses which ones to run using conditional blocks. However even if you are using uniform variables which does not change until rasterization is complete, you still can suffer from performance penalty by changing uniforms in some driver implementations.[1]

Non-implementation dependent solution is to use preprocessor directives in GLSL, and generating shaders for your effect combinations. This way we can organize code into different mini-shaders combined in compile time.

For example:

reShader* shader = new reShader();
if (reEFFECT_SHADOW_MAP & name)
{
    shader->defines.push_back("_SHADOW_MAP_"); // Shadow effect
}
if (reEFFECT_DIFFUSE_TEXTURE & name)
{
    shader->defines.push_back("_DIFFUSE_TEXTURE_");
}
if (reEFFECT_SKIN & name)
{
    shader->defines.push_back("_SKIN_");
}
return shader;

Vertex shader GLSL:

#ifdef _SHADOW_CAST_
#pragma include "vsm_cast.fp"
#else
float illumination()
{
    ...
}
#endif

vec4 getDiffuse()
{
#ifdef _DIFFUSE_TEXTURE_
    return diffuseColor * texture2D(diffuseTexture, uv);
#else
    return diffuseColor;
#endif
}

#ifdef _SHADOW_MAP_
#pragma include "vsm_shadow.fp"
#else
vec4 getColor()
{
    vec4 color = getDiffuse() * illumination();
    return color;       
}
#endif

void main()
{
    gl_FragColor = getColor();
}

Inluding shaders is a way of organizing your code, however GLSL does not provide a way for source code inclusion, since it does not really have a concept of files. We need a small function to do it for us:

std::string reShader::preprocess( std::string& src )
{
    std::stringstream ss(src), out;
    std::string line;
    string pragma("#pragma include");
    while (std::getline(ss, line))
    {
        if (size_t pos = line.find(pragma) != string::npos)
        {
            string include = line.substr(pos+pragma.size()+1, pos+line.size() - pragma.size()-4);
            out << reRadial::shared()->assetLoader()->loadFile("shaders/"+include) << endl;
        }
        else
            out << line << endl;
    }
    return out.str();
}

Conlusion

If you have too many effect combinations, you will end up with too many shaders, switches between shaders can also have performance penalty, so most of the times, combining shaders can also be used with conditionals. For instance multiple effects commonly used together can be defined as single effect, this way we can also specialize the code for certain combination. Your mileage will vary depending on the platform you target, benchmarks are necessary to find optimal render strategy for your needs.


Comments

comments powered by Disqus