突破內存的桎梏:移動端紋理壓縮應用與分析
導語
最近一段時間AR技術成為了時下熱門,越來越多的應用開發者投身到這些技術中來。應用中出現了3D的AR場景,圖形學也成為了必備的技術基礎。在開發過程中,往往為了追求更好的效果而使用了更加高清的素材,使得本就內存吃緊的手機面對更加嚴峻的挑戰,尤其是對iOS開發者而言。 為了解決這個問題,我們使用了紋理壓縮技術。使用這個技術可以大幅度的降低APP的內存(共享顯存)占用,從而在有限的內存限制下,使用更豐富的素材。
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);
- }
原文鏈接:https://www.qcloud.com/community/article/897529,作者:柯靈杰
【本文是51CTO專欄作者“騰訊云技術社區”的原創稿件,轉載請通過51CTO聯系原作者獲取授權】