Search Unity

How to generate shader variants without having to play my entire game

Discussion in 'Shaders' started by zornor90, Jan 2, 2019.

  1. zornor90

    zornor90

    Joined:
    Sep 16, 2015
    Posts:
    169
    As part of my game, I stream room prefabs at runtime from asset bundles. I've found that one of the biggest causes of lag is the initial shader load for any new shaders that the prefabs use. I've got a pipeline set up to load shaders from a variant collection, but I was wondering if there was a way to automate the variant collection generation

    Currently, I am having to run through the game before building in order to grab all the shader variants that are used in my level to create the collection to preload. However, I would love to be able to automate this as it is tedious to do this each time, as well as makes modding a lot harder. Would it be possible to just grab the materials, particles, etc inside of a room prefab at build time, and figure out what shader variants to generate based on that? I am currently running Unity 2018.2

    I threw together a possible way to generate them, however I am stumped by where I get the pass type from as per the constructor: https://docs.unity3d.com/ScriptReference/ShaderVariantCollection.ShaderVariant.html
    As far as I can tell this would require converting this: https://docs.unity3d.com/ScriptReference/Material.GetPassName.html
    to this: https://docs.unity3d.com/ScriptReference/Rendering.PassType.html

    Code (CSharp):
    1. private void GenerateCollection() {
    2.         GameObject[] precombines = PrecombineAssetMap.GetAllPrecombines();
    3.         ShaderVariantCollection newCollection = new ShaderVariantCollection();
    4.         for (int i = 0; i < precombines.Length; i++) {
    5.           Renderer[] renderers = precombines[i].GetComponentsInChildren<Renderer>();
    6.           for (int j = 0; j < renderers.Length; j++) {
    7.             Material[] mats = renderers[j].sharedMaterials;
    8.             for (int k = 0; k < mats.Length; k++) {
    9.               var variants = GetShaderVariants(mats[k]);
    10.               for (int x = 0; x < variants.Length; x++) {
    11.                 if (!newCollection.Contains(variants[x])) {
    12.                   newCollection.Add(variants[x]);
    13.                 }
    14.               }
    15.             }
    16.           }
    17.         }
    18.       }
    19.  
    20.       private ShaderVariantCollection.ShaderVariant[] GetShaderVariants(Material m) {
    21.         var collection = new ShaderVariantCollection.ShaderVariant[m.passCount];
    22.         for (int i = 0; i < m.passCount; i++) {
    23.         // Just using 0 as a placeholder here, need to figure out how to actually get it from the material
    24.           collection[i] = new ShaderVariantCollection.ShaderVariant(m.shader, 0, m.shaderKeywords);
    25.            
    26.         }
    27.         return collection;
    28.       }
    Thanks!

    EDIT: Looks like I'm not the only one with this question: https://forum.unity.com/threads/shadervariantcollection-best-practises.455447/#post-2952500
     
    Last edited: Jan 2, 2019
  2. zornor90

    zornor90

    Joined:
    Sep 16, 2015
    Posts:
    169
    Bump, I've done some research but not found anything about how to get the passes needed from the materials
     
  3. FiveFingerStudios

    FiveFingerStudios

    Joined:
    Apr 22, 2016
    Posts:
    383
    Did you ever solve this? I'm having the same issue.
     
  4. zornor90

    zornor90

    Joined:
    Sep 16, 2015
    Posts:
    169
    So here's what ended up happening:

    The variant direction was not the way to go. The issue was that I had lag each time a room was streamed in because I was using asset bundles, and I didn't have the shaders I needed grouped in a shared bundle, each asset bundle included the shaders separately meaning each time a room loaded it loaded yet another copy of all the shaders causing that laggy Shader.Parse() call (why this isn't threaded I guess we'll never know.)

    This meant that each of my room asset bundles contained all of the shaders needed. So if a room used the Standard shader, it would create a duplicate instead of using the one loaded in memory. This is because of how asset bundles work - at runtime the bundle either depends on another asset bundle for a dependency or includes it. If you don't specify a shared asset bundle with your dependent assets they will all be duplicated.

    The fix for this is to create a shared asset bundle with all the shaders you need that gets loaded first. There is one catch - If you need to use the Standard unity shaders (as opposed to a custom shader) you can download them from Unity's website and import them into your project as assets, then assign them to your materials instead of the built in standard shaders, and then put those downloaded ones into the shared asset bundle.

    You can also use the new addressable system which will automatically handle all your shader stuff.
     
    Last edited: Oct 15, 2019
  5. FiveFingerStudios

    FiveFingerStudios

    Joined:
    Apr 22, 2016
    Posts:
    383
    Thanks for the reply and letting me know the solution you went with.

    I'm currently not using asset bundles, so I've gone a different route. I had the same root cause issue in the profiler.... Shader.parse() was compiling the shaders as the game loaded in each new scene asynchronously.

    I orginally was trying to load only the ShaderVarients needed for each scene, but its very hard to know which ones are actually needed. The naming conventions don't quiest match up.

    For now, I actually have Unity putting all used shaders and variants into one Shader Varient Collection and loading it at the start of the game. I'm crossing my fingers and hoping that I'll be able to ship my game like that....we will see.

    Again, thanks for the detailed response.