為什么要用壓縮紋理
下面這張圖是一輛陸虎越野車模型所用的紋理,原始分辨率為1024 x 1024。
瀏覽器從服務端加載這樣一張圖片時,其格式通常為JPEG,文件尺寸只有166KB,但是當WebGL處理一張紋理時就需要按照位圖處理(這里所說的位圖是指沒有使用任何壓縮算法的原始圖片數據),如果圖像中每個像素需要RGB三個通道,每個通道需要8位空間,那么整張圖片就需要使用1024 x 1024 x 8 x 3 位的信息,也就是3M,這3M的信息都需要加載到GPU緩存當中,這和圖片文件采用什么樣的壓縮格式沒有任何關系。當調用gl.texImage2D方法時,瀏覽器內部就會將圖片文件進行解壓,轉換成位圖格式。如果圖片包含透明信息,那么RGBA格式那么還要額外增加內存使用。
在簡單了解WebGL處理紋理的過程后,會知道使用JPEG或者PNG文件作為紋理時會有這樣的問題:
- 需要有圖片解壓過程,比較耗時。
- 因為紋理數據較大,所以傳輸紋理數據耗時較多。
- 紋理數據占用內存較多。通常是瀏覽器和GPU各自保存一份位圖數據。
壓縮紋理的出現就是來解決這些問題,經過某種算法壓縮之后的紋理可直接被GPU使用,只有當shader進行紋理查詢(texture lookup)才會進行解壓操作,找到對應位置的像素顏色。GPU通常會對解壓過程進行優化從而提升性能。
瀏覽器的圖片緩存策略
為了更好的了解WebGL處理紋理時如何分配和使用內存,請看下面的實驗。
首先,頁面加載前文提到的陸虎車的紋理,打開Chrome的任務管理器(Task Manager),把 Image Cache 和 GPU Memory 這兩項勾選,查看圖片緩存和GPU內存用量。你可以打開這個demo自行觀察。
這時 Image Cache 顯示:4262K,GPU Memory 顯示:17.7M。
現在,我們注釋掉加載紋理的語句,這時候圖片不會加載,紋理也不會被繪制出來。
此時 Image Cache 顯示:0K,GPU Memory 顯示:13.7M。
如果加載圖片,但是不創建紋理會如何?
結果是 Image Cache顯示:166K,GPU Memory顯示:13.7M。
通過以上觀察可以推出以下結論。
- 當僅下載圖片時 Image Cache 以圖片原始格式進行緩存,當創建紋理時,瀏覽器會解壓成為位圖格式,並將位圖數據進行緩存。
- GPU Memory 以位圖格式保存紋理數據。
- 通過Chrome的Profile工具可以看到在JavaScript層面,其內存消耗只包含HTMLImageElement對象所占用的內存大小。
以上結論可以用下面這張圖來概括:
當使用壓縮紋理時,內存使用狀況變為:
壓縮紋理的種類
不同的GPU廠商會有不同的紋理壓縮格式,具體如下:
- S3TC/DXTn/BCn:桌面計算機常見的壓縮格式。名字雖然有不同叫法,但都是指同一種壓縮方式。通常以DDS文件格式保存。
- PVRTC/PVRTC2:iOS設備上使用的壓縮紋式。
- ETC/ETC2:隨着OpenGL ES 2.0 出現。
- ASTC:2012年出現的一種新壓縮格式。
- ATC:Adreno GPU支持的一種壓縮格式,Android手機上常用。
壓縮紋理支持情況
WebGL1.0里,壓縮紋理是通過擴展支持的,因此要看當前瀏覽器支持哪些擴展。具體判斷方法如下:
var availableExtensions = gl.getSupportedExtensions(); for (var i = 0; i < availableExtensions.length; i++) { if (availableExtensions[i].indexOf('texture') >= 0 && availableExtensions[i].indexOf('compressed') >= 0) { // show in console console.log(availableExtensions[i]); } }
在我的Macbook Pro Chrome上,上面代碼片段會輸出如下結果:
WEBGL_compressed_texture_s3tc
也就是說,Macbook Pro Chrome支持S3TC格式的壓縮紋理。
下面表格列舉了一些設備和瀏覽器對壓縮紋理的支持情況:
壓縮紋理工具
下面的工具可以將JPEG、PNG等常見的圖片轉換為壓縮紋理。此外還有很多在線轉換工具,這里就不一一列出了。
- Mali Android OpenGL GPU Texture Compression Tool:支持壓縮ASTC和ETC格式。
- PVRTexTool:可壓縮成PVRTC, ETC和ASTC格式。
- GPU Accelerated Texture Compression 可壓縮位DXTn(S3TC)格式。
壓縮紋理實戰
下面我們來通過實例來看如何實現壓縮紋理。由於兼容性,我們首先准備不同格式的壓縮紋理(可以用上面提到的工具提前對紋理進行壓縮)。
完整代碼請見這里。
下面來分析一下核心代碼,初始化時首先判斷當前瀏覽器支持什么格式的壓縮紋理,判斷方法如下:
var compressedTextureType = ['s3tc', 'etc1', 'pvrtc']; var currentCompressedTextureType = null; function getSupportedCompressedTextureType() { var availableExtensions = gl.getSupportedExtensions(); for (var i = 0; i < availableExtensions.length; i++) { for (var j = 0; j < compressedTextureType.length; j++) { if (availableExtensions[i].indexOf(compressedTextureType[j]) > 0) { var extension = gl.getExtension(availableExtensions[i]); // 下面這句話必須在getExtension之后調用 var formats = gl.getParameter(gl.COMPRESSED_TEXTURE_FORMATS); console.log(formats); for (var key in extension) { console.log(key, extension[key], '0x' + extension[key].toString(16)); } return { type: compressedTextureType[j], extension: extension, formats: formats }; } } } }
我的Chrome瀏覽器運行后會打印如下信息:
formats是一個數組,可以看到里面有四個數值:[33776, 33777, 33778, 33779]
遍歷extension會打印如下信息:
COMPRESSED_RGB_S3TC_DXT1_EXT 33776 0x83f0
COMPRESSED_RGBA_S3TC_DXT1_EXT 33777 0x83f1
COMPRESSED_RGBA_S3TC_DXT3_EXT 33778 0x83f2
COMPRESSED_RGBA_S3TC_DXT5_EXT 33779 0x83f3
這些信息就是瀏覽器支持壓縮紋理的具體格式,可以看到formats和extension中的信息是一樣的。只不過一個是數組,一個是對象。可以根據這些信息提前准備好壓縮格式。
在創建紋理的代碼中,使用 gl.compressedTexImage2D 替換原有的 gl.texImage2D 方法:
1
|
gl.compressedTexImage2D(gl.TEXTURE_2D, 0, type, width, height, 0, source);
|
這里 type 是前面通過extension獲取到的那些常量,需要根據當前紋理的具體格式選擇。source就是壓縮紋理的數據,這里需要注意的是不同壓縮格式獲取數據的方法都不一樣,通常壓縮紋理開頭部分存放一些描述信息,之后才是真正的紋理數據。
在Chrome中使用壓縮紋理后,打開Task Manager后可以看到 Image Cache 變為 0K,GPU Memory 也比之前有所下降。
壓縮紋理的優勢是節省內存開銷,但是紋理文件的尺寸卻比原來的JPG或者PNG要大,因此會影響加載的時間。那么項目當中是否要用壓縮紋理就需要權衡了。如果你的3D場景同時使用的紋理較多或者其他因素導致內存消耗較大,同時需要長時間運行,那么你可以考慮使用壓縮紋理,在場景初始化時需要預先加載必要的紋理,同時通過進度條指示狀態,這種做法類似大型3D游戲的初始化。要注意控制紋理的分辨率,過大的紋理壓縮之后仍然會上兆,這非常影響加載速度。