Unity shader中的multi_compile和shader_feature宏定义

关于multi_compile的使用知乎的这篇和这篇讲的很好,本文转载结合一下。

一.什么是ShaderVariant(Shader变体)

在写shader时,往往会在shader中定义多个宏,并在shader代码中控制开启宏或关闭宏时物体的渲染过程。最终编译的时候也是根据这些不同的宏来编译生成多种组合形式的shader源码。其中每一种组合就是这个shader的一个变体(Variant)。

Material ShaderKeywords与ShaderVariant

Material所包含的Shader Keywords表示启用shader中对应的宏,Unity会调用当前宏组合所对应的变体来为Material进行渲染。

在Editor下,可以通过将material的inspector调成Debug模式来查看当前material定义的Keywords,也可在此模式下直接定义Keywords,用空格分隔Keyword。如图1

image

图1:设置keyword

在程序中,可用Material.EnableKeyword()、Material.DisableKeyword()、Shader.EnableKeyword()、Shader.DisableKeyword()来启用/禁用相应的宏。

Enable函数应与Disable函数相对应。若一个宏由Material.EnableKeyword()开启,则应由Material.DisableKeyword()关闭,Shader.DisableKeyword()无法关闭这个宏。Material中定义的Keywords由Material的函数进行设置。

二.multi_complie 还是 shader_feature

shader_feature 和 multi_complie 是两个很相似的预编译指令,在Editor模式下,他们是几乎没有区别的。

共同点是:

声明Keyword,用来产生Shader的变体(Variant)

#pragma multi_compile A B
//OR #pragma shader_feature A B

//-----------------------A模块----------------------
#if A
  return fixed4(1,1,1,1); 
#endif 
//---------------------------------------------------

//-----------------------B模块-----------------------
#if B
  return fixed4(0,0,0,1); 
#endif
//---------------------------------------------------
  • 这个Shader会被编译成两个变体:一是只包含A模块代码的变体A;二是只包含B模块代码的变体B;
    指定的第一个关键字是默认生效的,即默认使用变体A;
  • 在脚本里用Material.EnableKeyword或Shader.EnableKeyword来控制运行时具体使用变体A还是变体B;
  • 它们声明的Keyword是全局的,可以对全局的包含该Keyword的不同Shader起作用;
  • 全局最多只能声明256个这样的Keyword;
  • 请注意Keyword的数量和变体的数量之间的关系,并可能由此导致的性能开销,比如声明#pragma
    multi_compile A B和#pragma multi_compile D E 这样的两组Keyword会产生 2x2=4 个Shader变体,但若声明10组这样的keyword,则该Shader会产生1024个变体;
    区别在于:

如果使用shader_feature,build时没有用到的变体会被删除,不会打出来。也就是说,在build以后环境里,运行代码Material.EnableKeyword("B")可能不起作用,因为没有Material在使用变体B,所以变体B没有被build出来,运行时也找不到变体B。

如果想解决这个问题,可以采取以下办法中的其中一种:

使用multi_complie 代替 shader_feature,multi_complie 会把所有变体build出来;
把这个Shader加入“always included shaders”中 (Project Settings -> Graphic);
创造一个使用变体B的Material,强行说明变体B有用;

三.__

上文已经提到了,最多声明256个全局Keyword,因此我们要尽量节省Keyword的使用数量。其中一个技巧是使用 __(两条下划线),如:

#pragma multi_compile __ A
//OR #pragma shader_feature __ A

//-----------------------A模块----------------------
#if A
  return fixed4(1,1,1,1); 
#endif 
//---------------------------------------------------

return fixed4(0,0,0,1);
  • 此方式相比#pragma multi_compile A B 的方式,我们可以减少使用一个Keyword。
  • 此方式仍会编译成两个变体:一是不包含A模块代码的变体非A;二是包含A模块代码的变体A;
    默认为 __ ,即变体非A生效。

四.multi_complie_local

全局的Keyword只能有256个(而且Unity自身已经使用掉了一部分,所以实际可用的更少),这或许会最终对我们造成限制,而且大部分Keyword并不需要进行全局声明。

