OpenGL ES 2.0 知識串講 (10) ——OpenGL ES 詳解IV(紋理優化)

出處:電子設備中的畫家|王爍 於 2017 年 7 月 19 日發表,原文鏈接(http://geekfaner.com/shineengine/blog11_OpenGLESv2_10.html)

 

上節回顧

上一節學習瞭如何從一張原始圖片中,獲取生成紋理所需要的信息,然後根據這些信息,通過OpenGL ES API在GPU內存中生成了一張紋理,並且還介紹了紋理屬性,知道了如何通過紋理座標將紋理映射到繪製buffer上。在瞭解了紋理重要性的同時,我們還知道了在應用程序中,紋理的大小影響着應用程序包的大小,也佔據了應用程序的大部分內存,在遊戲開發後期,對紋理進行各個層面的優化是一項非常重要的工作。所以,下面我們要對紋理進行優化。本節課,將重點講述各種壓縮紋理格式的處理辦法,如何在內存中管理紋理,以及總結了一些使用紋理的最佳方式。


壓縮紋理

通過上節課的學習,我們知道了紋理其實也就是一塊buffer,這塊buffer儲存了紋理的寬乘以高,那麼多個像素點的信息,比如寬高爲100*100的紋理,那麼這塊buffer就保存了這麼多像素點的信息,然而每個像素點的信息所佔的空間,是由紋理格式決定的,比如format爲GL_RGBA,type爲GL_UNSIGNED_BYTE的紋理,它在CPU中每個像素點需要佔用4個byte的空間,然而如果format爲GL_RGB,type同樣是GL_UNSIGNED_BYTE的紋理,在CPU中每個像素點需要佔用3個byte的空間,同樣的,把這兩張圖片的信息傳入GPU中生成紋理之後,在GPU中也會是第一張紋理佔用的空間比較大。所以紋理佔用空間的大小,跟紋理的尺寸大小,以及紋理格式都有關係。格式相同的情況下,紋理尺寸越大,佔用的空間越大。而相同紋理尺寸的情況下,有的格式佔用空間大,有的格式佔用空間小。當然,佔用空間大的紋理能表達的信息更多一些,比如GL_RGBA就比GL_RGB多了一個alpha通道。但是如果兩種格式都能表達足夠的信息,那麼我們會盡量選擇佔用空間小的格式來節省內存空間。而之前我們說到的紋理格式都是普通格式,在這節課,我們要介紹一種新的紋理格式,就是壓縮紋理格式,可想而知,這種紋理格式會把紋理信息進行壓縮,壓縮的時候雖然會根據壓縮比損失一定的紋理信息,導致紋理精度變低,但是如果對紋理圖片精度要求不是特別高的話,使用壓縮紋理可以節省大量的內存空間。

紋理非常佔用空間,進而導致會有三方面影響:1.如果紋理比較多的話,而且沒有做任何處理的話,那麼遊戲的包會比較大,用戶安裝起來會比較麻煩,這樣不太好;2.紋理比較大,而紋理需要從客戶端CPU傳給GPU,這樣會比較佔帶寬,而且傳輸數據也是非常耗電的。3.在CPU的時候佔主內存,而傳給GPU之後,也會佔用GPU內存。目前手機雖然發展比較快,但是依然有一些1-2G內存的手機在市面上,如果佔用過大內存,可能會導致遊戲閃退等不好的用戶體驗。

所以,針對這個又耗電,又影響用戶體驗,又佔用內存,卻又不得不使用的紋理,有幾種優化方式,第一個優化方式就是壓縮紋理。下面,我們將介紹壓縮紋理的概念、特點、使用方法。

我們知道,傳統的壓縮方案,比如JPG、PNG圖片本身都是壓縮圖片,都已經做了一定的壓縮,但是這些格式的圖片,能夠減小資源的大小,減小包體大小,但在把信息傳給GPU的時候,需要進行解包,在GPU中佔用一塊內存用於存儲紋理,所以這些壓縮圖片和普通圖片傳到GPU後,所佔用的GPU內存是一樣的。具體步驟就是:當一個圖形數據在被傳輸到GPU內存時,需要通過glTexImage2D這個API傳到GPU,而這個API只認識GL_RGBA等格式,所以需要先將這些壓縮圖片解壓縮到GL_RGBA或者GL_RGB等未壓縮的格式,才能傳入GPU。所以這些壓縮圖片意義不是很大,最多也只是讓遊戲包變小。而且雖然讓遊戲包變小了,但是還增加了一個解壓縮的過程,傳輸到GPU的時候,還是按照正常格式傳輸。1996年,斯坦福的三位教授發表了一篇論文叫做《基於已壓縮紋理的渲染》,提出了基於GPU的紋理壓縮方法,該方法使GPU可以直接從壓縮紋理中採樣並進行渲染,這也就使壓縮紋理有了可能。這種壓縮紋理,在CPU端需要使用特殊的原圖片,也就是比如PVR或者ETC格式的圖片,這些格式的圖片保存着生成壓縮紋理的信息,這些文件的大小要小於JPG、PNG等圖片,而且在傳輸給GPU的時候,不需要通過glTexImage2D這個API,而是通過壓縮紋理的專用API glCompressedTexImage2D,通過這種API使得圖片信息不需要經過解壓縮,直接可以傳給GPU,然後把信息保持壓縮狀態保存在GPU中,這種方法不僅能減小資源的大小,還可以節省CPU往GPU傳輸數據的帶寬,以及減少GPU的內存佔用。下面我以PVR格式來說明壓縮紋理,PVR格式是imagenation公司定義並提供授權的一種紋理壓縮格式,是目前在遊戲開發領域使用比較廣泛的一種壓縮格式,imagenation公司生成的GPU叫做powervr GPU,完全支持pvr格式,powervr這個GPU是蘋果的御用GPU,所以蘋果的設備,比如iphone等,基本都是支持PVR這種壓縮紋理的。pvr格式的圖片以pvr作爲後綴名。壓縮紋理有如下幾點好處:

  • pvr圖片原本就比jpg、png圖片小,這樣遊戲包體會比較小。
  • OpenGL ES API glCompressedTexImage2D識別PVR的格式,所以不需要解壓縮,就可以直接把pvr圖片的數據傳入GPU,再加上原本PVR格式的數據就少,所以這樣需要傳輸的數據也就很少,比較省帶寬。
  • 傳入GPU之後,也不需要解壓縮,所以也比較省內存。

壓縮紋理相比較於普通紋理以及我們已知的壓縮技術,需要有具備如下4個特點:

  • 雖然剛傳入GPU的時候不用解壓縮,但是使用的時候還是需要解壓縮的,爲了不影響渲染系統的性能,壓縮紋理需要解壓比較快,這樣才能保證使用它的時候能很快解壓出來。而我們平時所使用的壓縮技術,主要針對存儲或者文件傳輸進行設計的,不具備較快的解壓速度。
  • 支持精確讀取,根據上節課的內容,我們知道,我們會根據紋理座標去讀取紋理上的某些像素點的信息,而紋理中的任何位置都可能會被使用,所以我們需要先能快速精確定位到紋理中任何位置的紋素,然後把這些像素點解壓出來才能使用,所以要求壓縮紋理的技術必須能夠被快速精確的定位到。傳統的壓縮技術爲了保證最大的壓縮比例,一般都是使用可變的壓縮比例,這樣的話,如果想讀取某個像素的信息很難做到快速精準定位,往往需要解壓很大一部分相關的像素信息纔可以讀取到某個像素的信息。而壓縮紋理技術,則一般採用固定比例壓縮,固定比例只要知道偏移,壓縮比例係數,訪問紋素的時候,可以根據索引塊迅速定位到某一塊,然後獲取所需要的紋素信息。比如原本10000個數據壓縮成100個數據,那麼如果像讀取中間那個點,它的位置就從第5000個變成了第50個,這樣根據這個比例,就能精確定位到所需要的像素點。PVR等格式都是固定比例壓縮的,而JPG、PNG等格式都是非固定比例壓縮的。由於壓縮紋理的精準讀取特性與壓縮紋理的實現機制有關,在這裏我們展開說明一下壓縮紋理是如何被實現的。壓縮紋理是按照一個固定的壓縮比例進行壓縮的,壓縮算法會按照這個比例先將紋理分成多個像素塊,比如剛纔說到的100*100的紋理,也就是有10000個點,這10000個點會被分成塊,而我們假設壓縮比例爲4,也就是4個點爲一個塊,那麼這個圖片也就被分爲了2500塊,每個塊即包含了這4個點的信息,然後以塊爲單位來進行壓縮,將這4個點的信息進行壓縮,每個被壓縮的像素信息存儲在一個像素集合中,這樣就獲得了2500個壓縮後的像素集合,然後對這2500個像素集合製作一個塊索引圖,用於存儲每個像素塊的索引位置,這樣可以方便的找到對應的塊文件。然後將壓縮紋理傳入GPU,等到要使用的時候,可以根據紋理座標,確定要去紋理中的哪個點,然後計算出這個點屬於哪個塊,以及在這個塊中的偏移量,然後根據塊索引圖找到這一個像素塊,並對這一塊進行解壓縮,這裏的解壓縮只需要將這一小塊解壓縮即可,所以不會佔用很多的內存,然後再根據偏移量從解壓縮之後的內容中讀取到我們需要的紋理值。這樣的話就實現了精確讀取,而這種壓縮算法,被稱爲基於塊的壓縮算法,它是以塊進行壓縮和解壓縮的。在實際操作中,對一個像素塊的數據,還可以根據實際情況進行緩存。這種快速的解壓使得圖形渲染管線可以不在CPU中對圖片進行解壓,而是將壓縮紋理直接保存在GPU內存中,這樣既減少了資源對磁盤的佔用,也減少了紋理在傳輸過程中所佔用的帶寬,並且還大大節省了對GPU內存的佔用。這種基於塊的壓縮算法在壓縮紋理中比較常見,然而還有一些其他的壓縮紋理格式,比如第一代的PVR格式PVRTC就使用了不同的紋理壓縮和讀取算法,在這裏就不繼續展開說了。
  • 傳統的圖像壓縮技術,大多要考慮圖像的質量。而對於壓縮紋理的技術,每個紋理只是場景中的一部分,整體場景渲染質量的重要性高於單個圖片,壓縮紋理不用太在意壓縮質量,首要是先保證遊戲性能,所以壓縮紋理通常使用有損壓縮。
  • 不用太在意編碼速度,因爲壓縮紋理的壓縮過程是發生在應用成之外,在線下進行的,並非在用戶玩遊戲的時候進行的,所以不需要較高的編碼速度。Imagination公司提供了一個叫做PVRTextureTool的工具,供開發者在線下進行PVR圖片的生成,確實比較慢,但是由於不會影響用戶體驗,所以壓縮速度無所謂。使用壓縮紋理的核心制約因素是解壓速度。

滿足上述特點的紋理壓縮方法不僅可以減小原圖片資源的大小,減少了應用程序客戶端向GPU傳輸紋理數據的帶寬,減少了移動設備對電量的消耗,還大大減少了對GPU內存的佔用,並能夠配合GPU進行高效渲染。另外,在使用方式上,在OpenGL ES中,壓縮紋理與其他紋理的使用方式基本一樣,同樣的通過紋理座標進行採樣,支持多級紋理,應用程序除了通過特殊的API glCompressedTexImage2D傳輸紋理之外,其他方面和普通紋理幾乎沒有區別了。說到多級紋理,其實壓縮紋理比普通紋理更高級一些,我們上節課也說過,因爲普通圖片比如JPG圖片一般不包含多級紋理信息,而壓縮紋理格式比如PVR圖片是可以直接通過PrvTexTool之類的工具將多級紋理包含在一個圖像文件中,這樣直接通過一個文件就可以給一個紋理的多級進行直接賦值了,而普通圖片可能需要多張才能對一個紋理的多級進行賦值。而通過賦值的方式生成多級紋理,雖然佔用了更多的存儲空間,但是不需要再在使用的時候去生成了。

以上就是壓縮紋理的基本概念。生成紋理、設置紋理屬性等代碼不區分壓縮紋理還是非壓縮紋理,唯一不同的就是壓縮紋理使用的是glCompressedTexImage2D,而非壓縮紋理使用的是glTexImage2D。下面我們來詳細介紹一下glCompressedTexImage2D。

void glCompressedTexImage2D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const GLvoid * data);

