问题表象
在游戏的性能优化过程中,我们发现ShaderLab的内存占用很大,同时我们可以确定我们没有使用Standard的shader;然后我们扫描了我们游戏每个Shader的变体,发现某些shader的变体多的令人惊恐;这些变体众多的shader不仅影响首次提交到硬件时的编译时间,还对内存的占用有着巨大的关系,一个变体几十个的shader可能内存占用不多,也就几十kb不到,但是一个上千变体的shader其内存则是数兆不止,我们优化过的单个shader其变体由之前的数千个降低为几百个时时,其ShaderLab内存足足减少了几十兆。因此在某些时刻优化一下shader变体还是很有必要的,这里也只会聊一些关于变体的事情,其他的暂且不论。
方法
我们知道,使用#pragma multi_compile或者是shader_feature就可以创建不同的shader变体,我们通过Shader.EnableKeyword和Shader.DisableKeyword来控制需要启动哪些shader变体,而multi_compile和shader_feature是有一些区别的,multi_compile无论你的变体用没有用的都会编译生成,而shader_feature则是不用到的不会编译出来,但是对打包可能会麻烦点。所以我们游戏基本就没有用shader_feature。对于多种变体的网上其实也有蛮多的帖子,这里就不怎么赘述了,简单的说两句吧,以下一段伪码为例:
#pragma multi_compile Key_A1 Key_A2
#pragma multi_compile Key_B1 Key_B2
这里就会产生2*2=4个变体;#pragma multi_compile 越多连乘得到的变体数也就越大,对于某些为了节省Key数量的,一般会用__来代替,这样可以多省几个Key(对于5.x之前unity似乎就用一个int64的mask值按位来设置Key的,所以其容量也就只有64,而且unity本身占据了一些位置,5.x以后的版本Key的容量则扩展到256不过同样默认占据了一些位置)。因此这个时候我们就应该合理的规划Key的数量的,即你有没有必要需要这么一个Key选项,例如你定义了三个keymulti_compile Key_A1 Key_A2 Key_A3 如果你减掉一个Key_A3那么变体数就能减三分之一了,而变体的数量其实还跟你的SubShader的个数相关,变体数是每个SubShader中的变体数相加的,对于有一些shader而言,可能会为了支持不同的平台写上几个subshader,例如专门针对vulkan的,或者专门针对苹果Meta的;这个时候我们可以将这些shader进行分拆为多个单一的平台的shader,不同平台打包不同的shader即可,因为我们的目标平台可能并不需要另一个平台的shader这些,减少一个subshader就可以减少很多变体。
上面说到的大部分是我们自己定义的vertex/fragment shader,但有时候我们还会用到unity的surface shader,这个时候你可能会发现,我啥multi_compile都没写啊,怎么会有那么多的变体呢?其实你只要新建一个SurfaceShader看一下,啥都不写,你的cube就有阴影,而一般自己写的如果不写Tags { "LightMode" = "ShadowCaster" }则是不会产生阴影的。这是因为unity的surfaceshader为我们做了很多很多的事情,不信你注释掉FallBack试试,变体数立马减去一大半。surface可以支持很多通道,如ForwardBase,ForwardAdd,PrePassBase,Deferred等,这个我们可以通过shader的Show generated code来查看,搜索"LightMode"可以知道有哪些通道是生成了。对于Unity手游而言我们大多都是使用Forward渲染方式,因此对于Deferred方式通道可以直接关闭。例如我们新创建一个Surface其伪码代码如下,其变体数为55:
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
...
}
但是我们只需要在其后增加一些指令删除deferred通道即可减少4个变体,代码如下:
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows exclude_path:deferred
...
}
当然一般而已我们也不需要prepass通道,这样在surface 指令后添加exclude_path:deferred exclude_path:prepass可以更多的shader变体,在我们游戏的一些surface shader中,我只需要加上这两条指令,即可每个shader减少18个变体。
当然如果我们对Unity一些内置的cginc文件比较熟悉,那么我们就可以使用#pragma skip_variants指令来跳过某些变体选项,例如如下代码:
#pragma multi_compile_fwdadd
// will make all variants containing
// "POINT" or "POINT_COOKIE" be skipped
#pragma skip_variants POINT POINT_COOKIE
这里再说一个Unity可以通过Edit->ProjectSettings->Graphics->ShaderStrpping下有一个Instance Variants选项(Strip Unused|Strip All|Keep All)这里建议设置成Strip Unused对比一下可能你的某些Shader变体量能够大幅减少。
工具
上面针对如何削减变体讨论了一番,这里再提供一下如何快速的扫描工程中的每个shader的变体数的实现代码的思路,在unity开源编辑器源码后,可以很容易找到计算shader变体数的方法,由于unity开源的是2017之后的,对于之前的版本,还是需要反编译一下找到相对应的代码。福利一下,代码都放出来如下。
[MenuItem("Find/GetAllShaderVariantCount", false, 20)]
public static void GetAllShaderVariantCount()
{
#if UNITY_5_6
Assembly asm = Assembly.LoadFile(@"D:\Program Files\Unity_5.6.5f1\Editor\Data\Managed\UnityEditor.dll");
System.Type t2 = asm.GetType("UnityEditor.ShaderUtil");
MethodInfo method = t2.GetMethod("GetComboCount", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
#elif UNITY_2017_1_OR_NEWER
Assembly asm = Assembly.LoadFile(@"D:\Program Files\Unity_2018.3.0f2\Editor\Data\Managed\UnityEditor.dll");
System.Type t2 = asm.GetType("UnityEditor.ShaderUtil");
MethodInfo method = t2.GetMethod("GetVariantCount", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
#endif
var shaderList = AssetDatabase.FindAssets("t:Shader");
var output = System.Environment.GetFolderPath(System.Environment.SpecialFolder.DesktopDirectory);
string pathF = string.Format("{0}/ShaderVariantCount.csv", output);
FileStream fs = new FileStream(pathF, FileMode.Create, FileAccess.Write);
StreamWriter sw = new StreamWriter(fs, Encoding.UTF8);
EditorUtility.DisplayProgressBar("写统计文件", "正在写入统计文件中...", 0f);
int ix = 0;
sw.WriteLine("ShaderFile,VariantCount");
foreach (var i in shaderList)
{
EditorUtility.DisplayProgressBar("写统计文件", "正在写入统计文件中...", 1f * ix / shaderList.Length);
var path = AssetDatabase.GUIDToAssetPath(i);
Shader s = (Shader)AssetDatabase.LoadAssetAtPath(path,typeof(Shader));
var variantCount = method.Invoke(null,new System.Object[]{ s,true});
sw.WriteLine(path + ","+variantCount.ToString());
++ix;
}
EditorUtility.ClearProgressBar(); //清除进度条
sw.Close();
fs.Close();
}