Unity的multi_compile和shader_feature编译指示符、shader variant和asset bundle

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com

multi_compile编译指示符

multi_compile编译指示符的文档在这里。假如有以下的shader示例语句:

 1#pragma multi_compile  _USE_SEMITRANSPARENT _USE_OPAQUE
 2#pragma multi_compile  _MULTI_RED _MULTI_GREEN _MULTI_BLU
 3
 4...
 5
 6fixed4 frag (v2f i) : SV_Target
 7{
 8    fixed4 col = tex2D(_MainTex, i.uv);
 9
10    #if _MULTI_RED
11        col = col * fixed4(1,0,0,1);
12    #elif _MULTI_GREEN
13        col = col * fixed4(0,1,0,1);
14    #elif  _MULTI_BLUE
15        col = col * fixed4(0,0,1,1);
16    #endif
17
18    #if _USE_SEMITRANSPARENT
19        col.a = 0.5;
20    #elif _USE_OPAQUE
21        col.a = 1.0;
22    #endif
23
24    return col;
25}

那么根据排列组合,就会有 $ 2 \times 3 = 6 $ 种代码段的组合,即分别是 _USE_SEMITRANSPARENT与_MULTI_RED_USE_SEMITRANSPARENT与_MULTI_GREEN_USE_SEMITRANSPARENT与_MULTI_BLUE_USE_OPAQUE与_MULTI_RED_USE_OPAQUE与_MULTI_GREEN_USE_OPAQUE与_MULTI_BLUE。总得来说,就是各种排列组合对应编译生成的变体, $\color{#FF0000}{无论有没有被使用上,都会被编译打包到游戏包或者资源包中}$ 。所以 在运行时 ,可以使用 Material.EnableKeywordMaterial.DisableKeyword 的方式去启用禁用各个 shader variant 。如下代码段所示:

 1private void OnGUI()
 2{
 3    if (GUI.Button(new Rect(0,0,150,50),"Red"))
 4    {
 5        var mat = m_Renderer.material;
 6        mat.EnableKeyword("_MULTI_RED");
 7        mat.DisableKeyword("_MULTI_GREEN");
 8        mat.DisableKeyword("_MULTI_BLUE");
 9        m_Renderer.material = mat;
10    }
11
12    if (GUI.Button(new Rect(170, 0, 150, 50), "Green"))
13    {
14        var mat = m_Renderer.material;
15        mat.DisableKeyword("_MULTI_RED");
16        mat.EnableKeyword("_MULTI_GREEN");
17        mat.DisableKeyword("_MULTI_BLUE");
18        m_Renderer.material = mat;
19    }
20
21    if (GUI.Button(new Rect(340, 0, 150, 50), "Blue"))
22    {
23        var mat = m_Renderer.material;
24        mat.DisableKeyword("_MULTI_RED");
25        mat.DisableKeyword("_MULTI_GREEN");
26        mat.EnableKeyword("_MULTI_BLUE");
27        m_Renderer.material = mat;
28    }
29
30    if (GUI.Button(new Rect(510, 0, 150, 50), "SemiTransparent"))
31    {
32        var mat = m_Renderer.material;
33        mat.EnableKeyword("_USE_SEMITRANSPARENT");
34        mat.DisableKeyword("_USE_OPAQUE");
35        m_Renderer.material = mat;
36    }
37
38    if (GUI.Button(new Rect(680, 0, 150, 50), "Opaque"))
39    {
40        var mat = m_Renderer.material;
41        mat.DisableKeyword("_USE_SEMITRANSPARENT");
42        mat.EnableKeyword("_USE_OPAQUE");
43        m_Renderer.material = mat;
44    }
45}

从上面的代码可以看出,凡是属于同一组的,且为互斥关系的各个keyword,在启用某一个keyword的时候,需要同时把其他的keyword给禁用掉。

shader_feature编译指示符

shader_feature编译指示符的文档在这里。假如有以下的shader示例语句:

 1// 在界面选项中可以决定此keyword是否开启或者关闭,如果开启的话
 2// 会将此keyword的开启记录在使用了该shader的材质球文件中。如果
 3// 关闭的话则不会编译这段shader代码进去
 4#pragma shader_feature _IS_PURPLE
 5
 6...
 7
 8#if _IS_PURPLE
 9    uniform fixed _IsPurple;
10#endif
11    v2f vert (appdata v)
12    {
13        v2f o;
14        o.vertex = UnityObjectToClipPos(v.vertex);
15        o.uv = TRANSFORM_TEX(v.uv, _MainTex);
16        return o;
17    }
18
19    fixed4 frag (v2f i) : SV_Target
20    {
21        fixed4 col = tex2D(_MainTex, i.uv);
22#if _IS_PURPLE
23        col = col * fixed4(0.5,0,0.5,1);
24#endif
25        return col;
26    }

从上面的代码中可以看出, shader_feature 在形式上和 multi_compile 类似。也都是以类似“宏”的方式去控制某个语句是否生效。但通常, shader_feature 中所声明的那个keyword,一般都在材质/shader的inspector面板中,在 预编辑阶段 中指定其启用或者不启用,将其保存在一个材质球文件中。而能否在运行时使用Material.EnableKeyword、Material.DisableKeyword启用禁用某个关键字,则有以下两种情况:

假定有两个材质球文件 UseShaderFeaturePurple.matUseShaderFeature.mat。这两个材质球都使用了 同一个 shader文件。UseShaderFeaturePurple材质球在编辑阶段,启用了 _IS_PURPLE keyword,UseShaderFeature材质球没有启用 _IS_PURPLE keyword。那么在运行时:

  1. 编辑器模式 中, 只要有 UseShaderFeaturePurple材质球,那么使用了UseShaderFeature材质球的那个game object,可以在运行时,通过使用 Marterial.EnableKeyword 方法,使得UseShaderFeature材质球的shader代码,也能执行启用了 _IS_PURPLE keyword的代码段。
  2. 非编辑器模式中, 就算 有UseShaderFeaturePurple材质球,只要该材质球未被使用上,就不会被打包进游戏包中,这时候如果使用了UseShaderFeature材质球的那个game object,使用Material.EnableKeyword方法 也是无法启用 这个keyword。

在编辑器和在独立运行包中的差异,可以参阅示例工程。工程的编译好的windows版程序在这里下载

shader variant和assetbundle

从上一节对shader_feature的介绍可以看到,为了尽可能减少shader代码的体积,shader_feature的原则就是能够stripping掉的shader代码,就会stripping掉它。所以在打包带有shader_feature的代码,尤其是使用assetbundle的方式时候,会经常出问题。据Unity3D研发团队介绍,在未引入ShaderVariantCollection机制之前,一个包含了shader_feature编译指示符的shader,要打包在assetbundle里面且被正确的引用,需要include all the materials that use a specific shader in the same AssetBundle.。即如果某材质球用到了这个有shader_feature的shader的话,这shader和这材质球就必须要打在同一个assetbundle里。显然这是不利于分包和资源组织的。Unity3D引入了ShaderVariantCollection解决了此问题。

在引入ShaderVariantCollection之后,可以一定程度上解决这个问题。使用ShaderVariantCollection解决问题的步骤如下:

  1. 创建一个ShaderVariantCollection文件
  2. 将包含有shader_featured的shader文件添加到ShaderVariantCollection文件中的集合里
  3. 向ShaderVariantCollection文件中的集合添加变体标签(variant tags)
  4. ShaderVariantCollection文件和记录在它里面的shader文件,打包在同一个assetbundle中

Unity3D研发团队提供了一个演示使用ShaderVariantCollection文件的视频,点击下载。 关于更详尽的shader variant及shader打包的规则, Nicholas10128 在github上有一份比较详尽的文档

参考网页

How To Use Shader Features With Asset Bundles?

UnityShaderVariant的一些探究心得

知乎 Shader变体收集与打包

使用shader_feature的shader在bundle中不起作用?

ShaderVariantCollection解决shader_feature丢失