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(' ');
}
//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]