HLSL 可以被编译为虚拟机字节码(IL或者DXIL), 就表示允许着色器程序被编译和离线存储。
shader.WarmupAllShaders 是不可控的, 会把shader的所有的变体 都warmup一遍,
而shaderVariantCollection.WarmUp 预热的只有你会使用到的shader的变体
shaderVariantCollection: 所以在 首包安装 加载所有的shader, 执行 ShaderVariantCollection.WarmUp, 这样所有的shader就编译和catch了
再次运行游戏,如果已经 shader 已经预热过了,再执行 shaderVariantCollection.WarmUp 就什么也不做,没有消耗(详见 官方文档),
一. shaderVariant的缺失,可能会造成不同平台的差异(unity工程editor模式和真机模式表现不一致)
例如:如果你在unity中editor模式下表现正常,但是打包到真机后出现黑片(lightMap未正常表现)?紫片(shader直接丢失)?--------------恭喜你----原因很可能是shader的变体 shaderVariant没有正常表现。
核查问题的方法:
(1)在editor模式下frameDebug 查看正常的表现shader的属性和使用编译情况(查看编译后的变体代码Show generated code)
(2)真机连接frameDebug调试核查表现失常的shader的属性中是否缺失属性例如:是谁偷走了我的lightmap?? 没能切换使用正确的shaderVariant
那么解决问题就是:首先确定 shaderVariant是否打包进去了(用bundle拆解工具 AssetStudio核查bundle内的信息),第二是否执行命令“切换到lightmap_on的shaderVariant”.
(附加上查看bundle的工具:链接: https://pan.baidu.com/s/18YC-KZsW0Xjls8DVKle-lQ 提取码: gyjg)
二.shaderVariant到底是啥玩意?!
举个例子,对于一个支持法线贴图的Shader来说,用户肯定希望无论是否为Shader提供法线贴图这个Shader都能正确的进行渲染处理。一般有两种方法来保证这种需求:
1.在底层shader(GLSL,HLSL等)定义一个由外部传进来的变量(如int),有没有提供法线贴图由外部来判断并给这个shader传参,若是有则传0,否则传1,在Shader用if对这个变量进行判断,然后在两个分支中进行对应的处理。
2.对底层shader封装,如Unity的ShaderLab就是这种,然后在上层为用户提供定义宏的功能,并决定宏在被定义和未被定义下如何处理。最终编译时,根据上层的宏定义,根据不同的组合编译出多套底层shader.
上述两种方法,各有利弊,对于前者由于引入了条件判断,会影响最终shader在GPU上的执行效率。而后者则会导致生成的shader源码(或二进制文件)变大。Unity采取的是后者,我们这里也只讨论Unity对后者的使用。
Unity的Shader中通过multi_compile和shader_feature来定义宏(keyword)。
#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
//---------------------------------------------------
(1)这个Shader会被编译成两个变体:一是只包含A模块代码的变体A;二是只包含B模块代码的变体B;
(2)指定的第一个关键字是默认生效的,即默认使用变体A;在脚本里用Material.EnableKeyword或Shader.EnableKeyword来控制运行时具体使用变体A还是变体B;
(3)它们声明的Keyword是全局的,可以对全局的包含该Keyword的不同Shader起作用;全局最多只能声明256个这样的Keyword;
(4)请注意Keyword的数量和变体的数量之间的关系,并可能由此导致的性能开销,比如声明#pragma multi_compile A B和#pragma multi_compile D E 这样的两组Keyword会产生 2x2=4 个Shader变体,但若声明10组这样的keyword,则该Shader会产生1024个变体
(5)区别!!!!特别注意!!!!:
如果使用shader_feature,build时没有用到的变体会被删除,不会打出来。也就是说,在build以后环境里,运行代码Material.EnableKeyword(“B”)可能不起作用,因为没有Material在使用变体B,所以变体B没有被build出来,运行时也找不到变体B
multi_complie_local:
(1) local Keyword仍有数量限制,每个Shader最多可以包含64个local Keyword因为这种Keyword是局部的,
(2)Material.EnableKeyword仍是有效的,但对Shader.EnableKeyword或CommandBuffer.EnableShaderKeyword这种全局开关说拜拜
(3)当你既声明了一个全局的Keyword A ,同时又声明了一个同名的、局部的Keyword A,那么优先认为Keyword A是局部的。
下面是示例说明:
1.如果你在shader中添加了
#pragma multi_compile _A _B
#pragma multi_compile _C _D
那么无论这些宏是否真的被用到,你的shader都会被Unity编译成四个variant,分别包含了_A _C,_A _D, _B _C,_B _D四种keyword组合的代码
2.如果是
#pragma shader_feature _A _B
#pragma shader_feature _C _D
那么你的shader只会保留生成被用到的keyword组合的variant,如果只用到—AC,那么只会产生一个variant,当你使用Material.EnableKeyword(——A_D)时是无效的,因为没有此variant
定义方式:
{
定义方式中值得注意的是,#pragma shader_feature A其实是 #pragma shader_feature _ A的简写,下划线表示未定义宏(nokeyword)。因此此时shader其实对应了两个变体,一个是nokeyword,一个是定义了宏A的。
而#pragma multi_compile A并不存在简写这一说,所以shader此时只对应A这个变体。若要表示未定义任何变体,则应写为 #pragma multi_compile __ A。
}
编辑器查看和定义keywords:
三.能够控制生成shaderVariant的3中方式:
在上面的“二”中讲到 multi_compile 和 shader_feature会产生shaderVariant,此外还有一种:Project Settings->Graphics->Always Included Shaders中添加shader:
在此处添加的shader,会产生该shader所有的变体打入包中(multi_compile的变体不用收集也会被全部打进包体)
如果没有设置到GraphicsSetting-> always included shader中,那么会打包到依赖它的ab中,如果设置了就不会打包进去。而是再构建的时候,就导入到你的游戏
四.有必要将所有的shaderVariant全部打进包内吗?
没有必要,我们只需要将需要用到的shaderVariant打包进去就好了!!!
那么,怎样才能确定哪些shaderVariant是需要用到的?
unity给我们提供了解决方案:ShaderVariantCollection!!!!!!
(首先shader是单独打包,不会和material打包在一起,不然会造成资源冗余)
(1)Shader Variant Collection是用来记录shader中哪些变体是实际使用的。其优点主要有:在shader_feature与multi_compile结合使用时,能够设置生成何种变体,从而避免生成不必要的变体;shader不必和material打在一个包中,避免了多个包中存在相同的变体资源;明确直观的显示了哪些变体是需要生成的(multi_compile的变体不用收集也会被全部打进包体)收集仅对shader_feature有效。
(2)自动添加:在场景中跑工程,上图中有一个 “Save to asset”按钮,unity会自动收集shaderVariant.
(3)手动选择:自动添加的可能会出现冗余,手动添加可以核查确保正确,shader是你写的,你肯定知道需要用到哪些宏
(4)配置好需要生成的变体后,将collection与shader打在同一个包中,便能准确生成面板中所配置的shader变体
五.其他注意事项
(1).内存中ShaderLab的大小和变体成正比关系。从减少内存方面应该尽量减少变体数量,可以使用 #pragma skip_variants。
(2).在使用ShaderVariantCollection收集变体打包时,只对shader_feature定义的宏有意义,multi_compile的变体不用收集也会被全部打进包体。
(3).2018.2新功能OnProcessShader可以移除无用的shader变体。比#pragma skip_variants更合理。
(4).项目前期介入美术效果制作流程,规范shader宏定义使用,防止TA为了美术效果过度使用宏定义的情况,以过往项目经验来看,到后期进行此项工作导致的资源浪费非常之大。
(5)变体众多的shader不仅影响首次提交到硬件时的编译时间,还对内存的占用有着巨大的关系,一个变体几十个的shader可能内存占用不多,也就几十kb不到,但是一个上千变体的shader其内存则是数兆不止
(6).ShaderLab在相关shader加入内存时就已经产生,但如果没有被渲染的话不会触发CreateGPUProgram操作,如果提前在ShaderVariantCollection中收集了相关变体并执行了warmup(预热)的话,第一次渲染时就不会再CreateGPUProgram,对卡顿会有一定好处。因此第一次加载shader先warmup一下,虽然等待的时间长但是仅此一次,当你退出游戏下次再进入也不会再执行warmup,可以有效避免渲染卡顿
(7)!!!!!
加入Always In Clude中,打包, ,和 把shader打成bundle,这两种方式在内存中的引用情况是不一样的。这个可以动手试一下。。。