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

Table of Contents

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

multi_compile编译指示符

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

#pragma multi_compile  _USE_SEMITRANSPARENT _USE_OPAQUE
#pragma multi_compile  _MULTI_RED _MULTI_GREEN _MULTI_BLU

...

fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv);

    #if _MULTI_RED
        col = col * fixed4(1,0,0,1);
    #elif _MULTI_GREEN
        col = col * fixed4(0,1,0,1);
    #elif  _MULTI_BLUE
        col = col * fixed4(0,0,1,1);
    #endif

    #if _USE_SEMITRANSPARENT
        col.a = 0.5;
    #elif _USE_OPAQUE
        col.a = 1.0;
    #endif

    return col;
}

那么根据排列组合,就会有 $ 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 。如下代码段所示:

private void OnGUI()
{
    if (GUI.Button(new Rect(0,0,150,50),"Red"))
    {
        var mat = m_Renderer.material;
        mat.EnableKeyword("_MULTI_RED");
        mat.DisableKeyword("_MULTI_GREEN");
        mat.DisableKeyword("_MULTI_BLUE");
        m_Renderer.material = mat;
    }

    if (GUI.Button(new Rect(170, 0, 150, 50), "Green"))
    {
        var mat = m_Renderer.material;
        mat.DisableKeyword("_MULTI_RED");
        mat.EnableKeyword("_MULTI_GREEN");
        mat.DisableKeyword("_MULTI_BLUE");
        m_Renderer.material = mat;
    }

    if (GUI.Button(new Rect(340, 0, 150, 50), "Blue"))
    {
        var mat = m_Renderer.material;
        mat.DisableKeyword("_MULTI_RED");
        mat.DisableKeyword("_MULTI_GREEN");
        mat.EnableKeyword("_MULTI_BLUE");
        m_Renderer.material = mat;
    }

    if (GUI.Button(new Rect(510, 0, 150, 50), "SemiTransparent"))
    {
        var mat = m_Renderer.material;
        mat.EnableKeyword("_USE_SEMITRANSPARENT");
        mat.DisableKeyword("_USE_OPAQUE");
        m_Renderer.material = mat;
    }

    if (GUI.Button(new Rect(680, 0, 150, 50), "Opaque"))
    {
        var mat = m_Renderer.material;
        mat.DisableKeyword("_USE_SEMITRANSPARENT");
        mat.EnableKeyword("_USE_OPAQUE");
        m_Renderer.material = mat;
    }
}

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

shader_feature编译指示符

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

// 在界面选项中可以决定此keyword是否开启或者关闭,如果开启的话
// 会将此keyword的开启记录在使用了该shader的材质球文件中。如果
// 关闭的话则不会编译这段shader代码进去
#pragma shader_feature _IS_PURPLE

...

#if _IS_PURPLE
    uniform fixed _IsPurple;
#endif
    v2f vert (appdata v)
    {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.uv = TRANSFORM_TEX(v.uv, _MainTex);
        return o;
    }

    fixed4 frag (v2f i) : SV_Target
    {
        fixed4 col = tex2D(_MainTex, i.uv);
#if _IS_PURPLE
        col = col * fixed4(0.5,0,0.5,1);
#endif
        return col;
    }

从上面的代码中可以看出, 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丢失

kumakoko avatar
kumakoko
pure coder
comments powered by Disqus