写在前面的话

今天和一个老同事聊天,他之前也在引擎组,后来去了项目组,再之后跳槽去了一个国外大厂做项目主程,在我看来应该算是引擎技术+项目经验都有了,最近他又跳槽去了一个国内二三梯队的公司,比较有意思的是他放弃了主程,做了技术。

我觉得还蛮有意思的,聊了下来发现几点情况,1.现在二三梯队的公司对高大上的技术有了追求,所以技术专家比主程的职位还多,钱也更多,2.主程慢慢的偏向管理路线了,所以技术上是不如技术专家的。

聊完天,我觉得在游戏行业走技术路线还是可以的,虽然不能赚大钱(项目爆发,主程作为管理层应该可以拿到更多的钱),但是市场需求和薪资都是不错的。

ShaderVariants

什么是ShaderVariants

言归正传。什么是ShaderVariants,说实话,我一个靠OpenGL ES混饭吃的人,在来游戏公司之前,甚至可以说搞Unity之前,完全没听过什么是ShaderVariants。来了后天天听项目组说这个名词,我说GLSL的spec我都能背下来(不信可以翻看我写的笔记),整本spec只提到了variance和invariant看上去和ShaderVariants有点像,但是绝对是两个不同的概念。

所以这个事情我就纠结了,这些对OpenGL ES没我了解的人,咋都知道ShaderVariants呢。于是,作为技术人员,我必须搞明白它。

ShaderVariants就是Shader变种,比如我们的shader中如果用#pragma multi_compile了一个宏,就是一个Shader变种,两个宏就是两个shader变种。因为有没有定义这个宏,shader就可能可以变种成两个不同的shader。至于Shader中是否使用#define定义了这个宏,不影响Unity认为这个Shader会有几个Shader变种。

如梦初醒,这么简单的东西居然忽悠了我那么久。

ShaderVariants的影响

DX中也有Shader变种的概念,由于DX可以事先编译成binary,所以可以通过手动添加#define宏的方式,编译出很多版本的binary,放在游戏包里,用的时候直接根据情况选择使用就可以。

虽然OpenGL ES也支持shader binary,但是由于各家shader binary是不一样的,所以必须在运行的时候进行编译。但是编译总归是需要时间的,所以一般都会在加载场景之类的时候进行编译。不过如果变种太多,那么编译的时间就会相应变长,而如果很多变种是用不到的,那么在时间上和内存上都造成了浪费。

所以ShaderVariants还是需要优化一下的。

