Unity的Shader绝对是个黑盒,困扰了很多开发者,我也陆陆续续被困扰了好久。首先我们需要知道Unity的Shader从编写到最终被GPU执行经历了什么?
编写Shader的时候我们用的是HLSL语言,在打包的时候会根据目标平台生成对应的着色器语言,如Android平台是GLSL,iOS平台则是MSL。无论是整包,又或是构建Assetbundle,GLSL或MSL会被打进包中。这也能解释当使用AssetStudio这样的解包工具解开Shader的时候是无法看到原本的HLSL代码的。如果打包时变种比较多,那么生成GLSL或MSL就会慢,打包时间就会增长。
GLSL和MSL的大小决定了Profiler中shaderlab的内存,通常为了减少内存都会从减少着色器宏开始。当某个Shader需要被渲染的时候,运行时Opengle或metal的驱动程序会对它进行编译,也就是在Profiler中大家能看到的Shader.Parse还有Shader.CreateGpuProgram,这样GPU就才可以真正执行了。如果着色器过多势必就会卡顿。
GPU这点和CPU是不同的,CPU基本都提前编译成X8**ARM对应的.so,运行时则不再编译(JIT的方式除外)。而GPU?本质上只要能提供GPU能认识的二进制代码就可以提前编译,但是GPU的种类比较多,如果提前编译会造成不必要的浪费,而且游戏过程取决于运行条件并不是所有Shader都需要执行,现在采用的都是运行时在编译。
注意上面我用红色标记的两处,1.打包慢、2.运行卡,下面接着围绕这两点进行展开。
1.打包慢
打包为什么会慢呢? 不就是把HLSL文本文件变成GLSL或MSL文本文件吗,这些都是最简单的文件写入有什么慢的呢?确实!如果变种少的话一点都不慢,可是如果变种多了,多到一定程度就不是了。如下图所示,URP自带的shader,如果剥离了无用的变种,就已经包含了3072个变种了。
不就生成3072个着色器么?打包也不至于卡主吧。搞错了再来.JPG 我们取消勾选Skip unused shader_features
629万个变种啊,要生成629万个着色器文件,这还只是一个shader,(大家可以试着生成600W个文本文件看看自己电脑卡不开)想不打包慢都难啊。
#pragma shader_feature _ _TEST_ON //用到才会编译
#pragma multi_compile _ _TEST_ON //始终都要编译
平常在写Shader的时候,运行时需要开关的宏,通常会写multi_compile,打包前能确定的会选择shader_feature,这点没错,必须要严格贯彻。
如下图所示,Unity为了加快打包速度, 会同时开多线程进行编译GLSL和MSL。自unity2018开始,提供了Shader变种用户剥离,也就是下图底部的描述。
打包是发生在编辑模式下的Editor中,这个环境其实是比较复杂的。很有可能有些shader已经标记的#pragma shader_feature,但不知道为啥被编辑模式用到了(正式模式下是用不到的)却被无情的打进包中,导致profiler中shader lab内存增加和打包时间增长,所以项目中最好自己实现变种剥离。