Picture this: you've been tirelessly building your game, painstakingly adding increasingly detailed shaders and visual effects to enhance the gameplay experience. Suddenly, you notice a worrying drop in frames per second and an overall sluggish performance. You attempt to solve this by turning down some quality settings, yet the issue persists. What could be the problem?
The issue you’re likely facing is that your game is loading all possible variations of the shaders, even if they’re not being used in the current scene. This issue is especially prominent in large and complex scenes, which can quickly turn into resource hogs due to unnecessary shader loading.
Shader Variant Prefiltering, often referred to as Shader Permutation Stripping, can be very useful in this situation. By only loading the versions of the shaders that are actually utilized in your scene, you can optimize the performance of your shaders. This means that the engine will only load the specific variations of a shader required for the objects in your scene, enhancing performance, particularly in larger, detail-heavy scenes.
Let's delve into the process of implementing Shader Variant Prefiltering, we'll use Unity in this example.
Implementing Shader Variant Prefiltering
The implementation process for Shader Variant Prefiltering is relatively simple, however, it varies slightly depending on the Unity rendering pipeline you are using.
Make sure your project is set up to use the appropriate rendering pipeline by selecting the corresponding asset in the Graphics settings.
Determine the shaders that are applied to each object in your scene. To accomplish this, select the object in the Scene, then look at the Material component. The shaders that are being utilized can then be added to the relevant built-in Shader Variant Collection. You can have a script do this for you if there are too many shaders in your scene, we get into that in the next section.
Use the appropriate tools to optimize the shaders based on the usage in the scene. For Built-in Render Pipeline you can use the Unity Shader Variants feature by going back to the Unity Editor and selecting Assets > Shader Variants > Build Variants. For URP or HDRP you can use the Shader Variants Updater tool by selecting the URP/HDRP asset and then window > Universal/High Definition Render Pipeline > Shader Variants Updater.
To actually perform prefiltering on runtime you can use the following script and uncomment out the section depending on which rendering pipeline you are using:
using UnityEngine;
//using UnityEngine.Rendering;
//using UnityEngine.Rendering.Universal;
//using UnityEngine.Rendering.HighDefinition;
public class ShaderVariantPrefiltering : MonoBehaviour
{
public ShaderVariantCollection variantCollection;
// Uncomment this section to activate the prefiltering on runtime for Built-in Render Pipeline
/*
private void Start()
{
Graphics.activeTierChanged += OnActiveTierChanged;
}
private void OnActiveTierChanged(object sender, System.EventArgs e)
{
variantCollection.WarmUp();
}
*/
// Uncomment this section to activate the prefiltering on runtime for URP
/*
private void Start()
{
RenderPipelineManager.beginFrameRendering += OnBeginFrameRendering;
}
private void OnBeginFrameRendering(ScriptableRenderContext context, Camera[] cameras)
{
ShaderVariantCollection.WarmUp();
}
*/
// Uncomment this section to activate the prefiltering on runtime for HDRP
/*
private void Start()
{
HDRenderPipeline.beginFrameRendering += OnBeginFrameRendering;
}
private void OnBeginFrameRendering(ScriptableRenderContext context, HDCamera[] cameras)
{
ShaderVariantCollection.WarmUp();
}
*/
}
Automatically Identifying Shaders
This basic script identifies which shaders are being used on every object in a scene, and adds the shaders to the Shader Variant Collection.
using UnityEngine;
using System.Collections.Generic;
public class ShaderIdentification : MonoBehaviour
{
public ShaderVariantCollection variantCollection;
void Start()
{
var renderers = FindObjectsOfType<Renderer>();
foreach (var renderer in renderers)
{
var materials = renderer.sharedMaterials;
for (int i = 0; i < materials.Length; i++)
{
variantCollection.Add(materials[i].shader);
}
}
}
}
Diving Deeper
The main advantages of shader variant profiling is that it allows for runtime conditionals in shader programs without the negative GPU performance impact of dynamic branching. However, there are also some disadvantages to using shader variants. One of the main disadvantages is that a large number of variants can lead to increased build times, file sizes, runtime memory usage, and loading times. It also leads to greater complexity when manually preloading (“prewarming”) shaders. When a project contains a very large number of shader variants, these issues can lead to significant problems with performance and workflow.
When Unity creates shader variants, it uses static branching to create multiple small, specialized shader programs. At runtime, Unity uses the shader program that matches the conditions. This means that you can use shader variants for code that would likely result in reduced GPU performance in a dynamic branch, without suffering a GPU performance penalty.
As games continue to push the boundaries of graphics and visual effects, the complexity of graphics rendering is only going to increase. Techniques such as shader variant profiling will become increasingly more important as a way to optimize the performance and ensure that the your game runs smoothly for everyone :)