UWA对Shader的分析(数据来自Unity加载模块深度解析(Shader篇)

Shader本身尺寸不大,不管是在内存中,还是在AB中,一个Shader也就几KB到几十KB。官网上说着色器的大量变体会增加游戏打包时间(Large numbers of these shader variants increase game build time, and game data size.),使游戏包变大。但是我对这个表示怀疑,由于shader内容变多,所以包体变大我认可,但是在打包的时候不会把变体拆解吧,所以大也不会大很多。对此作了个实验,使用最简单的shader,打两个包,一个带fallback "Diffuse",一个不带,包体大小一样。但是在使用的时候用UWA工具看shader大小,一个724B,一个496B。

但Shader加载耗时开销的CPU占用很高,这主要是因为Shader的解析CPU开销很高,成为了Shader资源加载的性能瓶颈。

之前我对加载时间没有太重视,因为加载慢的放到场景切换的时候就可以了,但是看了龙之谷的优化分享,他们对场景切换时间要求压缩在5s以内,那么加载时间也就成了优化的一个目标。而一个Shader的加载时间可能会在几十ms左右,相当于几M的纹理或者上万面Mesh的加载时间了,而场景中动辄几十个Shader,这个加载时间就比较严重了。

经过UWA的分析,Shader加载的CPU耗时与其Keyword数量有关,Keyword数量越多,则加载开销也越大。keyword也就是ShaderVariants中定义的宏。(Shader的Keyword数量是会随着场景设置的不同而变化的。在Unity 5.x中,Unity默认会根据场景设置、Shader Pass等来调整Shader的Keyword,比如如果存在Lightmap的使用,则会默认将对应的Keyword打开,而对于没有使用Fog的项目,则会直接将相关Keyword关闭。参考文献见Unity官网:Shader variants to handle Fog and Lightmapping modes not used by any of the scenes are not included into the game data. See Graphics Settings if you want to override this behavior.

了解了Keyword对于Shader加载效率的重要性,下面就要降低Shader的Keyword数量,方法有很多:

  • 通过skip_variants操作在Shader中直接去除相关Keyword。
  • 直接去除Shader中的Fallback选项。
  • 通过ShaderVariantCollection来处理Shader的加载。

ShaderVariantCollection

对Shader比较熟悉的同学使用前两种办法都没问题,直接删除Fallback更直接,Shader尺寸会变小,而使用skip_variants更彻底,保留需要用的pass和宏定义控制的分支。

ShaderVariantCollection是Unity5.x的新特性,在ShaderVariantCollection中定义使用哪些Shader,以及这些Shader中需要的宏定义,然后在解析的时候,就会跳过那些不需要的宏定义引来的Shader变种,所以效果理论上和使用skip_variants相同,区别在于使用skip_variants的Shader在打包后就用不了这些feature了,也就相当于没有被打包进去。而ShaderVariantCollection的话,如果要用还是可以用的,只是这些Shader Variant第一次被用到的时候可能会出现卡顿(hiccup)——因为需要加载一个新的GPU程序代码到图形驱动(参考Unity文档:One downside of this default behavior, however, is a possible hiccup for when some shader variant is needed for the first time - since a new GPU program code has to be loaded into the graphics driver. This is often undesirable during gameplay)。在Unity5.x版本中,Shader的解析和CreateGPUProgram操作是分离的,所以Load进来后,还需要进行Shader.WarmupAllShaders操作。所以执行了ShaderVariantCollection要执行其warmup函数。

这三种办法其实可以同时使用,完全不用的Fallback删掉,有用的pass使用skip_variants,最后再用ShaderVariantCollection控制一下。

下面转载一篇文章,来自游戏蛮牛,链接为:http://www.manew.com/thread-101642-1-1.html 。

Unity5的ShaderVariants实验和总结(转载自游戏蛮牛,链接为:http://www.manew.com/thread-101642-1-1.html )

ShaderVariants(下文用shader变种描述)是unity中用来合并组织shader的一个方式之一,在shader中的使用类似宏定义。最近项目使用shader变种的时候发现了一些坑,所以做了如下实验和总结。其中前两节是基础部分,看官方文档也可以了解,只是通过实验来加强理解。第三节shader变种的打包是重点描述的,比较重要。

生成shader变种机制

做如下实验对比shader_feature:

Shader "Custom/Color"
{
        SubShader 
        {
            Pass 
            {
            Cull Off
            ZWrite Off
                        Lighting Off
 
                        CGPROGRAM
                        #pragma vertex vert
                        #pragma fragment frag
                        #include "UnityCG.cginc"
 
                        #pragma shader_feature RED GREEN BLUE
                //#pragma shader_feature GREEN
                //#pragma shader_feature BLUE
                        //#pragma multi_compile RED GREEN BLUE
                        //#pragma multi_compile __ GREEN
 
                        struct v2f 
                        {
                                fixed4 pos : SV_POSITION;
                        };
                         
                        v2f vert (appdata_base v)
                        {
                                v2f o;
                                o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
                                return o;
                        }
                         
                        half4 frag(v2f i) : COLOR
                        {
                                fixed4 c = fixed4(0,0,0,1);
                                #ifdef RED
                                c += fixed4(1,0,0,1);
                                #endif
                                #ifdef GREEN
                                c += fixed4(0,1,0,1);
                                #endif
                                #ifdef BLUE
                                c += fixed4(0,0,1,1);
                                #endif
                                return c;
                        }
                        ENDCG
                }
        }
 
        CustomEditor "ColorsGUI"
}

shader的变种数量可以通过shader面板上面查看到,点击Show按钮可以看到都有哪些变种

gamma

单一行命令

#pragma shader_feature RED

两个变种: __, RED(Patrick:Unity 5.5.x中在这种情况下,只有一种变种Just one shader variant.而且只有一个宏)

#pragma shader_feature RED GREEN

两个变种:RED,GREEN(Patrick:Unity 5.5.x中在这种情况下,只有一种变种Just one shader variant.虽然也能看到2个宏GREEN RED,但是好像只用到第一个宏。)

#pragma shader_feature RED GREEN BLUE

三个变种:RED,GREEN,BLUE(Patrick:Unity 5.5.x中在这种情况下,只有一种变种Just one shader variant.虽然也能看到3个宏BLUE GREEN RED,但是好像只用到第一个宏。)

#pragma multi_compile RED

一个变种:RED

#pragma multi_compile RED GREEN

两个变种:RED,GREEN

#pragma multi_compile RED GREEN BLUE

三个变种:RED,GREEN,BLUE

分析:shader_feature 和multi_compile 在Keyword 数量大于1时,生成变种的机制是一样的,都是一个keyword一个变种;当keyword只有1个时,shader_feature 会增加一个none变种。(Patrick:Unity 5.5.x中,当keyword只有1个的时候,shader_feature和multi_compile都是只有一个变种,当keyword超过1个的时候,multi_compile有n个变种,但是shader_feature虽然能看到n个宏,但是只有一个变种)

再来做个实验:

#pragma shader_feature __ RED

两个变种: none, RED(Patrick:Unity 5.5.x中,这种情况下shader_feature只有一个变种,可能是既然使用了shader_feature,那么就不会有none keyword的情况出现了。而且还一个特别奇怪的情况,如果#pragma shader_feature _,居然会有2个no keyword的变种。)

可见,当shader_feature 的keyword数量是1时不论是否有__符号,都会增加一个空keyword(__),除了这个在生成变种的机制上和multi_compile都是一致的。(Patrick:Unity 5.5.x中,multi_compile是会有两个变种,none和RED,但是shader_feature只会有一个变种。)

多行命令

#pragma multi_compile __ RED

#pragma multi_compile __ GREEN

四个变种:__,RED,GREEN,RED GREEN

分析:多行命令就是单行命令的乘法组合,shader_feature和multi_compile除了单一keyword时是否补__之外,在多行命令中也是一致的。(Patrick:Unity 5.5.x中,multi_compile是会这样,但是shader_feature也只会有一种变种Just one shader variant.而且也只能看到2个宏,并且好像只用到第一个宏。所以不知道是不是5.5.0有问题还是怎样,总之大家只要知道可以通过shader_feature和mul_compile来设置变种即可。)

匹配shader变种机制

为了实验shader变种的匹配,做一个方便定义keyword的shader界面,代码如下:

public class ColorsGUI: ShaderGUI {
 
	    private static bool bRed = false;
	    private static bool bGreen = false;
	    private static bool bBlue = false;
 
        public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
        {
	        // render the default gui
	        base.OnGUI(materialEditor, properties);
	 
	        Material targetMat = materialEditor.target as Material;
	 
	        bRed = Array.IndexOf(targetMat.shaderKeywords, "RED") != -1;
	        bGreen = Array.IndexOf(targetMat.shaderKeywords, "GREEN") != -1;
	        bBlue = Array.IndexOf(targetMat.shaderKeywords, "BLUE") != -1;
	 
	        EditorGUI.BeginChangeCheck();
	 
	        bRed = EditorGUILayout.Toggle("红", bRed);
	        bGreen = EditorGUILayout.Toggle("绿", bGreen);
	        bBlue = EditorGUILayout.Toggle("蓝", bBlue);
	 
	        if (EditorGUI.EndChangeCheck())
	        {
	            if (bRed) 
	                targetMat.EnableKeyword("RED");
	            else
	                targetMat.DisableKeyword("RED");
	            if (bGreen)
	                targetMat.EnableKeyword("GREEN");
	            else
	                targetMat.DisableKeyword("GREEN");
	            if (bBlue)
	                targetMat.EnableKeyword("BLUE");
	            else
	                targetMat.DisableKeyword("BLUE");
	        }
        }
}

shader界面如下:

gamma

这样,勾选一个颜色,就会enable一个keyword,通过查看结果颜色就能知道匹配到了哪个shader变种,实验如下:

#pragma multi_compile RED GREEN(两个变种:RED, GREEN)

材质keyword为RED : 显示红色(匹配RED)

材质keyword为GREEN : 显示绿色(匹配GREEN)

材质keyword为__ : 显示红色(匹配RED)(Patrick:当使用shader_feature的时候,显示的是黑色,这就是区别。但是如果使用#pragma multi_compile __ RED GREEN的时候,如果keyword为__,则显示黑色,不匹配,同样如果使用shader_feature也是会这样。)

材质keyword为RED GREEN: 显示红色(匹配RED)

分析:当keyword存在正好匹配的变种时直接匹配、当keyword不存在匹配变种时取第一个变种(Patrick:确实当#pragma multi_compile,且后面没有__的时候是这样的,但是如果有__,那么参照上条注释。)

(Patrick:如果使用多行命令,则会混合,比如材质keyword为RED GREEN,显示黄色。但是需要注意一点,#pragma multi_compile 后面如果没有__,则即使keyword为_,也是会显示颜色的。参照上面加注释的那一条)

shader变种打包

打包的代码如下:

[MenuItem("Assets/Build AssetBundles")]
static void BuildAllAssetBundles()
{
        List maps = new List();
        maps.Clear();
        //资源打包
 
        string[] files = {
                            "Assets/ShaderVariants/Resources/red.prefab",
                         };
 
        AssetBundleBuild build = new AssetBundleBuild();
        build.assetBundleName = "ShaderVariantsPrefab";
        build.assetNames = files;
        maps.Add(build);
 
        string[] file2s = {
                            "Assets/ShaderVariants/Resources/Shader/Colors.shader",
                          };
 
        AssetBundleBuild build2 = new AssetBundleBuild();
        build2.assetBundleName = "ShaderVariantsShader";
        build2.assetNames = file2s;
        maps.Add(build2);
 
        BuildAssetBundleOptions options = BuildAssetBundleOptions.DeterministicAssetBundle;
        BuildPipeline.BuildAssetBundles("Assets/StreamingAssets", maps.ToArray(), options, BuildTarget.StandaloneWindows);
 
        AssetDatabase.Refresh();
}

读包并实例化的代码如下:

public class BundleLoader : MonoBehaviour
{
        void Start () {
        	load();
    	}
 
	    public void load()
	    {
	        StartCoroutine(LoadMainGameObject("file://" + Application.dataPath + "/StreamingAssets/" + "ShaderVariantsShader"));
	        StartCoroutine(LoadMainGameObject("file://" + Application.dataPath + "/StreamingAssets/" + "ShaderVariantsPrefab"));
	    }
	 
	    private IEnumerator LoadMainGameObject(string path)
	    {
	        WWW bundle = new WWW(path);
	 
	        yield return bundle;
	 
	        if (bundle.url.Contains("ShaderVariantsShader"))
	        {
	            //依赖shader包
	            bundle.assetBundle.LoadAllAssets();
	        }
	        else
	        {
	            UnityEngine.Object obj = bundle.assetBundle.LoadAsset("assets/ShaderVariants/resources/red.prefab");
	            Instantiate(obj);
	            bundle.assetBundle.Unload(false);
	        } 
	    }
}

为了对比shader_feature 和multi_compile 以及shader依赖和非依赖打包,做如下实验:

#pragma shader_feature RED GREEN BLUE

将选中RED关键字的prefab打包,加载bundle和其中的prefab,显示了红色,此时改变此材质的keyword为GREEN或者BLUE,没有效果

#pragma multi_compile RED GREEN BLUE

将选中RED关键字的prefab打包,加载bundle和其中的prefab,显示了红色,此时改变此材质的keyword为GREEN或者BLUE,可以显示绿色和蓝色

分析:shader_feature声明变种时,打包只会打包被资源引用的keyword变种,multi_compile声明变种时,打包会把所有变种都打进去

#pragma shader_feature RED GREEN BLUE

将选中RED关键字的prefab和shader依赖打包,加载bundle和其中的prefab,显示了异常粉红,任何变种都没有生效

分析:shader_feature标记的shader单独依赖打包时,任何变种都不会打进去,分析原因估计是unity认为单包中shader没有被引用过

总结:unity5中新出的shader_feature可以只将引用过的shader变种打进包里面,听起来很有用,可是大部分项目中为了节省冗余shader的内存,shader都是作为依赖包单独成一包的,此时没有任何shader变种被打进包中;更何况即使shader没有依赖打包,如果计划代码中动态修改shader的变种而不是记录在材质里面,此时也不能用shader_feature。基本上我们的项目中shader_feature可以废弃了。

(Patrick:这个在官网也说过类似的话:使用#pragma shader_feature的Shader如果没有被任何material用到它的variant,那么在build的时候就不会被打进去。Individual shader features, for shaders that use #pragma shader_feature. If none of the used materials use a particular variant, then it is not included into the build. See internal shader variants documentation. Out of built-in shaders, the Standard shader uses this.

虽然并非全部原创,但还是希望转载请注明出处:电子设备中的画家|王烁 于 2017 年 8 月 3 日发表,原文链接(http://geekfaner.com/unity/blog2_ShaderVariants.html)