本期我们将剖析刚上新的Shader Analyzer 中和 Shader 变体相关的规则:“Build 后生成变体数过多的 Shader”、“项目中可能生成变体数过多的 Shader” 和 “项目中全局关键字过多的 Shader”。我们将力图以浅显易懂的表达,让职场萌新或优化萌新能够深入理解。
首先我们来了解下相关的概念与意义。
Shader 从字面意义来讲就是 “着色器”;功能上来讲就是用以实现图形渲染的一种技术,更直白地说就是一段实现特定功能的代码程序。Unity 工程中可以说所有物体的颜色、光照效果或质感等等,都和 Shader 有千丝万缕的关联。如图,Unity 2019.3.7 中在 Project 界面可以创建多种预设的不同类型的 Shader。
很多时候,不同效果之间只有一些微小的差距,为每一种渲染效果去专门写一个 Shader 是很不现实的。从设计原则的角度讲,我们应当尽可能共用重复的代码,而 Shader 的关键字(Keyword)就为我们提供了这个功能。
开发人员在写 Shader 时,可以在 Shader 的代码段中去定义一些关键字,然后在代码中根据关键字开启与否,去控制物体的渲染过程。如此一来同一份 Shader 源码就可以具备多种不同的功能。另外,我们可以在 Runtime 通过开启或关闭关键字的方式动态改变渲染效果。
这样在项目最终编译的时候,引擎就会根据不同的关键字组合去生成多份 Shader 程序片段。每一种关键字组合对应生成的程序就是这个原始 Shader 的一个变体(Variant)。
通俗地说,Shader 中的关键字就是一个个标签,方便材质在渲染时绑定不同的 Shader 变体,实现不同的效果。我们可以在 Shader 片段中使用编译指令(compile directives)来定义 Shader 关键字。从变体生成特点上可分为 “multi_compile” 和 “shader_feature” 两类,从作用范围角度可分为局部关键字和全局关键字。
在 Unity 中 multi_compile 类型的关键字定义方式如下:
该编译指令会导致编译时生成所有关键字组合的的变体,如下图:
而 shader_feature 类关键字定义方法如下:
一般来讲,带有 multi_compile 类关键字的 Shader,在 Build 时会把所有可能的关键字排列组合的变体全部生成,由此导致不必要的冗余和包体体积增大;但好处是方便动态选择 Shader 变体;
而对 shader_feature 而言,Unity 在 Build 时,不会将未使用的 shader_feature 关键字生成的变体 包含入内,只有实际被材质使用到的关键字对应的变体才会被 Build 和打入包中,从而减少了内存占用,精简了包体体积。
但代价是自己要做额外的工作,举例来说,有些 shader_feature 关键字对应的变体在 Build 时没有被材质使用到,但是在运行时可能会通过代码开启。这类变体实际需要使用,却没有被打入包中 ,就会导致理想中的效果无法生成。这时,就需要使用 Shader Variant Collection,手动将这些体加入到变体收集器里面。
需要说明的是,shader_feature 预编译指令行至少有两个关键字。如果只定义了一个关键字 KW_X,则会默认生成一个下划线关键字。以下两行指令等价:
一般 Shader 片段中 multi_compile 类关键字每增加一个,或者启用的 shader_feature 类关键字增加一个,该 Shader 的变体数量就会增加一份。而对于变体数与内存、显存的关系,UWA 曾做过以下实验:
使用 #pragma multi_compile 定义的一行关键字为一组,每组包含两个关键字,对产生的内存进行统计,结果如下:
由此可见变体数和 ShaderLab 的内存占用基本成正比。而由于没有使用 Shader 进行渲染,GfxDriver 内存不会增加,没有参与渲染的 Shader 变体是不会经历 CreateGPUProgram 传入 GfxDriver 内存中的。
然后我们来结合这次新功能中的相关规则进行具体说明变体对项目优化的意义。
对 Unity 项目而言,Shader 变体有其存在的积极意义。除了代码的共用与运行时渲染效果的动态改变之外,还增加了 Shader 程序在 GPU 上的执行效率。
对 GPU 来说,处理类似于 “if-else” 结构的分支语句不是它的强项,GPU 的特点和功能决定了它更适合去并列地 “执行” 重复性的任务,而不是去 “选择”。所以 Shader 变体的存在就很好地解决了这个问题,GPU 只需要根据关键字去执行对应的 Variant 内容就可以,避免了性能下降的可能。同时,项目在运行时,可以通过在代码中选择不同的 Shader 变体,从而动态地改变着色器功能。
但是 Shader 变体是一把双刃剑。在带来以上便利的同时,也存在着各种问题:
1)在 Build 阶段,过多的 Shader 变体数量会使得 Build 耗时明显上升,而最终的项目包体体积也会变得臃肿。
2)在项目运行阶段,Shader 变体会以其庞大的数量产生可观的内存占用,同时也会导致项目加载时间的增加,也就是俗说的 “卡顿”。
所以本条规则会扫描项目中的 Shader 脚本,根据项目中 Material 上开启的关键字情况去计算可能生成的变体数。开发团队可以在找出这些可能生成过多变体数的 Shader 后,结合项目实际情况去进行相应的修改。
由于 Unity 支持的全局关键字的总数有限(256 个全局关键字,64 个局部关键字),而 Unity 内部关键字已经占用了约 60 个 “名额”,所以我们建议开发团队尽可能使用局部关键字(shader_feature_local 和 multi_compile_local)。本条规则会对所有预编译指令定义的关键字进行识别,找出那些全局关键字过多的 Shader 以方便开发团队进行进一步的检查与修改。
项目进行打包(Build)的时候,会将项目实际使用的资源封装到包里面(如 Scenes In Build 中的场景依赖的所有资源等)。因此,并非所有的 Shader 资源都会被带入包中。另外,本文介绍的第一条规则,仅会检测目标路径下的 Shader 脚本文件,对于项目使用的一些内置的(Built-in)Shader 则无法检测到。所以本条规则的意义,就在于统计打包后实实在在使用的 Shader 资源对应的变体。
我们模拟了项目的 Build 流程,将那些在 Build 后生成变体数过多的 Shader 统计出来,方便开发团队根据项目的实际需求去进行进一步的检查和修改。(此外需要说明的是,本规则只支持 Unity2018.2 及其以上的版本。)
希望以上这些知识点能伴随本次的功能更新而在实际的开发过程中为大家带来帮助。需要说明的是,每一项检测规则的阈值都可以由开发团队依据自身项目的实际需求去设置合适的阈值范围,这也是本地资源检测的一大特点。同时,也欢迎大家来使用 UWA 推出的本地资源检测服务,可帮助大家尽早对项目建立科学的美术规范。
往期优化规则,我们也将持续更新。
《动画优化:关于 AnimationClip 的三两事》
《材质优化:如何正确处理纹理和材质的关系》
《纹理优化:让你的纹理也 “瘦” 下来》
《纹理优化:不仅仅是一张图片那么简单》
万行代码屹立不倒,全靠基础掌握得好!
性能黑榜相关阅读
《那些年给性能埋过的坑,你跳了吗?》
《那些年给性能埋过的坑,你跳了吗?(第二弹)》
《掌握了这些规则,你已经战胜了 80% 的对手!》