前言
笔者先前在开发某个重度SLG项目时,由于项目中的大地图使用了实时光照,Lightmap,雾效等,导致光照相关的Shader变体很多,在打包时光是编译Shader变体就要几个小时,而且发现移动平台在运行时占用内存很大,光是Shader资源就占了1G多的内存,因此Shader变体裁剪势在必行;
必备知识
关于Shader变体的相关知识可查阅官方手册,笔者也是读官方手册研究出解决办法,一般能发现这个文档,对Shader变体也是有了解的,链接附上:https://docs.unity3d.com/Manual/shader-variants-and-keywords.html
里面说明了Keywords的相关原理以及unity在打包时对keywords的处理方式;
思路分析
Shader变体裁剪的核心思想是打包时只编译我们需要的变体,怎样确定我们需要哪些变体呢?我们知道,unity编辑器会自动记录游戏中当前已经用了的变体,如图所示:
因此,我们先运行游戏的相关场景,让unity收集相关的变体,再Save to Asset,获取当前收集到的变体,即上图的all文件;
然后,在打包编译Shader时,可以依据上面all文件来编译需要的Shader变体,Unity提供了接口让我们可以自定义Shader变体裁剪:
我们在这个接口中,根据all文件中记录的变体,针对性地编译我们需要的变体;
实现细节:
1. 首先,编辑器和真机跑游戏时,需要的变体不一定是相同的,不过还好笔者只发现了一处,就是:
注意项目不要开这个选项,如果开了,只在编辑器上有效,在移动平台是不生效的(笔者那时的项目是这样的),导致编辑器收集的变体,和移动平台需要的变体不匹配;
不排除有其他的情况,如果出了移动平台丢失Shader的情况,用FrameDebug分别查看编辑器下和移动平台下对应的Keywords,找出差别分析一般就可以发现问题:
2. 如果对项目的所有Shader开启变体裁剪,那么很麻烦,得将所有的Shader的变体都收集全,这是一项不可能的工作,比如一些特效,谁知道什么时候触发呢,因此,笔者只针对包含大量Keywords的Shader开启变体裁剪,其他的Shader就不开裁剪,默认全编译,这样我们只需要收集特定情况的Shader变体;
3. 由于项目一般有不同的品质,在不同的品质下,光照和阴影计算一般是不同的,由此就有不同的变体;因此我们运行游戏时,得在各个情况下设置各个品质,触发unity对变体的收集;
4. 收集各个情况的Shader变体可能得写编辑器代码支持,对于场景,我们只需要跑到那就好,但是场景上面可能有各个模型,而模型只在需要的时候加载,为了便于收集模型的Shader变体,可写编辑器代码,手动将所有模型全部实例化到场景,这样我们就可以收集所有的模型的Shader变体了;
5. 其他情况:
得确定代码是否有在某些特殊情况下开启某些关键字,防止变体没收集到;
注意Shader keywords的声明是否正确,笔者项目先前升级Unity引擎时,unity对keywords做了修改,导致老的Shader在编译后不能得到正确的shader变体,总之用FrameDebug调试就行了;
注意,在打AB包时,如果我们没有ForceRebuild,打包会使用缓存,一但此时我们要收集缺失的变体,打AB包不会触发Shader的编译,因为Shader资源没变化,只是我们的处理流程改了。一般正式上线的项目,打AB得开ForceRebuild,否则可能由于缓存导致资源没重新打;
成效:
笔者的项目,在windows平台下跑游戏收集相关变体,可以直接应用到移动平台上,如安卓和ios,相当于只要在windows平台下收集一趟就好,还是很稳的。
使用方式:
笔者在最后面会将源码公布,大家可以直接下载源码导入到项目中,会有如下工具:
Save ShaderVariants,保存游戏当前收集到的变体,相当于:
Merge ShaderVariants,合并Shader变体:
当我们有新收集到的Shader变体时,需要将新收集到的和到all里面:
执行Merge ShaderVariants,就会帮我们合并;
提取需要预热的变体,保存到Resources文件夹下:
有时候我们需要预热某些Shader的变体,我们可以使用这个工具;
在我们收集到的所有Shader的变体中,提取我们指定的Shader的变体;
附加代码
#define SAVE_SHADER_INFO
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Rendering;
using UnityEngine;
using UnityEngine.Rendering;
using static UnityEngine.ShaderVariantCollection;
public class ShaderVariantsStrip : IPreprocessShaders
{
public const string ShaderVariantsCollectPath = "Assets/ShaderVariantsForStrip/all.shadervariants";
//这几个变体比较多的shader,只保留 在编辑器下跑游戏时 收集到的变体
public static HashSet<string> stripShaderNames = new HashSet<string> {
"FMGame/Lit",
"FMGame/Simple Lit",
"FMGame/Cloud",
"FMGame/Environment/Lit_Pond",
"FMGame/Versatile Blend",
"FMGame/Simple Lit HAlphaBlend",
"FMGame/FMLit_Scanning",
"FMGame/VFX/Unlit_Hologram",
"FMGame/GpuInstancesLit",
"FMGame/GpuInstancesSimpleLit",
"FMGame/Simple Lit GPUInstancing",
"FMGame/Lit GPUInstancing",
"FMGame/Lit_NoiseFlow",
"FMGame/VFX/Lit_BuildingScan",
"FMGame/Lit GPUInstancing Snow",
"FMGame/Simple Lit GPUInstancing Snow",
"FMGame/Lit_Snow",
"FMGame/Lit_Wind",
"FMGame/Lit_Outline",
"FMGame/SimpleLit_Snow",
"FMGame/Simple Lit Wind",
"FMGame/Environment/Lit_Pond_Snow",
"GPUSkinning/GPUSkinning_Outline",
"GPUSkinning/GPUSkinning_Lit",
"GPUSkinning/GPUSkinning_Lit_Skin4",
"GPUSkinning/GPUSkinning_SimpleLit",
"GPUSkinning/GPUSkinning_SimpleLit_Skin4",
"GPUSkinning/GPUSkinning_Sample_Unlit",
"GPUSkinning/GPUSkinning_Unlit",
"GPUSkinning/GPUSkinning_Unlit_Skin4",
"GPUSkinning/GPUSkinningUnlitSkin4_Outline",
"GPUSkinning/GPUSkinningUnlitSkin4_Shadow",
"MTE/URP/3 Textures2",
"MTE/URP/4 Textures",
"MTE/URP/5 Textures",
"MTE/URP/TextureArray2Tex_2Normal",
"MTE/URP/TextureArray3Tex_3Normal",
"MTE/URP/TextureArray4Tex_4Normal_Simplified",
"MTE/URP/TextureArray4Tex_4Normal",
"MTE/URP/TextureArray5Tex_5Normal",
"MTE/URP/TextureArray5Tex_6Normal",
"MTE/URP/TextureArray2Tex_NoNormal",
"MTE/URP/TextureArray3Tex_NoNormal",
"MTE/URP/TextureArray4Tex_NoNormal",
"MTE/URP/TextureArray4Tex_NoNormal_Simplified",
"MTE/URP/TextureArray5Tex_NoNormal",
"MTE/URP/TextureArray6Tex_NoNormal",
"MTE/URP/TextureArray8Tex_NoNormal",
"MTE/URP/TextureArray8Tex_8Normal",
"MTE/URP/TextureArray8Tex_8Normal_Simplified",
"MTE/URP/TextureArray8Tex_NoNormal_Simplified",
"Universal Render Pipeline/Lit",
"Universal Render Pipeline/Complex Lit",
"Universal Render Pipeline/Simple Lit",
"Shader Graphs/PhysicalMaterial3DsMax",
"efronli/UV",
"efronli/UV_b",
"efronli/NQ_whater",
"efronli/UV_Alpha Blended",
"Effect_Mid/Additive",
"Effect_Mid/Alpha Blend",
"Legacy Shaders/Diffuse",
"VFX/ComDissovle",
"JYi/round_VertexColorContrel",
"Kero/Alpha-Blended_HDR_URP",
"Hidden/TerrainEngine/Details/UniversalPipeline/BillboardWavingDoublePass",
"Hidden/TerrainEngine/Details/UniversalPipeline/WavingDoublePass",
"Hidden/TerrainEngine/Details/UniversalPipeline/Vertexlit",
"Hidden/Internal-PrePassLighting",
"Hidden/Internal-DeferredShading",
"Hidden/Universal Render Pipeline/UberPost",
"Hidden/Universal Render Pipeline/StencilDeferred",
};
#region for Build
private Dictionary<Shader, Dictionary<PassType, List<HashSet<string>> > > m_shader2keywords = new Dictionary<Shader, Dictionary<PassType, List<HashSet<string>> > >();
private static ShaderVariantsStrip m_ins = null;
public ShaderVariantsStrip()
{
m_ins = this;
ShaderVariantCollection shaderVariantCollection = AssetDatabase.LoadAssetAtPath<ShaderVariantCollection>(ShaderVariantsCollectPath);
if (shaderVariantCollection == null)
return;
SerializedObject serializedObject = new SerializedObject(shaderVariantCollection);
SerializedProperty m_Shaders = serializedObject.FindProperty("m_Shaders");
for (var i = 0; i < m_Shaders.arraySize; ++i)
{
var entryProp = m_Shaders.GetArrayElementAtIndex(i);
Shader shader = (Shader)entryProp.FindPropertyRelative("first").objectReferenceValue;
if (shader != null)
{
if(!m_shader2keywords.TryGetValue(shader, out Dictionary<PassType, List<HashSet<string>> >passType2keywords))
{
passType2keywords = new Dictionary<PassType, List<HashSet<string>> >();
m_shader2keywords.Add(shader, passType2keywords);
}
var variantsProp = entryProp.FindPropertyRelative("second.variants");
for (var j = 0; j < variantsProp.arraySize; ++j)
{
var prop = variantsProp.GetArrayElementAtIndex(j);
var keywordsStr = prop.FindPropertyRelative("keywords").stringValue;
keywordsStr.Replace("\r\n", string.Empty);
string[] keywordArray = keywordsStr.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
var passType = (PassType)prop.FindPropertyRelative("passType").intValue;
if(!passType2keywords.TryGetValue(passType, out List<HashSet<string>> keywordSetList))
{
keywordSetList = new List<HashSet<string>>();
passType2keywords.Add(passType, keywordSetList);
}
HashSet<string> keywordSet = new HashSet<string>();
foreach (var item in keywordArray)
{
keywordSet.Add(item);
}
keywordSetList.Add(keywordSet);
}
}
}
}
private StringBuilder stringBuilder = new StringBuilder();
private Dictionary<string, List<string>> shader2Varaints = new Dictionary<string, List<string>>();
public static void SaveBuildInfo(bool IsBuildAssetBundle)
{
if (m_ins != null)
{
foreach (var pair in m_ins.shader2Varaints)
{
foreach (var item in pair.Value)
{
m_ins.stringBuilder.AppendLine(pair.Key + ": " + item);
}
}
if (IsBuildAssetBundle)
File.WriteAllText("Assets/BuildShaderInfo_AssetBundle.txt", m_ins.stringBuilder.ToString());
else
File.WriteAllText("Assets/BuildShaderInfo_Resources.txt", m_ins.stringBuilder.ToString());
}
}
public int callbackOrder => -1000;
Dictionary<ShaderCompilerPlatform, List<HashSet<string>>> m_Platform2ValidKeywordSetList = new Dictionary<ShaderCompilerPlatform, List<HashSet<string>>>();
List<string> m_currentkeywords = new List<string>();
HashSet<ShaderCompilerPlatform> m_platforms = new HashSet<ShaderCompilerPlatform>();
List<HashSet<string>> m_CachHashSets = new List<HashSet<string>>();
private HashSet<string> GetHashSet()
{
HashSet<string> ret;
int count = m_CachHashSets.Count;
if (count > 0)
{
ret = m_CachHashSets[count - 1];
m_CachHashSets.RemoveAt(count - 1);
}
else
{
ret = new HashSet<string>();
}
return ret;
}
private void ReleaseHashSet(List<HashSet<string>> hashSetList)
{
foreach (var hashSet in hashSetList)
{
hashSet.Clear();
m_CachHashSets.Add(hashSet);
}
hashSetList.Clear();
}
/* 测试代码
[MenuItem("Build/Build Shader")]
static void BuildShader()
{
string path = "AssetBundles";
if(Directory.Exists(path))
Directory.Delete(path, true);
Directory.CreateDirectory(path);
BuildPipeline.BuildAssetBundles(path, new AssetBundleBuild[] {
new AssetBundleBuild()
{
assetNames = new string[]{ "Assets/MyShader/TestShader.shader"},
assetBundleName = "test"
}
}, BuildAssetBundleOptions.ForceRebuildAssetBundle, EditorUserBuildSettings.activeBuildTarget);
}
*/
public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
{
m_platforms.Clear();
foreach (var item in data)
{
if(!m_platforms.Contains(item.shaderCompilerPlatform))
{
m_platforms.Add(item.shaderCompilerPlatform);
}
}
m_Platform2ValidKeywordSetList.Clear();
foreach (var platform in m_platforms)
{
if(m_Platform2ValidKeywordSetList.TryGetValue(platform, out List<HashSet<string>> m_ValidKeywordSetList))
{
ReleaseHashSet(m_ValidKeywordSetList);
}
else
{
m_ValidKeywordSetList = new List<HashSet<string>>();
m_Platform2ValidKeywordSetList.Add(platform, m_ValidKeywordSetList);
}
if (m_shader2keywords.TryGetValue(shader, out Dictionary<PassType, List<HashSet<string>>> passType2keywords))
{
if (passType2keywords.TryGetValue(snippet.passType, out List<HashSet<string>> keywordSetList))
{
foreach (var keywordSet in keywordSetList)
{
HashSet<string> newSet = GetHashSet();
foreach (var keyword in keywordSet)
{
LocalKeyword localKeyword = new LocalKeyword(shader, keyword);
if (ShaderUtil.PassHasKeyword(shader, snippet.pass, localKeyword, snippet.shaderType, platform))
newSet.Add(keyword);
}
m_ValidKeywordSetList.Add(newSet);
}
}
}
}
bool needStrip = checkNeedStripShader(shader.name);
for (int i = data.Count - 1; i >= 0; --i)
{
ShaderKeywordSet shaderKeywordSet = data[i].shaderKeywordSet;
ShaderKeyword[] shaderKeywords = shaderKeywordSet.GetShaderKeywords();
m_currentkeywords.Clear();
if (shaderKeywords.Length != 0)
{
for (int j = 0; j < shaderKeywords.Length; j++)
{
string key = shaderKeywords[j].name;
m_currentkeywords.Add(key);
}
}
bool need = false;
if (needStrip)
{
bool needflag = false;
List<HashSet<string>> m_ValidKeywordSetList = m_Platform2ValidKeywordSetList[data[i].shaderCompilerPlatform];
foreach (var set in m_ValidKeywordSetList)
{
if(m_currentkeywords.Count == set.Count)
{
bool flag = true;
foreach (var keyword in m_currentkeywords)
{
if(!set.Contains(keyword))
{
flag = false;
break;
}
}
if(flag)
{
needflag = true;
break;
}
}
}
need = needflag;
}
else
{
need = true;
}
if (!need)
data.RemoveAt(i);
#if SAVE_SHADER_INFO
if(need)
{
string str1 = snippet.shaderType + ", " + snippet.passType + ", " + snippet.passName + ": ";
string str2 = string.Empty;
foreach (var item in m_currentkeywords)
{
str2 += item + ", ";
}
List<string> list;
if(!shader2Varaints.TryGetValue(shader.name, out list))
{
list = new List<string>();
shader2Varaints[shader.name] = list;
}
list.Add(str1 + str2);
}
#endif
}
}
private bool checkNeedStripShader(string shaderName)
{
if (stripShaderNames.Contains(shaderName))
return true;
return false;
}
#endregion
#region for Tools
[MenuItem("ShaderVariants/Save ShaderVariants")]
static void SaveShaderVariants()
{
string dir = Path.GetDirectoryName(ShaderVariantsCollectPath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
string message = "Save shader variant collection";
string assetPath = EditorUtility.SaveFilePanelInProject("Save Shader Variant Collection", "NewShaderVariants", "shadervariants", message, dir);
if (!string.IsNullOrEmpty(assetPath))
{
Type type = typeof(ShaderUtil);
MethodInfo methodInfo = type.GetMethod("SaveCurrentShaderVariantCollection", BindingFlags.Static | BindingFlags.NonPublic);
methodInfo.Invoke(null, new object[] { assetPath });
}
}
/// <summary>
/// 合并收集到的shader变体
/// </summary>
[MenuItem("ShaderVariants/Merge ShaderVariants")]
static void MergeShaderVariants()
{
string rootPath = "Assets/ShaderVariantsForStrip";
string allName = "all.shadervariants";
string allPath = rootPath + "/" + allName;
ShaderVariant shaderVariant = new ShaderVariant();
ShaderVariantCollection allCollection = new ShaderVariantCollection();
List<string> keywordList = new List<string>();
string[] fileNames = Directory.GetFiles(rootPath, "*.shadervariants");
foreach (var fileName in fileNames)
{
ShaderVariantCollection curCollection = AssetDatabase.LoadAssetAtPath<ShaderVariantCollection>(fileName);
SerializedObject serializedObject = new SerializedObject(curCollection);
SerializedProperty m_Shaders = serializedObject.FindProperty("m_Shaders");
for (var i = 0; i < m_Shaders.arraySize; ++i)
{
var entryProp = m_Shaders.GetArrayElementAtIndex(i);
Shader shader = (Shader)entryProp.FindPropertyRelative("first").objectReferenceValue;
if (shader != null)
{
shaderVariant.shader = shader;
var variantsProp = entryProp.FindPropertyRelative("second.variants");
for (var j = 0; j < variantsProp.arraySize; ++j)
{
var prop = variantsProp.GetArrayElementAtIndex(j);
var keywordsStr = prop.FindPropertyRelative("keywords").stringValue;
keywordsStr.Replace("\n", string.Empty);
keywordsStr.Replace("\r\n", string.Empty);
string[] keywordArray = keywordsStr.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
shaderVariant.keywords = keywordArray;
var passType = (UnityEngine.Rendering.PassType)prop.FindPropertyRelative("passType").intValue;
shaderVariant.passType = passType;
if (!allCollection.Contains(shaderVariant))
allCollection.Add(shaderVariant);
//自动添加非INSTANCING_ON变体
keywordList.Clear();
keywordList.AddRange(keywordArray);
if (keywordList.Remove("INSTANCING_ON"))
{
string[] keywords = keywordList.ToArray();
shaderVariant.keywords = keywords;
if (!allCollection.Contains(shaderVariant))
allCollection.Add(shaderVariant);
}
}
}
}
}
AssetDatabase.CreateAsset(allCollection, allPath);
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
}
static HashSet<string> NeedWarmShaders = new HashSet<string>
{
"FMGame/Lit",
"FMGame/Simple Lit",
"FMGame/Cloud",
"FMGame/Environment/Lit_Pond",
"FMGame/Versatile Blend",
"FMGame/Simple Lit HAlphaBlend",
"FMGame/FMLit_Scanning",
"FMGame/VFX/Unlit_Hologram",
"FMGame/GpuInstancesLit",
"FMGame/GpuInstancesSimpleLit",
"FMGame/Simple Lit GPUInstancing",
"FMGame/Lit GPUInstancing",
"FMGame/Lit_NoiseFlow",
"FMGame/VFX/Lit_BuildingScan",
"FMGame/Lit GPUInstancing Snow",
"FMGame/Simple Lit GPUInstancing Snow",
"FMGame/Lit_Snow",
"FMGame/Lit_Wind",
"FMGame/Lit_Outline",
"FMGame/SimpleLit_Snow",
"FMGame/Simple Lit Wind",
"FMGame/Environment/Lit_Pond_Snow",
"MTE/URP/3 Textures2",
"MTE/URP/4 Textures",
"MTE/URP/5 Textures",
"MTE/URP/TextureArray2Tex_2Normal",
"MTE/URP/TextureArray3Tex_3Normal",
"MTE/URP/TextureArray4Tex_4Normal_Simplified",
"MTE/URP/TextureArray4Tex_4Normal",
"MTE/URP/TextureArray5Tex_5Normal",
"MTE/URP/TextureArray5Tex_6Normal",
"MTE/URP/TextureArray2Tex_NoNormal",
"MTE/URP/TextureArray3Tex_NoNormal",
"MTE/URP/TextureArray4Tex_NoNormal",
"MTE/URP/TextureArray4Tex_NoNormal_Simplified",
"MTE/URP/TextureArray5Tex_NoNormal",
"MTE/URP/TextureArray6Tex_NoNormal",
"MTE/URP/TextureArray8Tex_NoNormal",
"MTE/URP/TextureArray8Tex_8Normal",
"MTE/URP/TextureArray8Tex_8Normal_Simplified",
"MTE/URP/TextureArray8Tex_NoNormal_Simplified",
"Hidden/Universal Render Pipeline/UberPost",
};
/// <summary>
/// 只保留需要裁剪的变体
/// </summary>
[MenuItem("ShaderVariants/提取需要预热的变体,保存到Resources文件夹下")]
static void FilterStripShaderVariants()
{
string allPath = ShaderVariantsStrip.ShaderVariantsCollectPath;
string resoursePath = "Assets/Resources/ShaderVariants/all.shadervariants";
if (!AssetDatabase.CopyAsset(allPath, resoursePath))
{
Debug.LogError($"复制{allPath}到{resoursePath}失败");
return;
}
AssetDatabase.Refresh();
ShaderVariantCollection allCollection = AssetDatabase.LoadAssetAtPath<ShaderVariantCollection>(resoursePath);
if (allCollection == null)
{
return;
}
SerializedObject serializedObject = new SerializedObject(allCollection);
SerializedProperty m_Shaders = serializedObject.FindProperty("m_Shaders");
for (var i = m_Shaders.arraySize - 1; i >= 0; --i)
{
var entryProp = m_Shaders.GetArrayElementAtIndex(i);
var firstProperty = entryProp.FindPropertyRelative("first");
Shader shader = (Shader)firstProperty.objectReferenceValue;
if (shader != null)
{
if (!NeedWarmShaders.Contains(shader.name))
m_Shaders.DeleteArrayElementAtIndex(i);
}
else
{
m_Shaders.DeleteArrayElementAtIndex(i);
}
}
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(allCollection);
AssetDatabase.SaveAssets();
}
#endregion
}