1 前言
最近一段时间AR技术成为了时下热门,越来越多的应用开发者投身到这些技术中来。应用中出现了3D的AR场景,图形学也成为了必备的技术基础。在开发过程中,往往为了追求更好的效果而使用了更加高清的素材,使得本就内存吃紧的手机面对更加严峻的挑战,尤其是对iOS开发者而言。
为了解决这个问题,我们使用了纹理压缩技术。使用这个技术可以大幅度的降低APP的内存(共享显存)占用,从而在有限的内存限制下,使用更丰富的素材。
2 什么是纹理压缩
常见的图片文件格式,比如PNG,JPG,BMP等,是图像为了存储信息而使用的对信息的特殊编码方式。它存储在磁盘中,或者内存中,但是并不能被GPU所识别。
这些文件格式当被读入后,还是需要经过CPU解压成bitmap,再传送到GPU端进行使用。
纹理格式是能被GPU所识别的像素格式,能被快速寻址并采样。压缩纹理,是一种GPU能直接读取并显示的格式,使得图像无需解压即可进行渲染,节约大量的内存。
3 常见的压缩纹理格式
3.1 DXT
DXT纹理压缩格式来源于S3(Silicon & Software Systems)公司提出的S3TC(S3 Texture Compression),基本思想是把4x4的像素块压缩成一个64或128位的数据块,是有损压缩方式。DXT1-DXT5是S3TC算法的五种变化,用于各种Windows设备。
压缩率:DXT1,DXT4,DXT5为4:1,DXT2、DXT3为2:1
主要支持Windows平台及Tegra系列的GPU的Android手机
支持GPU:
3.2 ETC
Ericsson Texture Compression,是由 Khronos 支持的开放标准,在移动平台中广泛采用。它是一种为感知质量设计的有损算法,其依据是人眼对亮度改变的反应要高于色度改变。类似于DXT,ETC也是把4x4的像素块压缩成一个64或128位的数据块,也是有损压缩。
这个系列,可以说是适用机型最广的格式。
ETC1支持几乎所有市面上的Android机,所有iPhone
ETC2支持大部分高端Android机,iPhone 5S及以上
3.3 PVRTC
PowerVR Texture Compression,PVRTC格式与基于块的压缩格式,比如S3TC、ETC的不同之处是,它使用2张双线性放大的低分辨率图,根据精度和每个像素的权重,融合到一起来呈现纹理,并且2-bpp和4-bpp都支持ARGB数据。PVRTC格式压缩比较高,也是有损压缩。
这个系列,是iPhone支持最广的格式
只支持长宽相等且为2的幂次方的纹理
支持部分Android机(GPU:PowerVR系列),iPhone全系列机型
支持的GPU
3.4 ASTC
ASTC(Adaptive Scalable Texture Compression,自适应扩展纹理压缩),这是ARM提出的,去年被Khronos组织认可,纳入到标准中来,不过并不是强制性的
有多种压缩方式可选,具有不同的压缩率
这个系列,可以说是综合性能和使用便捷性最好的系列。
支持部分高端Android机型,iPhone6及以上机型
4 主要优缺点
在几乎不损害图片质量和显示性能的情况下,大幅度降低内存(显存)开销,纹理压缩就是这样的一个技术。
不过,任何的技术都有其适用范围和优缺点,需要仔细评估再决定。
4.1 主要优点
占用内存(显存)大幅度降低
无额外性能开销
使用方便,只需少量代码
4.2 主要缺点
硬件相关,要考虑兼容性
压缩纹理文件大小比常规PNG和JPG文件大
需要额外的制作工具,无法直接在移动端生成
5 如何使用压缩纹理
5.1 保存格式
压缩纹理是图片数据的一种编码方式,我们还缺少一个容器去承载。就像MP4文件是H264的视频的容器一样。
我们选择了使用KTX的格式。
KTX是一个为OpenGL和OpenGLES程序设计的纹理存储格式。它可以简单的辨别里面所存储的纹理格式和其他相关信息。
5.2 文件结构
Byte[12] identifier
UInt32 endianness
UInt32 glType
UInt32 glTypeSize
UInt32 glFormat
Uint32 glInternalFormat
Uint32 glBaseInternalFormat
UInt32 pixelWidth
UInt32 pixelHeight
UInt32 pixelDepth
UInt32 numberOfArrayElements
UInt32 numberOfFaces
UInt32 numberOfMipmapLevels
UInt32 bytesOfKeyValueData
for each keyValuePair that fits in bytesOfKeyValueData
UInt32 keyAndValueByteSize
Byte keyAndValue[keyAndValueByteSize]
Byte valuePadding[3 - ((keyAndValueByteSize + 3) % 4)]
end
for each mipmap_level in numberOfMipmapLevels*
UInt32 imageSize;
for each array_element in numberOfArrayElements*
for each face in numberOfFaces
for each z_slice in pixelDepth*
for each row or row_of_blocks in pixelHeight*
for each pixel or block_of_pixels in pixelWidth
Byte data[format-specific-number-of-bytes]**
end
end
end
Byte cubePadding[0-3]
end
end
Byte mipPadding[3 - ((imageSize + 3) % 4)]
end
5.3 使用KTX格式
typedef struct __attribute__((packed))
{
uint8_t identifier[12];
uint32_t endianness;
uint32_t glType;
uint32_t glTypeSize;
uint32_t glFormat;
uint32_t glInternalFormat;
uint32_t glBaseInternalFormat;
uint32_t width;
uint32_t height;
uint32_t depth;
uint32_t arrayElementCount;
uint32_t faceCount;
uint32_t mipmapCount;
uint32_t keyValueDataLength;
} KTXHeader;
KTXHeader *header = (KTXHeader *)[data bytes];
BOOL endianSwap = (header->endianness == 0x01020304);
self.width = endianSwap ? CFSwapInt32(header->width) : header->width;
self.height = endianSwap ? CFSwapInt32(header->height) : header->height;
self.internalFormat = endianSwap ? CFSwapInt32(header->glInternalFormat) : header->glInternalFormat;
uint32_t mipCount = endianSwap ? CFSwapInt32(header->mipmapCount) : header->mipmapCount;
uint32_t keyValueDataLength = endianSwap ? CFSwapInt32(header->keyValueDataLength) : header->keyValueDataLength;
const uint8_t *bytes = [data bytes] + sizeof(KTXHeader) + keyValueDataLength;
constsize_tdataLength = [data length] - (sizeof(KTXHeader) + keyValueDataLength);
NSMutableArray *levelDatas = [NSMutableArrayarrayWithCapacity:MAX(mipCount, 1)];
const uint32_t blockSize = 16;
uint32_t dataOffset = 0;
uint32_t levelWidth = self.width, levelHeight = self.height;
while (dataOffset<dataLength)
{
uint32_t levelSize = *(uint32_t *)(bytes + dataOffset);
dataOffset += sizeof(uint32_t);
NSData *mipData = [NSDatadataWithBytes:bytes + dataOffsetlength:levelSize];
[levelDatasaddObject:mipData];
dataOffset += levelSize;
levelWidth = MAX(levelWidth / 2, 1);
levelHeight = MAX(levelHeight / 2, 1);
}