因此,我们可以使用multi_complie_local来声明局部的、只在该Shader内部起作用的Keyword,用法相似:

#pragma multi_compile_local __ A
//OR #pragma shader_feature_local __ A

//-----------------------A模块----------------------
#if A
  return fixed4(1,1,1,1); 
#endif 
//---------------------------------------------------

return fixed4(0,0,0,1);

但需要注意:

local Keyword仍有数量限制,每个Shader最多可以包含64个local Keyword
因为这种Keyword是局部的,Material.EnableKeyword仍是有效的,但对Shader.EnableKeyword或CommandBuffer.EnableShaderKeyword这种全局开关说拜拜
当你既声明了一个全局的Keyword A ,同时又声明了一个同名的、局部的Keyword A,那么优先认为Keyword A是局部的。

五.Shader编写规范

1.建议使用shader_feature时将定义语句写成完整模式,并且不要在一个语句中定义多个宏。

完整模式:#pragma shader_feature _ A,不建议写成#pragma shader_feature A。

不建议在一个语句中定义多个宏,如: #pragma shader_feature _ A B C,若一定要定义多个宏,请务必将其写成完整模式,不使用完整模式在切换shader时可能会与想要的效果不一致,具体原因尚未测得。

2.若在shader中使用shader_feature,请为这个shader指定一个CustomEditor

每个使用shader_feature来定义Keyword的shader都需要再末尾加个 CusomEditor “xxxx”,并在代码中实现类xxxx(需继承自UnityEditor.ShaderGUI),用来对Keywords定义进行设定。

这么做是因为Material中的部分Keyword是由shader中的属性(Properties)所控制的。比如shader中含有_NormalMap的属性并且定义了与_NormalMap相关的Keyword,这个Keyword需要在Material含有NormalMap时添加,不含NormalMap时移除。这个功能可由自定义的CustomEidtor实现。

具体如何写这个CustomEditor类可参考Unity builtin_shaders\Editor\StandardShaderGUI.cs。该文件可去Unity官网下载,下载时选择内置着色器即可。如图

image

3.其他减少变体数量的方案&注意事项

1.内存中ShaderLab的大小和变体成正比关系。从减少内存方面应该尽量减少变体数量,可以使用 #pragma skip_variants
2.在使用ShaderVariantCollection收集变体打包时,只对shader_feature定义的宏有意义,multi_compile的变体不用收集也会被全部打进包体
3.2018.2新功能OnProcessShader可以移除无用的shader变体。比#pragma skip_variants更合理
4.项目前期介入美术效果制作流程,规范shader宏定义使用,防止TA为了美术效果过度使用宏定义的情况,以过往项目经验来看,到后期进行此项工作导致的资源浪费非常之大
5.ShaderLab在相关shader加入内存时就已经产生,但如果没有被渲染的话不会触发CreateGPUProgram操作,如果提前在ShaderVariantCollection中收集了相关变体并执行了warmup的话,第一次渲染时就不会再CreateGPUProgram,对卡顿会有一定好处

六.Unity自身还内建的一些multi_compile,自建的multi_compile会产生较多的变体,实际开发中如果没有能力把控好变体建议慎用:

  • multi_compile_fwdbase:为基本前向渲染通道编译多个变体,不同的变体可能需要处理不同的光照贴图类型,或者一些使用了主平行光的阴影,而另一些禁用了。(参考Unity渲染管线)

  • multi_compile_fwdadd:为附加前向渲染通道编译多个变体,不同的变体可能需要处理不同类型的光源——平行光、聚光灯或者点光源,亦或者它们附带cookie纹理的版本。

  • multi_compile_fwdadd_fullshadows:同上,不过包含了可以让光源拥有实时阴影的功能。

  • multi_complie_fog:为处理不同的雾效类型(off/linear/exp/exp2)扩展了多个变体。

  • multi_compile_particles:主要包含了软阴影粒子的宏定义,可以在质量设置界面开关软阴影粒子。

你可能感兴趣的:(Unity shader中的multi_compile和shader_feature宏定义)