URP Shader Variant Auditor

Always Included Shaders

The setting

Project Settings > Graphics > Always Included Shaders is a list of shaders Unity puts into every build unconditionally. It exists for shaders nothing references at build time - ones assigned from script by Shader.Find, for example - which would otherwise be stripped out entirely and show up in the player as magenta.

Adding a shader here does guarantee it is in the build. It also changes how that shader is compiled, and the second effect is not mentioned next to the setting.

What changes

Unity decides which shader variants to compile by looking at materials. A material records which keywords it enables; the set of materials in the build is the set of keyword combinations that need programs. Everything else in the shader's shader_feature space is dropped.

A shader in the Always Included list arrives with no material. There is no keyword combination to narrow to, so Unity compiles the shader's full shader_feature space instead of the handful of combinations the project uses. The multi_compile space was always going to be compiled in full; the shader_feature space is what stripping was removing, and that removal is now gone.

For a small custom shader the difference is not interesting. For a render pipeline shader it is the difference between a build and no build. URP's Lit shader covers every lighting setup, shadow cascade configuration, lightmap encoding, decal mode, fog mode and rendering path the pipeline supports. Its full keyword space is very large by design, and stripping is the mechanism that makes it usable.

What it looks like when it happens

In the project this rule was written from - a URP racing game on Unity 6, URP 17 - the URP shaders were added to Always Included Shaders while chasing a magenta material in a player build. The symptom was not an error. The build simply entered "Compiling shader variants" and stayed there; the reported variant count was in the high hundreds of thousands, and the build did not complete.

Two things made it hard to diagnose:

  1. The project had five materials. A variant count in the hundreds of thousands is not something five materials can produce, so material count was the wrong place to look. The count came from one setting, applied to shaders that were never the project's own.
  2. Nothing reports the number before the build starts. The only feedback was a build that ran for tens of minutes and then did not finish.

That is the gap this package fills: the number, and its cause, before the build.

The fix

Do not remove the shader from the build - it was added for a reason. Restore the material context instead.

Material template under Resources. Create a material that uses the shader, with exactly the keywords you need enabled, and put it in a Resources folder. Everything under Resources ships whether or not a scene references it, so the shader is guaranteed to be in the build, and because it arrives attached to a material, stripping applies normally. Only that material's keyword combination is compiled.

Assets/Resources/ShaderTemplates/URP_Lit_Template.mat
Assets/Resources/ShaderTemplates/URP_Unlit_Template.mat

At runtime, instantiate from the template rather than calling Shader.Find:

var template = Resources.Load<Material>("ShaderTemplates/URP_Lit_Template");
var runtime  = new Material(template);   // keeps the template's keyword set
runtime.color = teamColor;

Then remove the shader from Always Included Shaders and re-run the audit. URP001 clears, and the project total drops to the "with stripping" figure the report was already showing.

One caution, because this is the way the fix goes wrong: the template has to enable every keyword combination you set at runtime. A combination no material declares gets stripped, and the player falls back to another variant or renders incorrectly while the editor still looks right, because the editor compiles on demand. Check the result in a player build, not only in play mode.

Addressables or a scene reference work the same way, for the same reason: the shader reaches the build through a material.

A shader variant collection is the other supported route. Record the variants you use in play mode, save the collection, and reference it from Graphics settings under Preloaded Shaders. This gives you an explicit list rather than a full compile.

IPreprocessShaders is the escape hatch when a shader genuinely has to stay in the Always Included list. Implement the callback and remove the keyword combinations the project never uses. URP004 in the report tells you which sets are worth removing and what each one costs.

Checking it worked

Run the audit again. The two numbers in the report header should converge:

before   842,000 variants on the current settings. 6,400 with stripping applied throughout.
after      6,400 variants on the current settings. 6,400 with stripping applied throughout.

If they have not converged, a shader is still reaching the build without a material - URP001 and URP002 list which.