Uber Shaders and Shader Permutations




Intro

If you've been playing around with different shaders in your own engine without a developed architecture, you've probably noticed that you're starting to accumulate a lot of shader files and separate classes for each type of shader. Then you start to think about how scary it will be when that scales, and how that kind of system is ultimately unmaintainable. That's where Uber Shaders (and other methodologies) come in handy.

Implementation

The concept and implementation are actually super simple. Lets take this shader as an example:

float4 SimpleLitPixelShader(VS_to_PS input) : SV_TARGET
{					  
    float4 textureColor = DiffuseTexture.Sample(DiffuseSampler, input.texCoord0);

    float nDotL = saturate(dot(input.normal, lightDirection));
	
    float4 output = textureColor * diffuseColor * nDotL;
	
    return output;
}
						

Pretty standard lighting shader, right? Now let's say we want a shader that does the same thing, but uses a normal map as well. Normally we'd copy/paste the shader into a new file and swap the logic for the normals, but we can do better than that. Preprocessor directives come to the rescue here, so we go from having two separate shader files to one that looks like this. This is our Uber Shader file:

float4 UberLitPixelShader(VS_to_PS input) : SV_TARGET
{					  
    float4 textureColor = DiffuseTexture.Sample(DiffuseSampler, input.texCoord0);
	
    float3 normal = input.normal;
	
#ifdef USE_NORMAL_MAP
    float4 bumpMap = NormalMap.Sample(NormalMapSampler, input.texCoord0);
    bumpMap = (bumpMap * 2.0f) - 1.0f;
	
    float3 viewSpaceBinormal = cross( normal, input.tangent );
	
    float3x3 texSpace = float3x3(input.tangent, viewSpaceBinormal, normal);
	
    normal = normalize(mul(bumpMap, texSpace));
#endif
	
    float nDotL = saturate(dot(normal, lightDirection));
	
    float4 output = textureColor * diffuseColor * nDotL;
	
    return output;
}
						

Creating the shader that uses the normal map now just requires a #define, and then includes our uber shader, which will enable the normal map logic. That file looks like this:

#ifndef USE_NORMAL_MAP
#define USE_NORMAL_MAP  1
#endif

#include "UberLitPixelShader.hlsl"
						

That's it! Easy right? We have all our logic condensed into one file, so now all we have to do to create a new shader is declare our defines for what we want and include the uber shader. There are other methodologies such as micro shaders, branching shaders, etc, but this is just my personal preference for my own engine. Definitely research the other techniques if you're curious or don't like the uber shader setup.

This is the first step toward setting up a solid material system, but now you can already see how easy it would be to create a tool that generates shaders!

Contact

  • edenspuzzle@gmail.com
  • 603-377-6737
  • LinkedIn