Table of Contents

Compile-time switches

A GPU runs the same shader code for many pixels at once, and branches in that code aren't free. Unity's answer is shader variants: the shader is compiled several times, once for each combination of features, and each material uses the variant with only the code it needs.

In Shader Script, you get variants by writing ordinary if statements on values that are known at compile time. The branches that don't apply to a variant are removed from it, so checking such a value costs nothing when the shader runs.

if (IsAlphaTestEnabled)
    OUT.alphaClipThreshold = Cutoff;

What's known at compile time

Value What it tells
A [KeywordSwitch] property The value the material selected (see below).
A [TextureIsSet] property Whether a texture is assigned to the material.
IsUrp, IsHdrp Which render pipeline the shader is compiled for.
CurrentPass Which pass the code is compiled for. See Passes.
IsTransparent Whether the material is transparent.
IsAlphaTestEnabled Whether alpha clipping is on.
IsAlphaModulate Whether the material uses multiply blending.
IsPremultiplyAlpha Whether the shader multiplies the color by alpha itself (a transparent URP material with Preserve Specular Lighting).
IsEmissionEnabled Lit shaders: whether emission is on.
IsSpecularWorkflow, IsMetallicWorkflow Lit shaders, URP: the workflow the material uses.
IsReceiveShadowsDisabled Lit shaders, URP: whether the material doesn't receive shadows.
Constants and enum values Always: const fields and enum values are replaced by their values.

The settings flags, like IsTransparent, follow the material's surface settings, whether the shader fixes them with an attribute or the material chooses them in the Inspector. See Surface settings.

KeywordSwitch

[KeywordSwitch] turns a material property into a compile-time switch. On an enum property, each enum value compiles into its own variant; on a float or int property, shown as a checkbox, on and off do.

public enum NoiseQuality { Low, Medium, High }

[KeywordSwitch] public NoiseQuality Quality { get; } = NoiseQuality.Medium;
[KeywordSwitch] public float UseDetail { get; } = 0;

public override void Frag(FragmentInput IN, ref UnlitFragmentOutput OUT)
{
    float2 uv = IN.uv0;
    float noise = ShaderLib.SimpleNoise(uv, 10);

    // Only the branch for the material's Quality is compiled into its variant.
    if (Quality == NoiseQuality.High)
        noise = ShaderLib.GradientNoise(uv, 40) * 0.5f + noise * 0.5f;

    if (UseDetail > 0.5f)
        noise *= ShaderLib.Voronoi(uv, 0, 20).x;

    OUT.albedo = (half3)new float3(noise, noise, noise);
}

In shader code, a [KeywordSwitch] property is a constant: an enum value, or 0 and 1 for a checkbox. Besides comparing it, you can use it in expressions like any other number.

Compare it with [Toggle], which also shows a checkbox but leaves the value an ordinary runtime value: both branches of the if are then compiled and evaluated. Use [KeywordSwitch] for features that are expensive or that most materials leave off, and [Toggle] for cheap choices, where more variants would cost more than the branch.

Keywords and builds

Each switch value corresponds to a material keyword:

  • an enum value: _<PROPERTY>_<VALUE>, for example _QUALITY_HIGH;
  • a checkbox: _<PROPERTY>_ON, for example _USEDETAIL_ON.

The Material Inspector keeps the keywords in sync with the property. If you change the property from a script, also enable the matching keyword and disable the others with Material.EnableKeyword and Material.DisableKeyword.

When you build a player, Unity includes only the variants that the project's materials use. If a script switches a material at runtime to a value that no material in the build uses, that variant is missing. Make sure some material uses every value you need, or include the variants with a shader variant collection.

Pipeline-specific code

IsUrp and IsHdrp let you write code that differs between the render pipelines. The branch for the other pipeline is removed from the compiled shader, but it's still compiled for both pipelines, so it can only use what exists on both: pipeline-specific built-in variables, such as the URP-only matrices, can't be used even inside such a branch.

// HDRP uses physical light units, so emission needs a much higher intensity there.
OUT.emission = IsHdrp ? EmissionColor.rgb * 1000 : EmissionColor.rgb;

See One shader, two render pipelines.