内存优化: 纹理压缩技术
相比普通格式图片,纹理压缩可以节省大量显存和 CPU 解码时间,且对 GPU 友好。
背景
有损压缩。所有的压缩纹理均为有损压缩,因此需要开发者 or 设计师验证压缩效果是否符合预期; 尺寸要求。部分压缩纹理要求宽高相等(PVRTC),或者宽高必须是2的幂次方,使用有些不便; 体积。压缩纹理虽然显存占用小,但是文件体积通常会比 JPEG 更大(看具体压缩格式),IO时间会更长; 格式&兼容性问题。压缩纹理格式多样,需要针对不同平台选用不同格式,意味着同一份素材可能需要存储多份格式;
主流纹理压缩格式、原理及兼容性情况
▐ 纹理压缩格式
| ||
▐ 压缩纹理素材生产
.ktx格式的压缩纹理素材,随后就可以在项目中直接使用了。不同格式的压缩纹理生产工具也不一样(PVETextTool、Adreno Texture Tool、...),而为了在各个平台中都能使用,通常需要生成不同格式的压缩纹理,社区有一些工具做了二次封装,可以生成多种格式的压缩纹理,如texture-compressor等。▐ KTX文件格式
KTX(Khronos texture)是一种通用的纹理压缩存储格式,OpenGL(ES)、Vulkan等均支持,KTX文件中包含了纹理加载所需的所有参数及数据,比如format 、type、宽高等等,更多信息见wiki。
如下是一个ktx文件的内容:
基于这个格式,可以实现KTX Loader,用于解析KTX资源,生成纹理(通常游戏引擎会自带)。如下所示,读取KTX文件到ArrayBuffer然后解析拿到元信息:
// KhronosTextureContainerconstructor(arrayBuffer, facesExpected, baseOffset = 0) { this.arrayBuffer = arrayBuffer; this.baseOffset = baseOffset;
// Test that it is a ktx formatted file, based on the first 12 bytes, character representation is: // '´', 'K', 'T', 'X', ' ', '1', '1', 'ª', '\r', '\n', '\x1A', '\n' // 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A const identifier = new Uint8Array(this.arrayBuffer, this.baseOffset, 12); if (identifier[0] !== 0xAB || identifier[1] !== 0x4B || identifier[2] !== 0x54 || identifier[3] !== 0x58 || identifier[4] !== 0x20 || identifier[5] !== 0x31 || identifier[6] !== 0x31 || identifier[7] !== 0xBB || identifier[8] !== 0x0D || identifier[9] !== 0x0A || identifier[10] !== 0x1A || identifier[11] !== 0x0A) { return; }
// load the reset of the header in native 32 bit uint const dataSize = Uint32Array.BYTES_PER_ELEMENT; const headerDataView = new DataView(this.arrayBuffer, this.baseOffset + 12, 13 * dataSize); const endianness = headerDataView.getUint32(0, true); const littleEndian = endianness === 0x04030201;
this.glType = headerDataView.getUint32(1 * dataSize, littleEndian); // must be 0 for compressed textures this.glTypeSize = headerDataView.getUint32(2 * dataSize, littleEndian); // must be 1 for compressed textures this.glFormat = headerDataView.getUint32(3 * dataSize, littleEndian); // must be 0 for compressed textures this.glInternalFormat = headerDataView.getUint32(4 * dataSize, littleEndian); // the value of arg passed to gl.compressedTexImage2D(,,x,,,,) this.glBaseInternalFormat = headerDataView.getUint32(5 * dataSize, littleEndian); // specify GL_RGB, GL_RGBA, GL_ALPHA, etc (un-compressed only) this.pixelWidth = headerDataView.getUint32(6 * dataSize, littleEndian); // level 0 value of arg passed to gl.compressedTexImage2D(,,,x,,,) this.pixelHeight = headerDataView.getUint32(7 * dataSize, littleEndian); // level 0 value of arg passed to gl.compressedTexImage2D(,,,,x,,) this.pixelDepth = headerDataView.getUint32(8 * dataSize, littleEndian); // level 0 value of arg passed to gl.compressedTexImage3D(,,,,,x,,) this.numberOfArrayElements = headerDataView.getUint32(9 * dataSize, littleEndian); // used for texture arrays this.numberOfFaces = headerDataView.getUint32(10 * dataSize, littleEndian); // used for cubemap textures, should either be 1 or 6 this.numberOfMipmapLevels = headerDataView.getUint32(11 * dataSize, littleEndian); // number of levels; disregard possibility of 0 for compressed textures this.bytesOfKeyValueData = headerDataView.getUint32(12 * dataSize, littleEndian); // the amount of space after the header for meta-data
...}▐ 使用压缩纹理(WebGL)
在 WebGL 上使用纹理压缩主要有如下步骤:
下载纹理压缩素材;
解析ktx文件;
判断设备支持的纹理压缩格式;
通过getExtension获取纹理压缩扩展;
上传纹理压缩数据到GPU;
其中上传纹理主要指
var ext = gl.getExtension('WEBGL_compressed_texture_etc');
var texture = gl.createTexture();gl.bindTexture(gl.TEXTURE_2D, texture);
gl.compressedTexImage2D(gl.TEXTURE_2D, 0, ext.COMPRESSED_RGBA8_ETC2_EAC, 512, 512, 0, textureData);▐ 兼容性情况
Android平台: Android平台由于机型、厂商众多,纹理压缩的支持情况较为复杂;其中ETC1支持的最为广泛,但是由于ETC1不支持Alpha通道,导致其使用场景有限,ETC2覆盖度也挺高但是需要启用OpenGL es 3.x;据google play统计,Android中高端机型对ASTC的支持度覆盖度有77%以上(具体到GPU型号上,高通骁龙415及以上(2015),ARM Mali T624(2012)及以上,NVIDIA Tegra k1(2014)及以上)。
iOS平台: iOS平台
PVRTC格式支持最广泛,苹果也推荐使用此格式;在2013 A7芯片发布后,开始支持(ETC/ETC2)格式,2014 A8芯片及以上,开始支持ASTC格式;
ETC + ASTC,iOS平台高版本使用ASTC、低版本PVRTC兜底即可覆盖所有设备。开发者运行时可以通过API glgetString(GL_EXTENSIONS)获取当前设备支持的压缩纹理格式,WebGL通过getSupportedExtensions()API获得相同信息。纹理压缩性能表现
▐ 体积
素材大小1024x1024:
结论:
相比jpeg等图片格式,纹理压缩通常体积会更大,这会导致IO时间变长;
不同格式的纹理压缩体积也不一样,压缩率高体积虽然降下来,但是素材质量会降低,使用时需要权衡;
纹理压缩格式GZip压缩效果不明显;
▐ 下载时间 & 内存
测试机型: pixel4、iPhone11ProMax
游戏引擎: pixi.js
压缩纹理在小程序实际场景中的性能表现
批量加载JPEG纹理内存增长情况
批量加载ASTC纹理内存增长情况
结论:纹理压缩格式相比普通纹理内存优势巨大,可以减少50%以上内存占用,但与此同时,素材下载时间会延长;
▐ 纹理上传GPU时间
不同纹理格式GPU上传时间
结论:纹理压缩格式GPU上传时间几乎可以忽略不记,相比普通纹理具有巨大的优势,也可以抵消一部分压缩纹理下载的耗时;
小程序Canvas纹理压缩实现方案
小程序下,我们是基于 OpenGL ES API 封装 WebGL API,纹理压缩也不例外,由于WebGL扩展中支持的纹理压缩格式在OpenGL ES中都有对应实现,比如 WEBGL_compressed_texture_astc扩展对应到GL的扩展名为 GL_KHR_texture_compression_astc_ldr等,因此只需要根据扩展名称映射到OpenGLES实现即可,比较简单,这里不再展开。
总结
纹理压缩在现代计算机图形中占据重要定位,现如今主流移动设备GPU都已支持纹理压缩,在实际场景中可以充分利用此能力优化游戏应用以带来更好的用户体验。
参考
http://sv-journal.org/2014-1/06/en/index.php?lang=en#8
https://developer.android.com/guide/playcore/asset-delivery/texture-compression
https://docs.unity3d.com/es/2019.4/Manual/class-TextureImporterOverride.html
https://blog.imaginationtech.com/pvrtc-the-most-efficient-texture-compression-standard-for-the-mobile-graphics-world/
https://cesium.com/blog/2017/02/06/texture-compression/
团队介绍
阿里跨平台技术人才储备丰富,独行快,众行远,欢迎优秀的你加入【淘系终端体验平台-跨平台技术团队】,一起打造靠谱的跨平台方案!这里有H5容器、Weex、Flutter、小程序、游戏互动等诸多解决方案,既有技术深度也有广泛业务场景,欢迎优秀的小伙伴来一起搞事情,一起把技术做稳一起为业务提效,手淘跨平台技术团队欢迎你的加入!