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基本都提前编译成X86和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内存增加和打包时间增长,所以项目中最好自己实现变种剥离。
如下代码所示,强制在打包或者打Assetbundle中对_TEST_ON这个宏进行剥离,代码比较简单就不解释了。
另外打包慢还有个坑,如下图所示,千万不能把shader放到built-in shader settings中,无论#pragma shader_feature 或者 #pragma multi_compile 都会被强行打包。即使上面的方法也无法进行shader 剥离,无情增加包体大小还有shaderlab内存。关键如果这个shader又被打进Assetbundle又会在打一份,完全的浪费。
引擎内置了剥离的方法,比如游戏没用实时光,或者雾都可以进行剥离,但这个毕竟还是个黑盒,我建议最好还是自己来写shader剥离。
2.运行卡
运行卡最典型的就是Shader被重复打包,如特效使用的shader被每个特效都用了,但是由于特效被分开打包,导致每个Assetbundle中都包含了一份相同的shader。每次加载特效都需要编译导致运行卡。
一定要对Shader进行依赖打包,所有Shader放在一个Assetbundle中,其他的所有Assetbundle都依赖它。现在又衍生出一个新坑,由于shader中都使用#pragma shader_feature,导致依赖包中的宏都被剥离了。这时候很多人就会使用#pragma multi_compile就又会引起上面的内存打打包慢的问题了。
最后说一下我在项目中思路
项目禁止使用#pragma multi_compile,必须使用#pragma shader_feature,写了个工具批量提取参与打包的材质的变种组合。提取材质上会用的宏,由于shader会被美术同学更换,导致宏的残留。可以参考我这篇文章,Unity3D研究院之更换着色器后删除材质残留宏和属性(一百二十五) 提取出来以后生成变种收集器ShaderVariants.shadervariants文件。 最终变种收集器和所有shader一起打包就不会被剥离了。
静态分析上面的方法已经足够,但是有些是动态的,如:运行时打开或关闭一个宏。这个是无法静态分析,之前我写过一个工具对所有静态的宏进行变种组合
如:C是动态开启关闭的
A B
A B C
根据A B组合动态生成一个C的,最后发现如果有好多个需要动态启动的宏并它们可能还有and 和 or的 互斥 包含条件,最终导致自动生成的变种收集器依然很大。最终我选择了分支预测。
在这篇文章中,我对分支预测进行了详细的测试 ARM Mobile Studio性能优化(三)
游戏中存在的一些扰动、溶解、边缘光闪烁效果,这种可能和任何宏都会组合,而且代码里需要动态启动或者关闭,边缘光这种菲尼尔效果不仅要动态开启,需要时动态设置边缘强度,这种效果就没有什么好办法,无法离线确定它们的组合,可以考虑整体的分支预测。
还有一种效果就在某个特殊情况下启动,而且编辑时就能确定是否需要开启,这样可以在离线情况下直接启动这个宏,这样就就可以局部分支预测了。因为大部分情况下_TEST_ON这个宏不会被启动。
1 2 3 4 5 6 7 8 9 | #if defined(_TEST_ON) if(_Test) { //do something } #endif |
因为宏可以离线分析出来,而且已经启动,动态开关可以直接设置里面的某个变量,决定效果是否开关
material.SetFloat(“_Test”, 1f);