glCompressedTexImage2D的功能和glTexImage2D一樣,都是把準備好的數據,從CPU端傳遞給GPU端,保存在指定的texture object中。唯一的區別就是glTexImage2D傳輸的是用於生成普通紋理的數據,在GPU中生成普通紋理,而glCompressedTexImage2D傳輸的是用於生成壓縮紋理的數據,在GPU中生成壓縮紋理。

這個函數的輸入參數與glTexImage2D的輸入參數差別不大。第一個輸入參數的意思是指定texture object的類型,可以是GL_TEXTURE_2D,又或者是cubemap texture6面中的其中一面,通過GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, GL_TEXTURE_CUBE_MAP_POSITIVE_Z, or GL_TEXTURE_CUBE_MAP_NEGATIVE_Z來指定,我們說了cubemap的texture其實也就是由6個2D texture組成的,所以這個函數實際上也就是用於給一張2D texture賦值。如果傳入其他的參數,就會報INVALID_ENUM的錯誤。第二個是指給該texture的第幾層賦值。多級紋理的概念上節課我們已經說過了,這裏的level也就是指定給紋理的第幾層進行賦值,第0層爲base level,剛纔說了,一張PVR圖片可以包含多級紋理所需要的數據,也就是從一張PVR原始圖片讀出的數據,可以通過這個API給一張紋理的若干級進行賦值。如果level小於0,則會出現GL_INVALID_VALUE的錯誤。而且level也不能太大,如果level超過了log2(max),則會出現GL_INVALID_VALUE的錯誤。這裏的max,當target爲GL_TEXTURE_2D的時候,指的是GL_MAX_TEXTURE_SIZE,而當target爲其他情況的時候,指的是GL_MAX_CUBE_MAP_TEXTURE_SIZE。第三個參數internalformat,就是指定原始數據傳入GPU之後,在GPU中的格式。可以通過glGet這個API,傳入參數 GL_NUM_COMPRESSED_TEXTURE_FORMATS,可以查詢到當前設備支持壓縮紋理格式的個數,傳入參數 GL_COMPRESSED_TEXTURE_FORMATS,可以查詢到當前設備支持哪些壓縮紋理格式,如果傳入了當前設備不支持的internalformat,則會出現 GL_INVALID_ENUM的錯誤。第四個參數width和第五個參數height,就是原始圖片的寬和高,也是新生成紋理的寬和高,因爲兩者是一樣的。圖片信息以數據的形式從CPU傳到GPU,可能每個像素點格式和包含的信息會發生變化,但是圖片的大小,也就是像素點的數量,每行多少個像素點,一共多少行,這個信息是不會發生變化的。width和height不能小於0,也不能當target爲GL_TEXTURE_2D的時候,超過GL_MAX_TEXTURE_SIZE,或者當target爲其他情況的時候,超過GL_MAX_CUBE_MAP_TEXTURE_SIZE否則,就會出現GL_INVALID_VALUE的錯誤。第6個參數border,代表着紋理是否有邊線,在這裏必須寫成0,也就是沒有邊線,如果寫成其他值,則會出現GL_INVALID_VALUE的錯誤。第七個參數和最後一個輸入參數的意思是:data是CPU中一塊指向保存實際數據的內存,而imagesize指定這塊內存中從data位置開始,保存壓縮紋理信息的大小,單位是unsigned byte。如果data不爲null,那麼將會有imagesize個unsigned byte的data從CPU端的data location開始讀取,然後會被從CPU端傳輸並且更新格式保存到GPU端的texture object中。如果image size與壓縮紋理的格式、寬高、data中實際保存的數據不匹配,則會出現GL_INVALID_VALUE的錯誤。這裏要注意,因爲pvr圖片有可能一張圖片中包含多級紋理的信息,那麼每生成一級紋理的信息,就需要調用一次該函數,每次調用的時候第二個參數level,最後兩個參數,size和數據位置都會相應做出變化。

