Writing Shader Code for the Universal RP

Keywords & Shader Variants

Before we go over Lighting, we need to talk about keywords and shader variants, as they are used in the URP ShaderLibrary and it’s important to know what they are actually doing so we can get the lighting functions to work properly.

In shaders we can specify more #pragma directives, some of which include multi_compile and shader_feature. These can be used to specify keywords for turning certain parts of the shader code “on” or “off”. The shader actually gets compiled into multiple versions of the shader, known as shader variants.

multi_compile
#pragma multi_compile _A _B _C (...etc)

In this example we are producing three variants of the shader, where _A, _B, and _C are keywords.

In the shader code, we can then use something like the following :

#ifdef _A
// compile this code if A is enabled
#endif

#ifndef _B
// compile this code if B is disabled, aka only in A and C.
// note the extra "n" in the #ifndef, for "if not defined"
#else
// compile this code if B is enabled
#endif

#if defined(_A) || defined(_C)
// compile this code in A or C. (aka the same as the above, assuming there's no other keywords)
// We have to use the long-form "#if defined()" if we want multiple conditions
// where || = or, && = and
// Note however that since the keywords are defined in one multi_compile statement
// It's actually impossible for both to be enabled, so && wouldn't make sense here.
#endif

// There's also #elif, for an else if statement.
shader_feature
#pragma shader_feature _A _B

This works exactly the same as multi_compile, with the exception that unused variants will be not be included in the final build. For this reason, it’s not good to enable/disable these keywords at runtime, since the shader it needs might not be included in the build! If you need to handle keywords at runtime, use multi_compile instead.

Shader Variants

With each added multi_compile and shader_feature, it produces more and more shader variants for each possible combination of enabled/disabled keywords. Take the following for example :

#pragma multi_compile _A _B _C
#pragma multi_compile _D _E
#pragma shader_feature _F _G

Here, the first line is producing 3 shader variants. But the second line, needs to produce 2 shader variants for those variants where _D or _E is already enabled.

So, A & D, A & E, B & D, B & E, C & D and C & E. That’s now 6 variants.

Third line, is another 2 variants for each of those 6,  so we now have a total of 12 shader variants. Since that line is a shader_feature, some of those variants might not be included in the build though.

Each added multi_compile with 2 keywords will double the amount of variants produced, so a shader that contains 10 of these will result in 1024 shader variants! It’ll need to compile each shader variant that needs to be included in the final build, so will increase build time as well as the size of the build.

If you want to see how many shader variants a shader produces, click the shader and in the inspector there’s a “Compile and Show Code” button, next to that is a small dropdown arrow where it lists the number of included variants. If you click the “skip unused shader_features” you can toggle to see the total number of variants instead.

There is also “vertex” and “fragment” versions of these directives which can be used to compile shader variants only for the vertex or fragment programs, reducing the total number of variants. For example :

#pragma multi_compile_vertex _ _A
#pragma multi_compile_fragment _ _B
// also shader_feature_vertex and shader_feature_fragment

In this example, the _A keyword is only being used for the vertex program and _B only for the fragment. There cannot be a variant that has both _A and _B enabled. Unity tells us that this produces 2 shader variants, although it’s more like one shader variant where both are disabled and two “half” variants when you look at the actual compiled code.

Strangely I haven’t seen these documented anywhere, but I’ve seen a few others mention the fragment one, and I tested the vertex one and it worked too. (Maybe they won’t work for every platform though?)

Maximum Keywords

There is also a maximum of 256 keywords per project, so it can be good to stick to the naming conventions of other shaders.

You’ll also notice for many multi_compile and shader_features the first keyword is usually left as just “_”. This doesn’t actually produce a keyword so leaves more space available for other keywords in the 256 maximum.

#pragma multi_compile _ _KEYWORD

#pragma shader_feature _KEYWORD
// Which is shorthand for (and only for shader_features)
#pragma shader_feature _ _KEYWORD

// If you need to know if that keyword is disabled
// We can then just do this instead :
#ifndef _KEYWORD
// or #if !defined(_KEYWORD)
// or #ifdef _KEYWORD #else
// code
#endif

We can also avoid using up the maximum keyword count by using local versions of the multi_compile and shader_feature. These produce keywords that are local to that shader, but there’s also a maximum of 64 local keywords per shader. 

#pragma multi_compile_local _ _KEYWORD
#pragma shader_feature_local _KEYWORD

// There's also local_fragment/vertex ones too!
#pragma multi_compile_local_fragment _ _KEYWORD
#pragma shader_feature_local_vertex _KEYWORD

You can read more about shader variants here.