ues6858 发表于 2024-7-15 18:34

Unity Shader资源(二)项目措置

一、 打包与变体收集

之前章节介绍了Shader变体的生成原因,为了让项目能够正常流畅运行,就需要考虑运行时的Shader资源打包问题。首先需要保证Shader变体不会丢掉,其次包体要尽可能小。对于Shader资源的打包很容易想到以下几种方案
生成方式长处错误谬误Shader跟随Material不会丢掉变体极大增加资源冗余Shader单独打包,全部使用multi_compile不会丢掉变体存在不用的变体Shader单独打包,自主收集变体信息极简的包体容易丢掉包体出于性能考虑,凡是会选择第三种方案,收集变体数据就是此中关键。常见的思路就是遍历所有打包资源,提取所有Scene、Prefab所引用的Material对其进行变体收集。ShaderVariantCollection除了能够保证变体数据不会丢掉,凡是还会在游戏启动时进行WarmUp。因此最好将mutli的变体数据也写入此中。(打包Shader时不需要手动引用HLSL文件,打包编译Shader时Unity会自动措置)
思路理清楚了,下面就是实现问题了。在Unity 2022中引入了Material Variants,能够便利得措置变体收集。但在之前的版本只能在了解源码的情况下,使用反射获取Unity内置方式来措置。
只给到关键的Material收集数据方式。核心思路是收集所有变体数据后,将材质的keyword与Shader的keyword求交集。Material中的Keyword可能并不是Shader中使用的(凡是由于切换Shader、Toogle生成当地Keyword造成),因此需要进行过滤。
    //shader数据的缓存
    static Dictionary<string, ShaderData> ShaderDataDict = new Dictionary<string, ShaderData>();

    static Dictionary<string, List<ShaderVariantCollection.ShaderVariant>> ShaderVariantDict = new Dictionary<string, List<ShaderVariantCollection.ShaderVariant>>();

    //添加Material计算
    static List<string> passShaderList = new List<string>();
    static MethodInfo GetShaderVariantEntries = null;
    public struct ShaderVariantData
    {
      public int[] passTypes;
      public string[] keywordLists;
      public string[] remainingKeywords;
    }

    /// <summary>
    /// 收集单个材质
    /// </summary>
    /// <param name=”curMat”></param>
    static void AddToDict(Material curMat)
    {
      Shader shader = curMat.shader;
      if (!curMat || !curMat.shader) return;

      string shaderName = shader.name;
      if (!ShaderDataDict.TryGetValue(shaderName, out ShaderData sd))
      {
            sd = GetShaderKeywords(shader);
            ShaderDataDict = sd;
      }

      var passTypes = sd.PassTypes;
      if (!ShaderVariantDict.TryGetValue(shaderName, out var svlist))
      {
            svlist = new List<ShaderVariantCollection.ShaderVariant>();
            ShaderVariantDict = svlist;
      }

      // 过滤有效keywords
      List<string> voildkeywords = new List<string>();
      List<string> allkeywords = new List<string>();
      foreach (string keyword in curMat.shaderKeywords)
      {
            foreach (var KeyWords in sd.KeyWords)
            {
                allkeywords = allkeywords.Union(KeyWords).ToList();
                if (KeyWords.Contains(keyword) && !voildkeywords.Contains(keyword))
                  voildkeywords.Add(keyword);
            }
      }
      Debug.Log($”Shader:{curMat.shader.name}\n\rallkeywords:{allkeywords.ListToString(” ”)}\n\rvoildkeywords:{voildkeywords.ListToString(” ”)}”);

      // 材质与Shader的keyword求交
      foreach (PassType o in Enum.GetValues(typeof(PassType)))
      {
            var pt = o;
            ShaderVariantCollection.ShaderVariant? sv = null;
            try
            {
                if (curMat.shaderKeywords.Length > 0)
                {
                  sv = new ShaderVariantCollection.ShaderVariant(shader, pt, curMat.shaderKeywords);
                }
                else
                {
                  sv = new ShaderVariantCollection.ShaderVariant(shader, pt);
                }
            }
            catch (Exception e)
            {
                //Debug.LogErrorFormat(”{0}-当前shader不存在变体(可以无视):{1}-{2}”, curMat.name, pt, JsonMapper.ToJson(curMat.shaderKeywords));
                continue;
            }

            //判断sv 是否存在,不存在则添加
            if (sv != null)
            {
                bool isContain = false;
                var _sv = (ShaderVariantCollection.ShaderVariant)sv;
                foreach (var val in svlist)
                {
                  if (val.passType == _sv.passType && System.Linq.Enumerable.SequenceEqual(val.keywords, _sv.keywords))
                  {
                        isContain = true;
                        break;
                  }
                }

                if (!isContain)
                {
                  svlist.Add(_sv);
                }
            }
      }
    }

    /// <summary>
    /// 获取所有Shader变体数据
    /// </summary>
    static void GetShaderVariantEntriesFiltered(Shader shader, string[] filterKeywords, out int[] passTypes, out string[][] keywordLists, out string[] remainingKeywords)
    {
      //2019.3接口
      //            internal static void GetShaderVariantEntriesFiltered(
      //                Shader                  shader,                     0
      //                int                     maxEntries,               1
      //                string[]                filterKeywords,             2
      //                ShaderVariantCollection excludeCollection,          3
      //                out int[]               passTypes,                  4
      //                out string[]            keywordLists,               5
      //                out string[]            remainingKeywords)          6
      if (GetShaderVariantEntries == null)
      {
            GetShaderVariantEntries = typeof(ShaderUtil).GetMethod(”GetShaderVariantEntriesFiltered”, BindingFlags.NonPublic | BindingFlags.Static);
      }

      passTypes = new int[] { };
      keywordLists = new string[][] { };
      remainingKeywords = new string[] { };
      if (toolSVC != null)
      {
            var _passtypes = new int[] { };
            var _keywords = new string[] { };
            var _remainingKeywords = new string[] { };
            object[] args = new object[] { shader, 1024, filterKeywords, toolSVC, _passtypes, _keywords, _remainingKeywords };
            GetShaderVariantEntries.Invoke(null, args);

            var passtypes = args as int[];
            passTypes = passtypes;
            //key word
            keywordLists = new string[];
            var kws = args as string[];
            for (int i = 0; i < passtypes.Length; i++)
            {
                keywordLists = kws.Split(&#39; &#39;);
            }

            //Remaning key word
            var rnkws = args as string[];
            remainingKeywords = rnkws;
      }
    }


二、 Shader变体剔除

2.1 Editor Shader Stripping



默认情况下,Unity会对没有使用Shader变体进行剔除,若某些变体是运行时代码措置的(代码开启keyword,但建议这种情况不要使用feature),这样在打包时可能就会呈现变体丢掉问题。
Unity对于Shader变体区分了三种情况Lightmap、Fog、以及其它。对于Lightmap、Fog给出更加细致的剔除设置。其它的Shader变体也给到了三种措置方式:Strip Unused(默认)、Strip All(剔除所有变体)、 Keep All(保留所有变体),凡是这部门不用改动。


2.2 Script Shader Stripping

构建Shader会将Shader变体数据传入IPreprocessShaders.OnProcessShader,可以按照keyword手动剔除不想使用的变体,这样可以减少编译时间和包体大小。分歧项目的都有各自的需求,官方给到一个例子:按照方针平台设置决定是否启用某些keyword,某些效果是用来查看衬着数据的就可以这样操作。
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEditor.Build;
using UnityEditor.Rendering;

class ShaderDebugBuildPreprocessor : IPreprocessShaders
{
    ShaderKeyword m_KeywordToStrip;

    public ShaderDebugBuildPreprocessor()
    {
      m_KeywordToStrip = new ShaderKeyword(”DEBUG”);
    }

    // Use callbackOrder to set when Unity calls this shader preprocessor. Unity starts with the preprocessor that has the lowest callbackOrder value.
    public int callbackOrder { get { return 0; } }

    public void OnProcessShader(
      Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
      {

      for (int i = 0; i < data.Count; ++i)
      {
            if (data.shaderKeywordSet.IsEnabled(m_KeywordToStrip) && !EditorUserBuildSettings.development)
            {
                var foundKeywordSet = string.Join(” ”, data.shaderKeywordSet.GetShaderKeywords());
                Debug.Log(”Found keyword DEBUG in variant ” + i + ” of shader ” + shader);
                Debug.Log(”Keyword set: ” + foundKeywordSet);
                data.RemoveAt(i);
                --i;
            }
      }
    }
}
三、 优化建议

将不需要的Shader变体从编译中剔除(Strip)能够优化项目性能。如果剔除了运行时需要的变体,Unity会加载较为接近的变体。为了避免变体被不测剔除,需要注意以下几点:

[*]使用shader_feature声明的宏,不要在运行时对其进行控制(建议查抄所有cs中措置的keyword,有些坑很难在Frame Debug中发现 )
[*]可以使用ShaderVariantCollection对Shader变体进行收集,并一起打包,避免变体被丢弃。
[*]同一KeyWord在分歧Pass中要统必然义方式
在声明宏时,需要注意以下几点以优化变体:

[*]shader_feature按照资源引用进行变体收集,对比 multi_compile有更少的变体。功能允许的情况下,优先使用shader_feature
[*]不要定义未使用的keyword
[*]针对shader stage声明宏
[*]可以使用preprocessor macros针对分歧方针平台进行措置
#if SHADER_API_DESKTOP
   #pragma multi_compile SHADOWS_LOW SHADOWS_HIGH
   #pragma multi_compile REFLECTIONS_LOW REFLECTIONS_HIGH
   #pragma multi_compile CAUSTICS_LOW CAUSTICS_HIGH
#elif SHADER_API_MOBILE
   #pragma multi_compile QUALITY_LOW QUALITY_HIGH
   #pragma shader_feature CAUSTICS // Uses shader_feature, so Unity strips variants that use CAUSTICS if there are no Materials that use the keyword at build time.
#endif

[*]开启Shader变体的强制匹配,便于发现Shader变体丢掉问题:PlayerSettings.strictShaderVariantMatching
[*]启动时执行变体预热,避免初度加载Shader时造成卡顿体验(新版本才有异步接口,略坑)
[*]技术条件允许的情况下,不要使用Unity内置的尺度Shader,例如”Universal Render Pipeline/Lit”之类的,变体数量极大。

参考


[*]Shaders core concepts
[*]Shader Stripping
[*]【Unity3D】Shader变体打点流程-变体剔除
[*]【Unity3D】Shader变体打点流程2-变体收集
[*]张鑫:一种Shader变体收集和打包编译优化的思路
[*]lfh:Unity SVC的一种收集方案
页: [1]
查看完整版本: Unity Shader资源(二)项目措置