這個函數沒有輸出參數,但是有以下幾種情況會出錯,除了剛纔說的那些參數輸入錯誤之外,還有雖然OpenGL ES main spec沒有對壓縮紋理格式進行規定,但是OpenGL ES有一些extension對這些壓縮紋理的使用進行了限制,比如有些壓縮紋理格式要求紋理的寬高必須是4的倍數(pvr),如果參數組合沒有按照extension中的規定,則會出現GL_INVALID_OPERATION的錯誤。而如果data中保存的數據沒有按照extension的規定保存正確的數據,那麼會出現比如undefine的結果,或者是程序終止的情況。

void glCompressedTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const GLvoid * data);

這個API的功能和剛纔的glCompressedTexImage2D類似,顧名思義,剛纔那個API是給texture object傳入數據,這個glCompressedTexSubImage2D是給texture object的一部分傳入數據。這個命令不會改變texture object的internalformat、width、height、參數以及指定部分之外的內容。

這個函數的第一個和第二個輸入參數和glCompressedTexImage2D的一樣,用於指定texture object的類型,以及該給texture的第幾層mipmap賦值。錯誤的情況也與glCompressedTexImage2D一樣,target傳入不支持的值,則會出現GL_INVALID_ENUM的錯誤,level傳入了錯誤的數字,則會出現GL_INVALID_VALUE的錯誤。 第三個、第四個、第五個和第六個輸入參數的意思是:以texture object的開始爲起點,寬度進行xoffset個位置的偏移,高度進行yoffset個位置的偏移,從這個位置開始,寬度爲width個單位高度爲height的這麼一塊空間,使用data指向的一塊CPU中的內存數據,這塊內存數據的format爲第七個參數,大小爲第八個參數imageSize,對這塊空間進行覆蓋。這裏的format和glCompressedTexImage2D的第三個參數internalformat是一個意思,如果傳入了當前設備不支持的format,則會出現 GL_INVALID_ENUM的錯誤。如果image size與壓縮紋理的格式、寬高、data中實際保存的數據不匹配,則會出現GL_INVALID_VALUE的錯誤。如果xoffset、yoffset、width、height其中有一個爲負,或者xoffset+width大於texture的寬,或者yoffset+height大於texture的高,那麼就會出現INVALID_VALUE的錯誤。如果width和height都爲0也沒關係,只是這樣的話這個命令就沒有效果了。

