in Code, Games, HowTo

Using Unity’s ShaderVariantCollection

Recently I have been working on porting an existing Unity game to macOS. It is a shoot-em-up called Blue Rider developed by Ravegan from Córdoba, Argentina. It was originally released on PC (Steam), XBox, Playstation 4, and Switch.

Blue Rider Logo

The main things I needed to work on to bring Blue Rider to macOS were the menu system, the resolution handling, the input handling, and some optimizations to achieve a smooth framerate.

(Note: I’m using Unity 5.6.7f1 since Blue Rider was written using a 5.x version. I’m not 100% sure, but based on the current documentation, I think what I’m doing here still applies with recent versions.)

The Problem

One such optimization had to do with shaders. The game was hitching sometimes when an enemy appeared and when it exploded. It only seemed to happen some times and with some enemies.

I’ll explain what I found when I broke out the profiler and then I’ll provide one possible solution using ShaderVariantCollection.

The Analysis

I fired up the profiler and ran through the game until I saw spikes when an enemy appeared and when it died.

The profiler identified the culprit as Shader.EditorLoadVariant.

Unity Profile - Shader.EditorLoadVariant

Unity Profile – Shader.EditorLoadVariant

Specifically it’s the compilation of shader variants – Shader.CreateGPUProgram.

Unity Profile - Shader.CreateGPUProgram

Unity Profile – Shader.CreateGPUProgram

The game is asking to use a variant of a shader that it hasn’t compiled yet, so it compiles it immediately. Because this process is slow it can’t fit into one frame, so the game feels like it pauses and then tries to catch up. This is known as “hitching”.

This normally happens only the first time a shader variant is compiled. Subsequent uses during the same run are either in memory or are cached. Unfortunately, Unity doesn’t seem to cache these compiled shaders between runs, so it happens every time you run the game.

After a lot of searching and reading online, there didn’t seem to be any answers to the questions in this post.

A Solution

The game I’m working on has hundreds of objects and many shaders & shader variants, so a per-object solution seemed challenging. The solution I came up with was to use a ShaderVariantCollection for each of the game’s levels.

(That seems to be the entire official documentation on these—please let me know if I’ve missed something).

Generating The Collections

From the docs:

Typical use of ShaderVariantCollection is to record the shader variants used during a play session from the editor (under Graphics Settings), save them out as an asset, and add to the list of preloaded shaders (again in Graphics Settings). Additionally, you could call WarmUp on a ShaderVariantCollection object manually.

I chose to create one shader variant collection per level and then load them & call WarmUp at the beginning of each level. To start with, I created a folder in Assets/Resources to save these files. Putting it in Resources ensures that Unity will include these files in the build. I chose to put them in Assets/Resources/Shaders/Levels.

In the graphics settings, the tools we want are at the very bottom of the panel:

Unity Graphics Settings - Shader Preloading

For each level:

  1. Just before you start your level, click the Clear button.
  2. Play through the level, triggering as many things as you can. You want the game to load all possible shaders & shader variants for the level.
  3. When you’re done, click the Save to asset… button.
  4. Save the level’s collection to the folder we created above (Assets/Resources/Shaders/Levels) using the level name so we can find it later on. In my project this would be “map_01.shadervariants”, “map_02.shadervariants”, etc..

Once you’ve done that, you should have one “.shadervariants” file per level.

Keep in mind that if you change your level, you may need to do this all over again for that level. (This is why, in my opinion, this is a hacky solution. Unity doesn’t provide any real tools for managing these collections more intelligently.)

Using The Collections

Now that we have the files, we simply have to load them after we have loaded each level.

On the MonoBehaviour responsible for loading levels, I created a method called OnSceneFinishedLoading to be called when a scene is finished loading, and added it to the SceneManager’s sceneLoaded event:

Here’s the OnSceneFinishedLoading method:

I’m loading the collection from the Resources based on the name of the level. If the resource is found and is of the correct type, I call WarmUp on the shader variant collection to compile the shader variants. Finally, I unload the collection since we don’t need it anymore.

Unfortunately, the ShaderVariantCollection class is very basic and doesn’t give us access to the contents in any significant way. We cannot, for example, create a loading bar for it, get a list of the variants it contains, or get the details of the variants to do anything with them. If we had those capabilities it would be possible to build a more intelligent editor tool to work with them.

The Result

Doing things this way may result in a bit of a delay at the beginning of the level, but it’s less noticeable since we’re doing all the other level loading around the same time; it also prevents hitching during gameplay which is most important.

This may not be the only solution! As a relative newcomer to Unity and C#, it’s quite possible that I’ve missed something. If you have any suggestions or corrections, please feel free to contact me or comment below.

Write a Comment