unity shader 变种(多重编译 multi_compile)
让我们好好聊聊Unity Shader中的multi_complie
这篇文章比较全面的介绍了Unity中shader的multi_compile与shader_feature的定义和使用以及限制。
一、定义
在unity中我们可以通过使用#pragma multi_compile或#pragma shader_feature指令来为shader创建多个稍微有点区别的shader变体。这个Shader被称为宏着色器(mega shader)或者超着色器(uber shader)。实现原理:根据不同的情况,使用不同的预处理器指令,来多次编译Shader代码。
在运行时,Unity从Material宏Material.EnableKeyword和Shader.DisableKeyword或全局着色器宏Shader.EnableKeyword和Shader.DisableKeyword中选择适当的着色器变体。如果这两个宏都未启用,则Unity使用第一个宏。
二、使用
//定义两个TEST_1,TEST_2两个宏
#pragma multi_compile TEST_1 TEST_2
//在shader中使用
#ifdef TEST_1
//Todo
#endif
#ifdef TEST_2
//Todo
#endif
上面这个命令会产生2种着色器变种:TEST_1,TEST_2。
要生成未定义预处理器宏的着色器变体,请添加一个仅为下划线(__
)的名称。这是避免使用两个宏的常用技术,因为对项目中可以使用的宏数量有限制,例如:
#pragma multi_compile __ TEST_1
在脚本中控制使用:
//使用TEST_1变种
Shader.EnableKeyword ("TEST_1");
Shader.DisableKeyword ("TEST_2");
三、组合
#pragma multi_compile TEST_1 TEST_2
#pragma multi_compile TEST_3 TEST_4 TEST_5
它产生总共六个着色器变体(TEST_1_TEST_3,TEST_1_TEST_4,TEST_1_TEST_5,TEST_2_TEST_3,TEST_2_TEST_4,TEST_2_TEST_5)。
所以如果有10行multi_compile,每行2个选项,那么将一共产生1024个着色器变体。
请记住,着色器变体数量将以这种方式疯狂增长。
四、#pragma shader_feature
shader_feature非常相似multi_compile。唯一的区别是Unity shader_feature在最终版本中不包含未使用的着色器变体。所以shader_feature适用于在我们在编辑器中,选中材质,设置它使用的shader的宏,如果在程序中动态的去设置可能无效(原因下面说明)。而对于multi_compile,会把所有的变体都编译进程序里,所以适合需要在程序运行中动态改变状态的宏,适合全局设置 。
材质中设置位置截图:
五、宏限制
在unity中限制了全局的宏个数为265个,而unity内部使用了大约60个,所以在多个不同的着色器中定义全局宏时需要注意宏数量不要超过限制。
使用本地宏替代一部分全局宏:使用shader_feature_local和multi_compile_local。
shader_feature_local:类似于shader_feature,但枚举宏是本地的。
multi_compile_local:类似于multi_compile,但枚举宏是本地的。
在项目中除非是希望通过全局API启用的那些特定宏,否则应尽量使用本地宏,
使用更多本地宏和更少的全局宏,以减少每个着色器的宏总计数。如果存在具有相同名称的全局和本地宏,则Unity会优先使用local宏。
注意:
(1)不能将本地宏与进行全局宏更改的API一起使用(例如Shader.EnableKeyword或CommandBuffer.EnableShaderKeyword)。
(2)每个着色器最多有64个唯一的本地宏。
(3)如果Material启用了本地宏,并且其着色器更改为不再声明的宏,则Unity会创建一个新的全局宏。
六、内置multi_compile快捷方式
unity中提供一些内置的宏用于编译多个着色器变体。这些主要用于处理Unity中不同的光照,阴影和光照贴图类型。
multi_compile_fwdbase:编译PassType.ForwardBase所需的所有变体。变体处理不同的光照贴图类型,并启用或禁用主方向光的阴影。
multi_compile_fwdadd:为PassType.ForwardAdd编译变体。这将编译变体以处理Directional,Spot或Point Light类型及其变体与Cookie纹理。
multi_compile_fwdadd_fullshadows:同样multi_compile_fwdadd,但也包括灯具有实时阴影的能力。
multi_compile_fog:扩展为多个变体以处理不同的雾类型(off / linear / exp / exp2)。
大多数内置快捷方式都会产生许多着色器变体。如果您知道项目不需要它们,您可以使用#pragma skip_variants跳过编译它们中的一些。例如:
#pragma multi_compile_fwdadd
#pragma skip_variants POINT POINT_COOKIE
该指令跳过包含POINT或POINT_COOKIE的所有变体。
七、查看shader变种数量
#pragma multi_compile TEST_1 TEST_2 TEST_3
#pragma multi_compile TEST_4 TEST_5
#pragma multi_compile TEST_6 TEST_7
上面的组合会产生3x2x2=12种变体,我们可以点击show查看具体的变体组合名称。
// Total snippets: 1
// -----------------------------------------
// Snippet #0 platforms ffffffff:
Keywords always included into build: TEST_1 TEST_2 TEST_3 TEST_4 TEST_5 TEST_6 TEST_7
12 keyword variants used in scene:
TEST_1 TEST_4 TEST_6
TEST_1 TEST_4 TEST_7
TEST_1 TEST_5 TEST_6
TEST_1 TEST_5 TEST_7
TEST_2 TEST_4 TEST_6
TEST_2 TEST_4 TEST_7
TEST_2 TEST_5 TEST_6
TEST_2 TEST_5 TEST_7
TEST_3 TEST_4 TEST_6
TEST_3 TEST_4 TEST_7
TEST_3 TEST_5 TEST_6
TEST_3 TEST_5 TEST_7
这里查看的是所有会被编译的变体的数量,也就是#pragma multi_compile声明的宏的全部组合。
#pragma multi_compile TEST_1 TEST_2 TEST_3
#pragma multi_compile TEST_4 TEST_5
#pragma shader_feature TEST_6 TEST_7
// Total snippets: 1
// -----------------------------------------
// Snippet #0 platforms ffffffff:
Keywords stripped away when not used: TEST_6 TEST_7
Keywords always included into build: TEST_1 TEST_2 TEST_3 TEST_4 TEST_5
6 keyword variants used in scene:
TEST_1 TEST_4 TEST_6
TEST_1 TEST_5 TEST_6
TEST_2 TEST_4 TEST_6
TEST_2 TEST_5 TEST_6
TEST_3 TEST_4 TEST_6
TEST_3 TEST_5 TEST_6
上面的组合会产生3x2x1=6种变体,#pragma shader_feature没有特别处理的话只有会默认包括第一个宏。
八、编译
(1)material的ShaderKeywords
Material所包含的Shader Keywords表示启用shader中对应的宏,Unity会调用当前宏组合所对应的变体来为Material进行渲染。在Editor下,可以通过将material的inspector调成Debug模式来查看当前material定义的Keywords,也可在此模式下直接定义Keywords,用空格分隔Keyword。
优点:根据material中的ShaderKeywords自动生成变体。无需额外设置
缺点:多个不同的material包中可能存在相同的shader变体,造成资源冗余。若在程序运行时动态改变material的keyword其变体可能并没有被生成
如上图设置:如果ShaderKeywords中没有设置TEST_6,这是如果我们想在程序中通过代码动态使用TEST_6这个宏(Shader.EnableKeyword("TEST_6"))。可能不能得到想要的效果,因为TEST_6这个变种没有生成。
(2)把Shader加入到Always Include Shaders列表里
找到ProjectSetting->Graphics->Always Include Shaders列表,将我们需要的shader添加到里面,这样unity将会把这个shader的所有的变种都生成出来。
优点:我们不用担心项目发布出去以后有些变种没有生成,不能在程序中动态的去控制我们的宏。
缺点:生成的变体数量庞大,导致发布时间变长,游戏包体过大。比如你把standardShader放进去,由于它有大量的keyword,全部变种都生成的话大概有几百兆。
(3)使用ShaderVariantCollection是生成指定变体
ShaderVariantCollection是unity5.x以后用来记录shader的哪些变体需要被生成。这样做的好处就是在shader_feature与multi_compile结合使用时,能够设置生成何种变体,从而避免生成不必要的变体;shader不必和material打在一个包中,避免了多个包中存在相同的变体资源;明确直观的显示了哪些变体是需要生成的。
生成方式:
(1)通过Create->Shader-> Shader Variant Collection,就可以新建一个shader variant collection文件,手动添加需要编译的变种
(2)通过Edit->Project Settings->Graphics中的save to asst...按钮,生成unity帮我们自动收集的,使用到的变种信息。
这时候只需要先Clear一下,然后依次打开我们的所有场景,把需要的物体都显示一遍,Unity就会自动记录下来哪些着色器的哪些着色器变体已经被使用到。统计完后只需点击下面的保存按钮就可以生成我们所需要的ShaderVariantCollection资源。当然你也可以为你的每一个场景或者按需生成足够多的ShaderVariantCollection资源。自动收集的功能不一定百分百可靠,最好事后多检查。
ShaderVariantCollection加载:
启动时预加载:
最简单最粗暴的使用方式就是在游戏启动的瞬间就直接加载ShaderVariantCollection资源并编译里面的着色器变体,Unity已经为我们做好这一步了,依然还是在图形设置面板里,只需把需要启动是就编译的ShaderVariantCollection添加在Preloaded Shaders里面
代码加载:
由于ShaderVariantCollection也是一种资源,可以跟纹理、模型等等资源一起打包和加载等,只需在加载之后调用一句WarmUp。
ShaderVariantCollection shaderVariantCollection = Resources.Load <ShaderVariantCollection>( "MainShaderVariant");
if (shaderVariantCollection )
shaderVariantCollection.WarmUp ();
也可以把ShaderVariantCollection放在Resources目录下,好像会被自动加载(未验证)。