這個函數沒有輸出參數。除了剛纔那些因爲參數問題導致的錯誤,還有,如果target指定的這個texture還沒有被glCompressedTexImage2D,以對應於format的壓縮紋理格式internalformat分配好空間。則會出現GL_INVALID_OPERATION的錯誤。如果參數組合沒有按照壓縮紋理格式format對應的extension中的規定,比如部分壓縮格式不允許只替換部分內容,那麼通過這個API,只能傳入xoffset=yoffset=0,以及width和height爲紋理的實際寬高,否則會出現GL_INVALID_OPERATION的錯誤。而如果data中保存的數據沒有按照extension中的規定保存正確的數據,那麼會出現比如undefine的結果,或者是程序終止的情況。

下面我們來介紹一下壓縮紋理的格式,剛纔我們都是以pvr這種壓縮紋理格式來進行串講的,那麼其實還是有很多種壓縮紋理格式的,不同的GPU支持不同的壓縮紋理格式。比如各種格式的PVR和ETC,所以使用壓縮紋理格式之前,還是需要確認一下當前設備是否支持。在OpenGL ES2.0中,雖然定義了glCompressedTexImage2D這個API供應用程序上傳壓縮紋理,但是OpenGL ES2.0並沒有定義任何紋理壓縮格式,所以壓縮紋理的格式通常由圖形硬件廠商或者一些第三方組織定義和實現的,而且每個GPU支持的壓縮格式format基本都是不同的,比如剛纔說的,蘋果公司使用的是powerVR的GPU,支持的比較好的是PVR,iOS全系列設備都支持PVR的壓縮紋理。android手機手機是按照khronos的標準,基本大部分android設備都支持ETC格式。khronos我們之前介紹過,是制定OpenGL ES等等一系列Spec的權威組織。這些都是大家默認的,但是其他一些壓縮格式則需要查詢具體的硬件支持信息,所以如果想知道GPU具體支持哪些壓縮格式,需要使用API glGetString傳入GL_EXTENSIONS,這樣獲取到設備支持的所有的extension,然後在這些extension列表中查詢到支持哪些壓縮紋理格式對應的extension,從而獲悉該設備支持哪些壓縮紋理格式。所以在寫遊戲的時候用到了壓縮紋理,最好還是通過這些函數來確認下支持哪些壓縮紋理格式。說一些後話,其實在OpenGL ES3.0中,提供了一個壓縮紋理的標準,也就是glCompressedTexImage2D必須支持某些格式,那麼當使用這些格式的壓縮紋理的時候,就不需要check了。

下面我來介紹一下PVR和ETC這兩種壓縮格式。

首先是PVR,PVR分爲PVRTC和PVRTC2這兩種壓縮格式,PVRTC2相對比與PVRTC作出的升級有:1.pvrTC2開始支持NPOT的紋理,2.增強了圖像質量,尤其表現在一些高對比度,大面積顏色不連續的部分,或者紋理的邊沿,3.更好的支持alpha預乘,alpha預乘是一種優化算法,我們會在之後的課程再介紹alpha預乘。

PVRTC和PVRTC2的都支持2bpp和4bpp這兩種壓縮比例,也就是說一個像素點的信息,可以壓縮到2位或者4位,我們知道未壓縮的一個像素點的信息可能有16位或者32位,所以壓縮比例是相當高的,PVR支持alpha通道。alpha通道還是很有用的,上節課我們說到的紋理格式中很多格式都是帶alpha的,比如GL_RGBA。雖然PVR支持alpha,但是如果用不到alpha通道,儘量還是不要用。舉個例子,一個RGBA32的圖片,每個像素佔32位,4byte,壓縮成pvr 4bpp,而一個RGB24的圖片,每個像素佔24位,壓縮成pvr 4bpp。壓縮後的大小一樣,那麼RGBA32圖片的損耗率一定比RGB24圖片的損耗率高,才能達到壓縮後同樣大小的效果,也就是說RGB24壓縮後質量更好一些。

Imagination公司提供了一個叫做PVRTextureTool的工具用於處理PVR格式的圖片,通過這個工具可以創建cubemap、字體紋理、生成多級紋理等,但是最主要還是用於生成pvr格式的壓縮紋理。PVRTextureTool除了支持pvr格式,還支持png、etc等格式的轉換。常用的轉換工具還有圖形學的texturepacker,以及iOS系統中Xcode自帶的texturetool等。

