3.3.2 包体优化:资源文件
同样,对资源文件大小的优化也能有效减小包体体积,下面将介绍对资源文件的一些处理和优化。
3.3.2.1 着色器资源的优化
在着色器的编译过程中,着色器元数据主要用于帮助编译器优化着色器代码,提高着色器的性能和效率,如图 3.13所示。同时,在着色器运行时,着色器元数据还可以帮助应用程序动态地创建管线布局和绑定描述符集等,从而提高应用程序的可维护性和性能。在做Shader Half精度优化的时候,UE处理元数据时是有bug的,其他部分没有问题,就是将顶点类型和着色器种类作为给定参数,输出对应不同平台的文本结果时,Metal平台就变成了IR中间文件。
图3.13 着色器的编译流程
首先要考虑的就是控制着色器变体的数量,图3.14所示的是一些可能增加材质变体的属性。
图3.14 材质变体
大家都知道Swith开关及其他各种开关,最终产生的变体数量很大,几十万个都是有可能的,所以这里使用材质实例模板的概念来控制变体数量,如图 3.15所示。材质实例不可以有变体,所有的变体都来自母材质或者材质实例模板,模型只能引用材质实例。
图3.15 材质实例模板
在着色器中,分支语句是一种常用的控制流结构。根据分支语句的判断条件,可以将分支分为动态分支和静态分支。动态分支在着色器运行时根据变量的值来决定执行哪一条分支语句。例如,以下代码片段展示了一个简单的动态分支语句:
在上述代码中,根据变量lightType的值来决定执行哪一条分支语句。由于变量lightType的值是在着色器运行时确定的,因此这是一个动态分支。静态分支在着色器编译时已经确定执行哪一条分支语句。例如,以下代码片段展示了一个简单的静态分支语句:
在上述代码中,宏USE_NORMAL_MAP的值是在着色器编译时确定的,因此可以根据USE_NORMAL_MAP的值在编译时决定执行哪一条分支语句。由于分支语句是在编译时确定的,因此这是一个静态分支。
动态分支和静态分支各有优缺点。动态分支可以根据变量的值来决定执行哪一条分支语句,更加灵活,但在运行时需要进行分支判断,可能会降低着色器的性能。静态分支可以在编译时确定执行哪一条分支语句,更加高效,但相对应地会增加着色器的变体。
在一定的条件下,着色器可以使用Uniform常量来避免一些静态分支,小的计算可以通过动态分支进行计算。这里复用了其他项目实现的动态分支的节点,配合UE的分支节点,来做动态分支的计算,如图 3.16所示。
UE的分支节点的if语句编译后的代码如下,if语句的计算被展开,无论是否满足if条件,都会进行所有的计算:
图3.16 分支节点
这里多次采样是相对重度的操作,所以考虑使用动态分支去处理这种情况。扩展的动态分支节点展开的方式如下,可以看到,由分支关键字来确定if语句的逻辑执行流程,执行时只会运行if语句某个分支的代码,并不会全部计算:
同时这里需要注意,寄存器的数量是有限制的,在着色器中,寄存器的数量会影响着色器的执行效率。通常情况下,寄存器是GPU中用于存储中间计算结果和变量的主要存储设备。寄存器数量的限制取决于GPU硬件的特性和着色器的复杂度。当着色器使用的寄存器数量超过GPU硬件规定的限制时,可能会导致着色器使用缓存来存储寄存器数据。由于缓存访问速度比寄存器访问速度慢,因此会导致着色器的执行效率降低。另外,寄存器数量的增加还可能导致着色器使用更多的内存带宽,从而进一步降低着色器的执行效率。
图 3.17所示的是使用Mali的一个工具对寄存器做的分析,如果出现Register Spilling(寄存器溢出)的情况,会有一些说明。
图3.17 使用Mali的工具分析寄存器的数量
除了对着色器变体的控制,还可以对着色器代码做进一步压缩。
.ushaderbytecode是UE中的一种二进制格式的着色器字节码文件,其包含了已经编译好的着色器程序,如图 3.18所示[6]。在UE中,着色器代码通常是使用UE自带的材质编辑器或程序代码编写的。当需要将着色器代码打包到游戏或应用程序中时,UE会将着色器代码编译成二进制格式的.ushaderbytecode文件,并将其与其他资源一起打包到pak文件中。
图3.18 着色器代码的结构
.metallic文件是苹果公司开发的Metal图形编程框架中使用的一种着色器代码格式。.metallic文件是通过MetalKit框架的MTKMaterialLoader类进行加载和解析的,只支持流式加载,不支持文件被压缩。
由于着色器中包含大量的重复性字节段,所以使用Zstd+训练好的字典来代替.ushaderbytecode文件中默认的LZ4压缩算法。表3.4展示了不同字典大小下Zstd算法的压缩率。
表3.4 不同字典大小下Zstd算法的压缩率
虽然实际上Zstd + 字典的方式会减少着色器代码文本大小,但是解压单个着色器代码文本的速度并没有LZ4算法快。解压操作是在Render Thread中进行的,增加了Render Thread的耗时。把着色器代码从Pak中拷贝出来的操作是在Game Thread中执行的,由于压缩后着色器代码文本的大小减小了,实际从Pak中拷贝出来时申请的内存大小变小了,申请内存的开销会缩小,还减少了Game Thread的耗时。综合分析,实际效率并没有被拉低,最后采用了这种方式。
对于.metallic文件,也可以采用同样的方法对AIR文件进行Zstd+字典方式的压缩,但在实践中简化了这一方式,直接对.metallic整个文件使用Zstd压缩,在引擎初始化之前解压到本地去使用,虽然实际增加了App本身的占用大小,但是App的安装包体减小了。
3.3.2.2 贴图资源的优化
贴图资源一直是游戏里占用磁盘空间较多的一类资产,我们有针对性地对这方面内容做了优化。
● ASTC HDR/RGBM:对支持HDR的机型使用这种支持HDR的贴图压缩格式,不支持就回退到RGBM的编码模式。
● Cube Reflection Clean:UE中使用CubeMap在Pak包中会有冗余,清理掉这部分内容。
● Enable Compression for arbitrarily sized texture:UE对某些尺寸的贴图不支持压缩,这里做了一些优化。
● ETC1S/UASTC:基于ASTC和ETC的算法思路做了转码的修改,压缩率能提高一半以上。
3.3.2.3 Pak的压缩格式
Pak的默认压缩算法选择了Oodle,对于Pak中的大部分资源使用Oodle压缩,压缩比较高。表3.5展示了使用不同压缩算法后最终的资源大小。
表3.5 不同压缩算法的结果对比
3.3.2.4 数据结果
表3.6展示了在iOS平台和Android平台上资源最终优化的大小对比。可以看到,资源大小相比较早的版本有了大幅降低。
表3.6 资源文件优化前后的数据