|
一、 打包与变体收集
之前章节介绍了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[shaderName] = sd;
- }
- var passTypes = sd.PassTypes;
- if (!ShaderVariantDict.TryGetValue(shaderName, out var svlist))
- {
- svlist = new List<ShaderVariantCollection.ShaderVariant>();
- ShaderVariantDict[shaderName] = 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[4] as int[];
- passTypes = passtypes;
- //key word
- keywordLists = new string[passtypes.Length][];
- var kws = args[5] as string[];
- for (int i = 0; i < passtypes.Length; i++)
- {
- keywordLists[i] = kws[i].Split(' ');
- }
- //Remaning key word
- var rnkws = args[6] 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[i].shaderKeywordSet.IsEnabled(m_KeywordToStrip) && !EditorUserBuildSettings.development)
- {
- var foundKeywordSet = string.Join(” ”, data[i].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的一种收集方案
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|