然後再說下ETC,ETC格式是愛立信公司於2005年提出的壓縮格式,ETC和PVR都是有損壓縮紋理格式,支持4bpp的壓縮比例,不支持alpha通道。從這裏看出ETC還是不如PVR的。而由於ETC不支持alpha通道,但是很多紋理是需要alpha信息的,那麼有如下兩種解決辦法:1.比如原本是GL_RGBA的圖片圖片,製作成ETC的時候,首先,先把RGB三個通道的信息壓縮成一張ETC圖片,然後把這張ETC圖片的高度增加一倍,多出來的這一塊空間寬和高就和原圖片寬高一致,這樣,把原圖片的alpha值壓縮到這塊空間中,做成一張灰度圖即可。最終結果就是得到一張ETC的圖片,但是這張圖片的高度是原來兩倍,下面的一半是原圖片的RGB信息,上面的一半是原圖片的alpha信息轉成的灰度圖。這樣應用程序中需要在PS中對紋理多做一次採樣,每個點的RGB採樣一次,A採樣一次。但是這種解決方案是有限制的,因爲它增加了紋理的尺寸,而紋理的尺寸在GPU中是有限制的,寬高不能超過GL_MAX_TEXTURE_SIZE,是有最高高度的,一般這個最高限制是2048。那麼假如原圖片高度超過1024,那麼擴大一倍,就超過2048了,就超出限制了,就會報錯了。所以使用這種解決方案,原圖片的高度需要在GL_MAX_TEXTURE_SIZE的一半以下。2.比如還是原本是GL_RGBA的圖片圖片,製作成ETC的時候,製作兩張ETC圖片,一張用於保存RGB信息,另外一張用於保存alpha信息,這樣就生成了兩張壓縮紋理,而這兩張壓縮紋理使用多重紋理的方案,把它們同時傳入同一個shader,然後再在這個shader就可以使用到原圖片的RGBA所有信息了。多重紋理上節課說過了,這裏就不再進行解釋說明了。Arm公司提供的工具Mali texture compression tool可以很好的處理這兩種解決方案。

ETC2現在已經支持alpha通道了,但是RGBA壓縮成ETC2後爲8bpp,RGB壓縮成ETC2爲4bpp,RGB壓縮成ETC1也是4bpp,所以還是很佔空間,不過ETC2的壓縮損耗率已經比ETC1好了,雖然還是不如PVR。Unity中提供一種壓縮格式叫做etc2 RGB + 1bit alpha 4bpp,也就是如果alpha只有1bit,那麼可以壓縮成這種4bpp的etc2格式,這樣內存終於省下來了。

其他壓縮紋理格式的使用方式與這兩種格式相似,這樣壓縮紋理部分就說完了。儘管OpenGL ES3.0提供了壓縮紋理標準,使各個平臺都可以使用同一種壓縮紋格式,但是目前市面上的設備還需要很長時間才能完全普及OpenGL ES3.0。因此我們仍然需要對不同設備使用不同的壓縮紋理格式。通過壓縮紋理, 遊戲包變小了,傳輸紋理的時候帶寬也節省了,在GPU中的內存佔用量也變小了。這是紋理優化的第一步,下面,我們我們將來說一下紋理優化的第二步,紋理緩存,texture cache。

紋理緩存

OpenGL ES 2.0知識點中關於紋理優化的API只有壓縮紋理,我們可以通過選擇適當的圖片格式,或者使用壓縮紋理來減少紋理對內存的佔用,但是由於紋理優化太重要了,所以在這裏說另外幾種紋理優化的辦法。壓縮紋理只是針對單一紋理的,而遊戲中需要使用到大量的紋理,所以需要有個機制對紋理進行管理,去管理紋理的加載,使用和銷燬,儘量使同一張紋理不被重複加載,也儘量把不用的紋理刪除,這樣可以節約內存。那麼下面,我們將介紹一下紋理緩存機制,去管理場景中每個紋理的生命週期。

紋理緩存的目的是:雖然通過壓縮紋理,可以對一張紋理進行了優化,但是在遊戲中需要使用大量的紋理,而且很多紋理會被使用不止一次,假如每次使用紋理的時候都要對紋理進行重新上傳,比如遊戲植物大戰殭屍,裏面有10個相同向日葵,每個向日葵都使用相同紋理,那麼假如每次畫向日葵我都要重新傳上傳一次紋理,那麼肯定是不好的,最好的辦法就是我只上傳一個texture,可以提供給這10個向日葵使用。而且如果是在畫向日葵的時候才調用glTexImage2D或者glCompressedTexImage2D去上傳紋理,那麼也是不太好的,因爲這兩個API都是需要耗費一定時間的,可能會導致遊戲變卡。另外如果下一幀需要使用某個紋理,但是由於內存不夠,在上一幀剛把該紋理從內存中刪除掉,那麼也是不好的。換句話來表達,就是:紋理緩存系統的主要目標是使當前場景需要顯示的紋理駐留在內存中,而開發者的職責則是定義哪些是當前場景需要使用的資源,我們始終應該在進入一個場景時預加載相關紋理,這是一個耗時的過程,而這個過程是在主線程中完成的,不適合在遊戲進行的過程中讀取和加載,所以要避免動態加載紋理。另外,在紋理的使用期間,它應該只被創建1次。 所以解決方案是需要實現三個功能:

  • 在進入場景的時候就把這個場景中要用到的所有的texture load好。
  • 所有的向日葵使用同一個紋理。
  • 當這個場景不再需要向日葵也不會在繪製新的向日葵的時候,把向日葵的紋理刪掉,以節省內存。這些解決方案就是通過texture cache實現的。

