Conditional shader features can be implemented in different ways:

  • Static (compile-time) branching
  • Shader variants compilation
  • Dynamic (runtime) branching

While static branching avoids branching-related shader execution overhead at runtime, it’s evaluated and locked at compilation time and does not provide runtime control. Shader variant compilation, meanwhile, is a form of static branching that provides additional runtime control. This works by compiling a unique shader program (variant) for every possible combination of static branches, in order to maintain optimal GPU performance at runtime.

Such variants are created by conditionally declaring and evaluating shader functionality through shader_feature and multi_compile shader keywords. The correct shader variants are loaded at run time based on active keywords and runtime settings. Declaring and evaluating additional shader keywords can lead to an increase in build time, file size, and runtime memory usage.

At the same time, dynamic (uniform-based) branching entirely avoids the overhead of shader variant compilation, resulting in faster builds and both reduced file size and memory usage. This can bring forth smoother and faster iteration during development.

On the other hand, dynamic branching can have a strong impact on shader execution performance based on the shader’s complexity and the target device. Asymmetric branches, where one side of the branch is much more complex than the other, can negatively impact performance. This is because the execution of a simpler path can still incur the performance penalties of the more complex path.

When introducing conditional shader features in your own shaders, these approaches and trade-offs should be kept in mind. For more detailed information, see the shader conditionals, shader branching, and shader variants documentation.

Source: Unity Technologies Blog