首页 > 技术文章 > Unity shader的内置宏与变体(二)

vkensou 2021-08-30 22:59 原文

简介

本文总结Unity变体与Shader打包相关内容。基于Unity 2020.3和Built-in管线。

1.宏的定义

首先说明,本文中的宏不包含由#define定义的宏。
Unity提供了两种定义宏的方法:

  • multi_compile
  • shader_feature

以及相应的局部版本(2019引入):

  • multi_compile_local
  • shader_feature_local

另外还有只在特定阶段起作用的宏定义方法(2020.3引入),如:

  • multi_compile_fragment
  • shader_feature_local_vertex

实际使用中multi_compile与shader_feature基本没有区别。他们主要有两个区别。第一个是在shader_feature后面只跟了一个宏的时候,会生成两个变体,一个是不包含该宏,另一个是包含该宏。另一个区别是打包时的表现。在打包的时候shader_feature不会包含没有使用的变体,而multi_compile会排列组合所有变体。

2.变体

Unity在编译shader时,不同的宏组合会生成独立的shader程序,这些独立的shader程序就叫做变体。
实际项目中,宏的使用非常复杂:有些宏是静态的,有些宏是动态的。因此Unity很难帮我们正确的收集所有变体:不是缺少,就是冗余。因此,Unity提供了两个操作变体的工具:Shader Variant Collection和IPreprocessShaders.OnProcessShader。

2.1 Shader Variant Collection

Shader Variant Collection是一种包含shader变体列表的文件。在Project窗口右键依次点击[Create] - [Shader] - [Shader Variant Collection]可以创建它。或者在Graphics Setting里可以直接获得当前编辑器中使用到的变体集。该文件如下图所示:

image

利用Shader Variant Collection我们可以手动的将需要的变体加入其中。
加入的时候需要设置Pass Type和宏组合。其中Pass Type就是shader中在pass的Tags里设置的LightMode。具体见参考资料。

2.2 IPreprocessShaders.OnProcessShader

我们可以通过继承IPreprocessShaders以及实现OnProcessShader接口来实现剔除变体的目的。简易的代码如下:

public class ShaderProcess : IPreprocessShaders
{
    public int callbackOrder => 0;

    public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
    {
        for (int i = data.Count - 1; i >= 0; --i)
        {
            if (data[i].graphicsTier != UnityEngine.Rendering.GraphicsTier.Tier1)
            {
                data.RemoveAt(i);
            }
        }
    }
}

通过实现这个接口,我们可以更深入的理解Unity变体的编译过程,Unity是以shader阶段为单位进行编译的,如:编译VS时调用一次该接口,编译PS时再调用一次该接口。因此Unity建议在定义宏时加上阶段后缀,这样可以减少shader的编译时间。另外在Build-in管线中,宏是区分Tier的,即使并没有使用Tier相关宏。Tier的使用比较少,而且在SRP中已经废除了Tier机制,建议直接把Tier2和Tier3的变体剔除掉。

3.总结

宏的使用要小心谨慎。使用不当可能会导致变体爆炸,极大的拖慢打包速度,以及增大内存。而缺变体会导致效果出错。Unity提供的两种变体操作方法,一种是手动增加变体,一种是手动减少变体。或许这两种方法正好对应着shader_feature和multi_compile。在实际使用中,笔者发现这两种方法同时使用有重复用功的嫌疑。比如如果完整的手动收集了变体,那么可能并不需要再剔除了。那么是不是可以只使用其中一种方法呢?比如所有宏都使用shader_feature定义,然后通过shader variant collection手动收集所有需要的变体。再或者所有宏都使用multi_compile定义,然后通过IPreprocessShaders.OnProcessShader剔除掉所有不可能出现的宏组合。具体怎么处理还有待实践验证。

参考资料:
[1]: https://docs.unity3d.com/Manual/SL-MultipleProgramVariants.html
[2]: https://docs.unity3d.com/Manual/shader-variant-collections.html
[3]: https://docs.unity3d.com/ScriptReference/Rendering.PassType.html
[4]: https://docs.unity3d.com/Manual/shader-predefined-pass-tags-built-in.html
[5]: https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@11.0/manual/urp-shaders/urp-shaderlab-pass-tags.html#urp-pass-tags-lightmode

推荐阅读