我們先來看一下紋理的生命週期,texture2D在被創建時就會從磁盤中加載數據並上傳至GPU內存中,然後在Texture2D沒有被銷燬之前,GPU會一直緩存這個紋理對象,可以通過glDeleteTexture銷燬這個texture2D對象。如果直接通過控制texture2D對象,來管理紋理的話,我們需要小心的處理texture2D的生命週期,必須在紋理不再被使用之後刪除,而且我們還要注意有可能紋理只是暫時不被使用,如果不小心刪除了,當下次使用的時候還需要重新創建。所以綜上所述,通過這種方式來管理非常複雜,所以我們一般不會直接去創建texture2D對象,而是通過texturecache來創建和銷燬texture2D對象。texturecache提供了對Texture2D對象更好的管理方式。

texture cache是單例,通過這個texture cache去創建紋理的話,會先查看一下原本紋理緩存中是否有存放這個原始圖片對應的紋理,如果有的話則直接拿出來使用,如果沒有的話,就創建一個紋理,保存在紋理緩存中,供這次以及以後如果需要的時候使用。每次創建的時候,把texture引用計數加1。所以如果創建了N個使用相同紋理,那麼紋理引用計數則爲N+1,而即使當切換場景的時候,由於紋理緩存是單例,不會把紋理release的,所以雖然切換場景的時候會把所有的紋理清除,那麼紋理的引用計數還爲N + 1 - N = 1。所以,無論如何切換場景,加入紋理緩存的紋理將一直存在着,這樣也就實現了剛纔我們說到的紋理緩存的第二個功能,無論繪製多少個向日葵,哪怕是不同場景中的向日葵,都使用同一張紋理。

我們剛纔說到的第三個功能,當確定不再使用該紋理之後,希望能把該紋理刪除。但是根據剛纔我們對引用計數的計算,只要將紋理加入texture cache,那麼除非texture cache沒了,不然紋理引用計數至少爲1,也就是通過系統是無法自動刪除這個texture了。但是沒關係,texture cache可以提供了一些remove紋理的函數,比如removeUnusedTextures函數,通過剛纔對引用計數的分析,我們也就知道了假如紋理沒有被使用,那麼引用計數就是1,而removeUnusedTextures這個函數,就是將所有不在被使用的,引用計數爲1的紋理進行release。但是這個函數有一點不好,比如當前場景中沒有向日葵也沒有豌豆射手了,我就把向日葵和豌豆射手的紋理全部清除了,但是我一會可能還要創建豌豆射手,那樣我又要重新創建紋理了。所以texture cache需要有直接removeTexture和removeTextureForKey的函數,這樣就直接只把向日葵的紋理刪除掉,因爲開發者知道這個時候向日葵的可以刪除了,但是豌豆射手的還有用,所以先把向日葵的紋理單獨刪除掉。但是有可能這個時候場景還有向日葵,紋理的引用計數不爲1,那麼通過這個函數,就將其引用計數減一,比如場景還有2個向日葵,那麼引用計數就從3變成2,當場景中這兩個向日葵也銷燬後,引用計數就變成0,該紋理將被銷燬。另外還需要removeAlltexture,就是假如遊戲快結束了,之後再也不會使用這些紋理了,那麼對所有的紋理進行release,紋理緩存不再保存和關心這些紋理的信息了,等場景結束的時候,這些紋理將被徹底銷燬了。所以texture cache是通過這些函數,可以讓開發者根據遊戲的邏輯,進行紋理的刪除。

這樣,就還剩下第一個功能沒有解決掉,就是在遊戲剛開始的時候就把所有要使用到的texture都load進來,這個是需要開發者自己來控制的,但是當然texture cache也應該提供了一些相關的函數來實現。舉個例子,比如我在第一個場景中需要用到300個texture,那麼根據這個功能要求,我需要在遊戲一開始就預load 300個texture,但是假如一次性預load這麼多texture是會非常卡的,如果全部放在第一幀去處理,那麼第一幀估計要等很久。然而texture cache需要有函數addImageAsync,這個函數會把300個texture分爲300幀,每一幀去預load一個texture。這樣分攤下去,就不會很卡了。這個是texture cache爲預load提供的方法。

而這樣還不夠,因爲雖然它實現了預load,實現了多個使用相同紋理的sprite使用同一張紋理,但是什麼時候將紋理從紋理緩存中刪除,實際還需要開發者自己來控制,剛纔我們只是簡單的說了一句,開發者需要根據遊戲邏輯來對紋理進行刪除,但是如果全部紋理都需要開發者來控制邏輯會有些複雜。其實在這裏我們也會介紹一些機制來幫助開發者來控制。

我們先來看一下在實際的遊戲中對資源管理的需求。通常在遊戲循環中只應做一些邏輯計算,以更新各種遊戲對象的狀態。爲了不影響遊戲循環,我們應該在進入場景時,或者其他一些異步時間,預加載所有需要的資源文件,將它們緩存起來,並在適當的時候刪除緩存以減少對內存的佔用。然而每個資源對生命週期有不同需求,舉例如下:

  • 有些資源在遊戲開始時就需要載入,並且駐留在內存中,直至遊戲結束,比如每個場景通用的一些按鈕等元素。
  • 有些資源的生命週期對應於特定的場景,如某個boss只在某個關卡出現,屬於這個關卡特需的資源,別的關卡並不需要。
  • 還有一些資源很難定義生命週期,比如跑酷遊戲中的資源和玩家跑動的距離有關,這時則需要小心的進行動態預加載。

