unity shader shader_feature and multi_compile

官网链接:https://docs.unity3d.com/Manual/SL-MultipleProgramVariants.html

Unity5中使用了一种被称为着色器编译多样化(Multiple shader program variants)的新技术,常被称为“megashaders”或“uber shaders”,并通过为每种情况提供不同的预处理指令来让着色器代码多次被编译来实现。

在Unity中,这可以通过 #pragmamulti_compile 或者 #pragma shader_feature 指令来在着色器代码段中实现。这种做法对表面着色器也可行。

在运行时,相应的着色器变体是从材质的关键词中取得的(Material.EnableKeyword和 DisableKeyword),或者全局着色器关键词(Shader.EnableKeyword和 DisableKeyword)。

1.1 multi_compile的用法简析

若我们定义如下指令:

1
#pragma multi_compile FANCY_STUFF_OFFFANCY_STUFF_ON

也就表示定义了两个变体:FANCY_STUFF_OFF和FANCY_STUFF_ON。在运行时,其中的一个将被激活,根据材质或者全局着色器关键词(#ifdef FANCY_STUFF_OFF之类的宏命令也可以)来确定激活哪个。若两个关键词都没有启用,那么将默认使用前一个选项,也就是关闭(OFF)的选项FANCY_STUFF_OFF。

需要注意,也可以存在超过两个关键字的multi_compile编译选项,比如,如下代码将产生4种着色器的变体:

1
#pragma multi_compile SIMPLE_SHADINGBETTER_SHADING GOOD_SHADING BEST_SHADING

当#pragma multi_compile中存在所有名字都是下划线的一个指定段时,就表示需在没有预处理宏的情况下产生一个空的着色器变种。这种做法在着色器编写中比较常见,因为这样可以在不影响使用的情况下,避免使用两个关键词,这样就节省了一个变量个数的占用(下面会提到,Unity中关键词个数是有129个的数量限制的)。例如,下面的指令将产生两个着色器变体;第一个没有定义,第二个定义为FOO_ON:

1
#pragma multi_compile __ FOO_ON

这样就省去了一个本来需要定义出来的 FOO_OFF(FOO_OFF没有定义,自然也不能使用),节省了一个关键词个数的占用。

若Shader中有如上定义,则可以使用#ifdef来进行判断:

1
#ifdef FOO_ON//代码段1#endif

根据上面已经定义过的FOO_ON,此#ifdef判断的结果为真,代码段1部分的代码就会被执行到。反之,若#pragma multi_compile __FOO_ON一句代码没有交代出来,那么代码段1部分的代码就不会被执行。

这就是着色器编译多样化的实现方式,其实理解起来很容易,对吧。

1.2 shader_feature和multi_compile之间的区别

#pragma shader_feature 和#pragma multi_compile非常相似,唯一的区别在于采用了#pragmashader_feature语义的shader,在遇到不被使用的变体的时候,就不会将其编译到游戏中。所以,shader_feature中使得所有的设置到材质中的关键词都是有效的,而multi_compile指令将从全局代码里设置关键词。

另外,shader_feature还有一个仅仅含有一个关键字的快捷表达方式,例如:

1
#pragma shader_feature FANCY_STUFF

此为#pragma shader_feature _ FANCY_STUFF的一个简写形式,其扩展出了两个着色器变体,第一种变体自然为不定此FANCY_STUFF变量(那么若在稍后的Shader代码中进行#ifdef FANCY_STUFF的判断,则结果为假),第二种变体为定义此FANCY_STUFF变量(此情况下#ifdef FANCY_STUFF的判断结果为真)。

1.3 多个multi_compile连用会造成指数型增长

可以提供多个multi_compile流水线,然后着色器的结果可以被编译为几个流水线的排列组合,比如:

1
#pragma multi_compile A B C#pragma multi_compile D E

第一行中有3种选项,第二行中有两种选项,那么进行排列组合,总共就会有六种选项(A+D, B+D, C+D, A+E, B+E, C+E)。

容易想到,一般每以个multi_compile流水线,都控制着着色器中某一单一的特性。请注意,着色器总量的增长速度是非常快的。

比如,10条包含两个特性的multi_compil指令,会得到2的10次方,也就是1024种不同的着色器变体。

1.4 关于Unity中的关键词限制Keyword limit

当使用着色变量时,我们应该记住,Unity中将关键词的数量限制在了128个之内(着色变量算作关键字),且其中有一些已经被Unity内置使用了,因此,我们真正可以自定义使用关键词的数量以及是小于128个的。同时,关键词是在单个Unity项目中全局使用并计数的,所以我们要千万小心,在同一项目中存在的但没用到Shader也要考虑在内,千万不要合起来在数量上超出Unity的关键词数量限制了。

1.5 Unity内置的快捷multi_compile指令

如下有Unity内置的几个着色器变体的快捷多编译指令,他们大多是应对Unity中不同的光线,阴影和光照贴图类型。详情见rendering pipeline 。

指令——————————————————————————————– 表示
multi_compile_fwdbase 编译正向基础渲染通道(用于正向渲染中,应用环境光照、主方向光照和顶点/球面调和光照(Spherical Harmonic Lighting))所需的所有变体。这些变体用于处理不同的光照贴图类型、主要方向光源的阴影选项的开关与否。
multi_compile_fwdadd 编译正向附加渲染通道(用于正向渲染中;以每个光照一个通道的方式应用附加的逐像素光照)所需的所有变体。这些变体用于处理光源的类型(方向光源、聚光灯或者点光源),且这些变种都包含纹理cookie。
multi_compile_fwdadd_fullshadows 此指令和上面的正向渲染附加通道基本一致,但同时为上述通道的处理赋予了光照实时阴影的能力。
multi_compile_fog 编译出几个不同的Shader变体来处理不同类型的雾效(关闭/线性/指数/二阶指数)(off/linear/exp/exp2)

1.6 使用指令跳过某些变体的编译

大多数内置的快捷指令导致了很多着色的变体。若我们熟悉他们且知道有些并非所需,可以使用#pragmaskip_variants语句跳过其中一些的编译。例如:

1
#pragma multi_compile_fwdadd// 将跳过所有使用"POINT"或 "POINT_COOKIE"的变体#pragma skip_variants POINT POINT_COOKIE

OK,通过上面经过翻译&理解过后的官方文档材料,应该对Unity中的着色器编译多样化有了一个理解。说白了,着色器变体的定义和使用与宏定义很类似。

1.7 对知识的提炼

上面交代了这么多,看不懂没关系,我们提炼一下,看懂这段提炼,关于着色器变体的意义与使用方式,也就懂了大半了。

若我们在着色器中定义了这一句:

1
#pragma shader_feature _THIS_IS_A_SAMPLE

这句代码理解起来,也就是_THIS_IS_A_SAMPLE被我们定义过了,它是存在的,以后我们如果判断#ifdef _THIS_IS_A_SAMPLE,那就是真了。我们可以在这个判断的#ifdef…… #endif块里面实现自己需要的实现代码X,这段实现代码X,只会在你用#pragma multi_compile 或#pragmashader_feature定义了_THIS_IS_A_SAMPLE这个“宏”的时候会被执行,否则,它就不会被执行到。

实现代码X的执行与不执行,全靠你对变体的定义与否。这就是着色器编译多样化的实现方式,一个着色器+多个CG头文件的小团队(如标准着色器),可以独当一面,一个打一群,可以取代一大堆独立实现的Shader的原因所在。