Quick answer: Unity strips unused shader variants at build time. Variants needed at runtime (e.g. enabled via SetKeyword) must be recorded via a ShaderVariantCollection or listed in Always Included Shaders. Record a Play session into an SVC, add it to Preloaded Shaders, and call WarmUp() during loading.
Here is how to fix Unity shaders that work in the Editor but render magenta or fallback in builds. You ship a build, enable a shader feature at runtime via material.EnableKeyword("_FOG_ON"), and the shader breaks — either pink or silently wrong. The Editor always has every variant; the build has only what Unity thinks you need. If you enable keywords at runtime that no material uses at build time, those variants are stripped and missing.
The Symptom
- Shader renders correctly in Editor but magenta/fallback in build
- Shader works for static materials but breaks when you toggle keywords at runtime
- First frame using a new keyword combination stutters heavily (compile hitch)
- Build log shows “X shader variants compiled, Y stripped” with high Y
- Material.EnableKeyword logs a warning about missing variant
What Causes This
Variant explosion. A shader with 5 multi_compile keywords can have up to 32 variants per pass. Modern PBR shaders with fog, lightmaps, shadows, and multiple light types easily reach hundreds of variants per shader. Including all of them in a build would bloat the file by hundreds of MB.
Strip is default behavior. Unity scans every material in the build (in scenes, in Resources, in Addressables) and collects the keyword sets they use. Only those variants are kept. Unused combinations are stripped. This is the correct default — the problem is when runtime code enables a keyword that no material had at build time.
Always Included Shaders cover a narrow case. Shaders listed in Project Settings > Graphics > Always Included Shaders are always built — but only their default variant. Keyword combinations are still stripped from them unless collected.
Shader Variant Collection records actual usage. An SVC is a list of (shader, keyword-set, pass-type) tuples. Variants in an SVC are preserved through build stripping. Recording a Play Mode session captures every variant the game actually uses.
Build-time keyword stripping callbacks. Unity invokes IPreprocessShaders during build. Packages (URP, HDRP, custom) implement strippers that remove variants based on project settings. If the stripper is too aggressive, valid variants vanish. URP’s stripper respects Quality Settings — features disabled at the active quality level are stripped.
The Fix
Step 1: Record a variant collection. Edit > Project Settings > Graphics. Scroll to Currently Tracked Shader Variants. Click Clear. Enter Play Mode. Play through every scenario that might trigger unique shader variants — day/night, rain effects, damage shaders, underwater, menu shaders, every quality preset. Exit Play Mode.
Click Save to Asset. Unity writes a ShaderVariantCollection file with everything used during that session.
Step 2: Add the SVC to Preloaded Shaders. Project Settings > Graphics > Preloaded Shaders. Drag your SVC into this list. Build-time stripping now preserves all variants in the SVC.
Step 3: Warm up at loading screens. To avoid compile hitches when a variant is first used, warm it up explicitly:
using UnityEngine;
public class ShaderWarmup : MonoBehaviour
{
[SerializeField] private ShaderVariantCollection collection;
void Start()
{
if (collection != null && !collection.isWarmedUp)
collection.WarmUp();
}
}
Call this before revealing the scene — during a loading screen or splash. WarmUp compiles and uploads each variant synchronously and can take several seconds on large collections, but prevents per-frame hitches later.
Step 4: Use Always Included Shaders sparingly. For shaders referenced purely by code at runtime (not by any scene material), add them to Always Included Shaders. This keeps the shader in the build. Pair with an SVC entry to also keep needed variants.
Step 5: Be explicit about multi_compile. In your shader:
#pragma multi_compile _FOG_OFF _FOG_ON
#pragma multi_compile_local _DAMAGE_OFF _DAMAGE_ON
multi_compile generates variants that survive stripping based on material usage. multi_compile_local is shader-local (only variants used by this shader’s materials survive). shader_feature only generates variants if a material uses the keyword — so runtime-only EnableKeyword does not preserve shader_feature variants.
Use multi_compile for keywords toggled at runtime. Use shader_feature for keywords set at build-time via material checkboxes.
Debugging Variant Issues
Inspect a shader in the Project window. Click Compile and Show Code. The dropdown shows compiled variants and active keywords. If the keyword you want is absent, it was stripped or never declared.
Build log (Console after a build) shows compilation statistics:
Compiling shader "Universal Render Pipeline/Lit"
Pass UniversalForward (Vertex + Fragment)
224 variants compiled (0.8s), 1892 stripped by scriptable stripper
If your variant count is suspiciously low, a stripper is over-eager. Check URP’s Shader Stripping options in the URP asset: Prefiltering strips keywords for features that no quality level enables. Set strictly based on actual needs.
Scripted Variant Collection
For CI/CD pipelines, build the SVC programmatically:
using UnityEditor;
using UnityEngine;
public static class BuildSVC
{
[MenuItem("Tools/Append Current To SVC")]
public static void Append()
{
var svc = AssetDatabase.LoadAssetAtPath<ShaderVariantCollection>("Assets/Graphics/Shipping.shadervariants");
var current = ShaderUtil.SaveCurrentShaderVariantCollection("Assets/Graphics/Temp.shadervariants");
// merge manually or use editor utility
EditorUtility.SetDirty(svc);
AssetDatabase.SaveAssets();
}
}
Automate this so QA sessions append to a master SVC that ships with builds.
“Stripping is the default because shipping every variant is unacceptable. Record what you need or lose what you miss.”
Addressables Note
When using Addressables, shader variants referenced only by addressable content must be explicitly included. Either assign shaders to a shared Addressable group or add the SVC to Preloaded Shaders as normal. The build pipeline does inspect addressable content for variant usage.
For related shader issues, see Unity Shader Magenta/Pink.
Record SVC. Preload SVC. WarmUp at loading. multi_compile for runtime keywords.