我們並不能簡單通過texturecache去完美解決這個紋理,這個時候需要開發者要思考一下,本張場景到什麼階段,哪些紋理可以被刪除了,以及切換場景的時候,還要判斷一下,哪些紋理在下個場景還會被用到。所以有這麼一種紋理管理機制,可以供開發者使用,用於管理當一個紋理在連續兩個場景都會用到的情況。比如第一個場景用到了紋理123,而第二個場景用到了紋理234,那麼假如在切換場景的時候,把紋理123通過removeAlltexture全部刪掉,那麼在進入場景2,的時候,234又要被load,所以其中23這兩個紋理剛被刪掉,又要被load進來,就會造成一種浪費。那麼可以這樣,利用引用計數,在切換場景的時候,先不執行removeAlltexture函數,這樣,噹噹前場景結束的時候,這個場景的剩餘紋理的引用計數均爲1,然後進入下一個場景,將下一個場景需要用到的紋理引用計數加一,那麼根據剛纔的例子,紋理1的引用計數繼續保持爲1,紋理23的引用計數變成2,紋理4的引用計數爲1,然後再把剛纔那個場景用到的紋理引用計數減一,這樣紋理1的引用計數變成0,紋理23的引用計數變成1,紋理4的引用繼續保持爲1,這個時候刪除引用計數爲0的紋理,就把紋理1刪除掉了,紋理23留在了場景2,沒有被刪掉。然後對引用計數不爲0的進行加載,而紋理23由於沒有被刪掉,所以不會再次被加載了,只需要加載紋理4即可。這樣,切換了兩個場景,一共只需要load 4張紋理,其中紋理23只load了一次,而之前一共需要load 6張,紋理23需要load兩次,這種解決方案會比較省資源一些。這樣,我們便能夠靈活的處理資源中在多個場景之間的共享,並有效的在每個場景僅保留需要的資源。對於每個場景或者關卡,我們只需要定義其所需要的所有資源列表,資源管理器就會在進入場景或者關卡之前預加載所需的數據,並在離開場景或者關卡的時候刪除不再被使用的資源。

在這裏展開說一下,其實並非只有紋理才能通過這種機制來管理,資源信息除了紋理,還有很多,比如音頻數據等。這些資源也可以通過這種機制來管理場景或者關卡層面的資源。通過cache對資源進行緩存,通過引用計數解決場景之間資源的重用。

還有一個開發者需要自己控制的地方,剛纔也提到了,就是大的遊戲,比如跑酷,用到的紋理很多,雖然在進入場景的時候使用了異步加載的方式,但是還是不太好,最好還是開發者控制一下,分階段進行load。也就是動態預加載,動態預加載不是在場景開始的時候加載資源,也並非在需要的時候加載資源,而應該小心的估計那些資源即將被使用,提前動態的按需加載相關資源,比如跑酷就可以在剛進入場景的時候加載前50KM要使用到的資源,然後到50KM再加載一次資源,依次類推,每50KM加載一次新的資源,總之,我們需要根據遊戲的特點,在一個場景或者關卡內定義很多異步預加載方案,實現巨大場景下游戲的無縫平滑體驗。

總結

紋理時每個圖形應用程序中的重要內容,對其使用不當就容易導致很嚴重的性能、內存、耗電等問題。然而,紋理在應用程序中,並不是一個獨立的部分,它和各個系統都有着緊密的聯繫。

硬件層面

  • 提升紋理傳輸速度。在底層,它可以和GPU進行交互,通過對GPU進行優化可以實現更快的傳輸、像素讀取等數據操作。所以,在硬件層面,遊戲引擎公司,比如cocos、unity,都會和硬件公司打交道,看看是否可以提高硬件的加載速度,這樣從CPU往GPU傳輸紋理的時候可以更快。
  • 增加內存,儘量緩存紋理。除了提高數據傳輸速度,還有一個辦法是提高內存,我們在上面也說過,如果下一幀需要使用一張紋理,但是在使用之前,剛剛由於內存不足,導致這張紋理剛被刪除掉,那麼就需要對這張紋理進行重新加載,那麼這種情況是非常糟糕的,所以,如果能在GPU中儘量緩存多一些紋理,這樣的話會提高紋理緩存的命中率,這樣的話也是很好的。
  • 特殊的壓縮紋理格式。硬件層面還有最重要的一個方面,就是支持特殊的壓縮紋理,這樣,遊戲引擎公司通過對硬件的瞭解,就可以針對硬件的不同,提供和硬件更匹配的解決方案。

當然,以上這三個方面的提升,即使遊戲引擎公司不要求,硬件公司也會去做的,幾乎所有的硬件廠商公司都會針對自己的硬件產品提供一些特定的優化,這些優化可以用來提升紋理的傳輸速度、獲得更快的數據讀取速度、針對某些數據類型的計算進行優化。而這些通常會設計到一些特殊的GL extension,當然,它帶來的性能提升也是很明顯的。所以,遊戲公司只需要去和這些硬件公司合作即可。

軟件層面

  • 紋理預加載。在應用程序方面,主要是基於引擎提供的功能,使用一些更好的方式管理和使用紋理。所以,在軟件層面,首先是始終要提前預加載紋理,避免在遊戲運行中動態加載資源。通常我們應該在進入一個關卡或者其他時機提前加載資源,並讓它們駐留在內存中,直到不再使用該資源爲止。這樣做能改善應用程序的渲染性能,不會造成類似卡頓的糟糕體驗。上面我們也介紹過texturecache提供的預加載函數,並且,還介紹了一種基於引用計數的方式來管理資源,這樣,就可以很好的處理各種資源的預加載,以及在場景、關卡之間的過渡。
  • 刪除不用紋理。減少不用紋理的佔用量,發現不再使用的紋理資源,則將其remove掉,這要求開發者必須清楚的定義每個資源的生命週期。texturecache可以提供的一些函數,以及基於引用計數的資源管理方式,可以幫助開發者方便的管理資源的生命週期,及時釋放不再使用的資源,開發者只需要描述紋理使用的生命週期即可。另外,我們也可以手動計算紋理所佔用內存的大小,以手動清理一些資源。以上這兩點,上面已經詳細介紹過了,在這裏就不再展開解釋了。
  • 紋理合並。將小紋理合併成大紋理來減少繪製次數。雖然現在DC已經不對性能產生太大影響了,但是兩次DC之間切換渲染狀態依然是引起CPU功耗的大戶,所以合併紋理,合併VBO、IBO,進而減少DC,是節省了CPU大量時間的,只需要注意一點,由於合併紋理,導致帶寬壓力變大(一次傳入過多的BO信息和紋理),所以要避免性能bound在帶寬上,做一下平衡
  • 使用多級紋理。使用多級紋理,可以來減少GPU內存的佔用,如今的智能設備具有不同的分辨率,並且差異極大,應用程序通常都按照比較高的分辨率來設計。這樣的話,針對分辨率比較小的機型,使用大的紋理是沒有意義的。舉個例子,假如一個手機的分辨率爲800*600,但是我們原本紋理的尺寸爲1024*1024,那麼其實整個手機的屏幕也沒有那麼大,這張紋理對應在手機上的尺寸可能只有512*512,那麼如果沒有多級紋理,而保持使用大分辨率紋理的話,需要從1024*1024的紋理中,採樣出來512*512個像素值信息,所以,最終的結果還是從這個大紋理中只取出來了512*512個像素點信息。所以,針對一些分辨率低的設備,僅上傳相應級別的多級紋理即可。我們可以通過計算設備的分辨率和資源的分辨率來決定要使用哪一級的多級紋理,從而減少對低端設備內存的浪費。而且使用多級紋理,可以減少對內存帶寬的佔用,因爲只需要上傳低級別的mipmap,也就是上傳給GPU的信息少了,這些內存帶寬的使用也就少了。還有,使用多級紋理,在GPU對紋理進行採樣的時候也更快,因爲原本需要從1024*1024個像素點分析獲取512*512個像素,現在只需要從512*512個像素點分析獲取數據了,這樣採樣的效率會大大提高,今兒提升渲染的性能。但是多級紋理會佔用更多的CPU內存,大概多1/3左右的內存,所以是一個用CPU內存換帶寬和GPU內存的方法。
  • 使用多重紋理。目的也是爲了減少draw call,舉個例子,我們想要繪製一個太陽和一間房子,這兩個物體個對應一個紋理,如果沒有使用多重紋理,那麼我們會先使用太陽的紋理以及VBO和IBO繪製出來太陽,然後同樣再根據房子的紋理以及VBO和IBO繪製出來房子,這樣就需要繪製兩次,且由於兩次繪製是串行執行,所以,佔用了大量的時間。然而,如果我們使用多重紋理,一次性把太陽和房子的紋理都傳入渲染管線,然後把太陽和房子的VBO和IBO合併,在shader中做好邏輯,使用兩套uv座標,多重紋理可以充分利用GPU並行執行的能力,減少管線的切換等,從而能夠有效的提升渲染的性能,這樣,也就可以通過一次draw call,將太陽和房子都繪製出來了。所以我們應該儘量在一次繪製命令中,傳入更多的紋理來代替多次繪製。
  • 最後一種方法,是使用alpha預乘,使用alpha預乘可以減少透明紋理在場景混合時的計算量。

資源層面

在資源方面,使用的紋理格式、大小等,均對應用程序的性能有直接的影響。在開始講這一塊的優化之前,我先講述一下如何計算紋理所佔據的內存大小。

雖然紋理的大小可以使用一些工具來查看紋理的內存佔用,比如Xcode自帶的OpenGL ES Analytics等,但是,在應用程序中,計算紋理的內存佔用仍然非常有必須。一方面,我們可以打印出來遊戲進行中各個時間點內存的大小,並將這些內存佔用關聯到具體的紋理上,這樣我們就能夠更好的對紋理的使用進行優化,比如有些資源過大,或者有些資源佔用時間過長等。另外一個方面,在運行過程中,計算內存的大小可以給應用程序設定一些警戒值,從而及時對紋理資源進行釋放,以達到最佳的性能。

下面我們來說如何計算紋理所佔據的內存大小,通過前面講解的與紋理格式相關的知識,我們很容易對紋理所佔內存進行計算,其計算公式爲:紋理所佔內存大小=紋理寬*紋理高*bpp。這裏所計算出來的結果的單位爲bit,而bpp的全稱爲bit per pixel,也就是每個像素佔據多少位。比如RGBA8888的格式,每個像素佔據32位,那麼分辨率爲1024*1024的紋理,所佔據的內存空間爲1024*1024*32 = 4MB。所以,只要知道紋理的格式,就可以計算出紋理佔據的內存大小。

所以,在資源層面,我們可以通過一些方式來減小資源的大小。

  • 使用合理格式的紋理。比如使用GL_RGBA8888和GL_RGBA4444的bpp分別爲32和16,那麼使用16位紋理格式比32位的紋理格式要少佔用一半的內存,這樣針對精度要求不高的紋理,使用GL_RGBA4444就可以省去很多內存。再比如一些用於背景等非透明圖象的紋理,就可以使用比如GL_RGB565之類的格式,由於不需要alpha通道,也就不使用帶alpha通道的紋理格式,也能大大減少對內存的佔用。還有對應alpha通道要求簡單的圖像,則可以使用GL_RGBA5551的紋理格式。
  • 第二個方法是,使用壓縮紋理,比如PVR格式的bpp爲2或者4,壓縮比非常高,上面已經詳細介紹過這個方案了,這裏也就不再展開說明了。

紋理是遊戲開發的重要內容。紋理的重要性往往要在遊戲開發的後期纔會暴露出來,早期程序和美工往往會肆無忌憚的使用紋理、特效、動畫,到臨近發佈的時候纔會突然面臨內存、耗電等極其嚴重,甚至影響遊戲發佈的紋理。所以,這節課和上一節課,從各個角度深入講述紋理的相關知識,希望大家可以明確與紋理相關的各個方面的細節,以實現各個方面的優化,從而使得遊戲具有更好的性能和體驗。

本節教程就到此結束,希望大家繼續閱讀我之後的教程。

謝謝大家,再見!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章