OpenGL ES 2.0 知識串講 (9) ——OpenGL ES 詳解III(紋理)

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

 

上節回顧

上面一節課,我們學習了一個OpenGL ES程序必須具備的一些API,從準備shader,到傳入繪製信息,到最後的執行繪製命令。然而在上節課結束的時候,我們也提到了OpenGL ES除了這些必備的API之外,還存在一些別的模塊。比如這節課我們要說的紋理。紋理,其實我們可以理解爲是存在於GPU中的圖片信息,是OpenGL ES中很重要的一個概念,也是遊戲開發的重要組成部分。我們看到的絢麗的遊戲界面,其實就是在一個個模型上,貼上紋理構成的。可以說遊戲中的這些元素,它們的形狀依靠的是頂點座標,而色彩基本都是依靠紋理。那麼這節課,我們主要對紋理進行介紹。


生成紋理前的準備工作

我們在上節課的時候學過buffer object的概念,說過buffer object其實就是對應GPU的一塊內存,可以從CPU這邊往GPU那邊傳輸數據給buffer object賦值,每個buffer object也有自己的屬性,比如GL_BUFFER_SIZE,usage等。而texture object和buffer object基本類似,也就是GPU中的一塊內存,這塊內存的數據也是從CPU端傳入的,而CPU端的數據,一般都是解析一張JPG或者PNG等格式的圖片獲取到的。那麼現在,我先介紹一下這些普通文件的數據內容,以及如何從中獲取到我們生成紋理所需要的信息。

我們知道想要表示一個像素點的顏色信息,一般需要4個通道,RGBA,每個通道需要8位,用於保存一個0-255的值。所以想要表示一個像素點的顏色信息,如果不壓縮的情況下需要32bit,也就是4byte,而我們的手機像素,比如iphone5,它的屏幕分辨率爲1136×640,也就是說,一張原始尺寸爲手機屏幕那麼大的圖片,所需要的空間爲1136*640*4=將近300萬byte,也就是將近3M,而我們可以猜測一個遊戲中,隨着切換場景,更換主角等,需要大量的紋理,也就是需要大量的圖片資源,雖然不是每個圖片資源的原始尺寸都需要像手機屏幕那麼大,但是大量的圖片資源,也會導致這需要大量的空間,而佔用了大量的空間,也就影響到遊戲的包體大小,在PC時代,遊戲的包體影響還不是特別大,大型遊戲的包體經常都是數GB,甚至更大。但是手機時代,一個手機,比如iphone的總存儲空間也就最多128GB,而大多數手機的總存儲空間也就幾十GB,甚至十幾GB。那麼包體的大小就不能太大,再加上,大家下載遊戲大多是通過Wi-Fi甚至是3G或者4G,追求的就是想快速下載下來,然後快速進行遊戲,所以包體的大小有時候也會決定一個玩家的去和留。而且比如Google Store直接對包體的大小有限制。再加上現在手機的屏幕分辨率越來越大,那麼對應的原始圖片也需要越來越大,所以遊戲的圖片資源的大小,就是遊戲開發者比較關注的一個地方。當然會有人說,那麼手機是可以擴展SD卡的,這就好比電腦可以接一個外接硬盤一樣,讀寫速度總歸是沒有自身存儲空間的讀寫速度快。也會有人說,可以搞一個小的包體,然後用資源包的形式下載資源,那麼第一次下載的時候包體就小了。但是這樣的話玩遊戲的時候總歸是要下載資源包的,這樣也就回到了玩家下載半天還沒有下載好,影響去留的問題上。

所以縮小包體,一直是遊戲開發者的一個關注點。縮小包體的一個重要方向,就是縮小紋理對應的原始圖片文件的大小。所以這些原始圖片通常壓縮爲PNG、JPG或者TGA格式,以佔用更少的磁盤空間。而基本不會使用BMP這種無壓縮的格式。

而想要生成紋理,第一步要做的就是讀取這些壓縮的原始圖片,在CPU端將它們解壓,然後由於傳入GPU的時候需要用到OpenGL ES的API,所以解壓之後還要轉換成可以被OpenGL ES API接受的格式,才能保證紋理的正確生成。

首先,我們根據原始圖片文件的絕對路徑,去讀取到這個文件中所有的數據,這裏我們讀取到的數據,包含了這個原始圖片的顏色信息,也就是我們剛纔計算的,一個圖片的顏色信息是這個圖片的像素點的個數*4那麼大一塊空間保存的信息,除了這些顏色信息,還會有這個原始圖片的文件格式等信息。在這裏都會讀取出來。假如這個原始圖片的文件中的內容不爲空,下一步會根據原始圖片的文件格式,調用不同的函數去解析原始圖片中讀取到的數據。由於每種格式的文件都是有着固定格式的,比如前多少位用於保存圖片的格式(指明該文件的圖片顏色信息中RGBA通道信息,以及每個通道的位數),再比如哪幾位保存圖片的寬和高,再比如哪一位開始保存的是圖片的顏色信息等。知道了這些信息後,就可以根據圖片的寬和高以及每個像素所佔的空間,算出了圖片中用於保存圖片顏色信息的總長度,並且根據起始點位置,就可以從圖片的數據中把圖片的顏色信息完全取出來了。截至到現在,我們已經從這個原始圖片文件中得到了我們想要的東西了,回憶一下,我們獲取到了圖片的寬高和顏色信息的格式,獲取到了圖片的顏色信息和保存顏色信息的這塊空間。那麼根據這些信息,我們自信已經可以生成紋理了。

從原始圖片中獲取到的這些信息我們可以稱之爲CPU端的內存,我們要用這塊內存中的數據去生成一張GPU中的紋理。在CPU端,這組數據是按照行的順序進行存放的,比如先存放了第一行第一列那個像素的信息,假如佔據了1個byte,然後存放第一行第二列那個像素的信息,依次類推,那麼假如原始圖片的寬度爲15,每個像素佔據1個byte,那麼在CPU端一行也就只有15byte。然後我們從這組數據中取數據去生成紋理的時候,也要按照這個順序一一讀取,然後把讀取到的數據去GPU中構建一張紋理圖片。那麼問題就來了。由於在CPU中數據也是按照組存放的,有幾行就有幾組,按照剛纔我們的假設在CPU端一行只有15個byte,假如我們讀取的時候假如按照8對齊,那麼先讀了8個,再讀8個,那麼就讀取了16byte的信息,也就出現了讀越界的情況。所以,一定要設定好對齊規則,這個對齊規則可以理解成一次性讀取CPU幾個數據。那麼在這裏我們可以看到,我們會根據圖片的寬度和CPU中每個像素佔的位數相乘得到CPU中每行的bit,然後除以8,得到CPU中每行佔據多少個byte。然後看看其能被多少整除。如果能被8整除,那麼我們每次可以從CPU讀取8byte的數據;當然也可能只能被1整除,比如圖片寬度爲15,每個像素佔的位數爲3byte,那麼每行所佔據的位數就是45byte,就只能被1整除。當然對齊的字節數越高,系統越能對其優化。所以使用紋理儘量使用POT,也就是power of two,2的冪次方作爲寬度。

所以不管如何,我們在這裏一定要設置好一個合理的對齊規則。而這個對其規則,是通過OpenGL ES API來設置的,glPixelStorei。

void glPixelStorei(GLenum pname, GLint param);

原則上說這個API是用於設置像素的存儲模式了,其實我們可以理解爲這個API是用於設置我們讀寫像素的對齊規則。

這個函數的第一個輸入參數只能是GL_UNPACK_ALIGNMENT或者GL_PACK_ALIGNMENT。將客戶端的顏色數據傳輸至GL服務端的過程稱爲解包unpack。相反,將服務器像素讀取到客戶端的過程叫做打包pack。我們剛纔說的就是GL_UNPACK_ALIGNMENT,也就是將數據從CPU端解包出來的時候的對齊準則。而GL_PACK_ALIGNMENT則是將數據從GPU端讀取出來的對齊準則。如果使用了其他參數,那麼就會出現GL_INVALID_ENUM的error。第二個輸入參數爲一個整形數據用於指定參數的新的值。默認爲4,可以設置的值爲1、2、4、8。如果使用了其他值,那麼就會出現GL_INVALID_VALUE的error。

這個函數沒有輸出參數。

那麼截至到現在,通過一個原始圖片,獲取生成紋理的全部信息,所有的準備工作,我們就已經做完了,下面開始講解,如何通過這些信息生成紋理。


生成紋理

void glGenTextures(GLsizei n, GLuint * textures);

剛纔我們說過,texture object與buffer object類似。都是GPU中的一塊buffer。我們也知道buffer object有glGenBuffers、glBindBuffer、glBufferData、glBufferSubData、glDeleteBuffers五大API,分別用於buffer object的創建,賦值和刪除。同樣的,texture object也有同樣的五大API,分別是glGenTextures、glBindTexture、glTexImage2D、glTexSubImage2D和glDeleteTexture,功能與剛纔buffer object的5個API基本一樣。比如glGenBuffers這個API,就是用於先創建texture object的name,然後在glBindTexture的時候再創建一個texture object。我們先說 glGenTextures,而glBindTexture這個API一會我們再進行說明。

這個函數的第一個輸入參數的意思是該API會生成n個texture object name,當n小於0的時候,出現INVALID_VALUE的錯誤。第二個輸入參數用於保存被創建的texture object name。這些texture object name其實也就是一些數字,而且假如一次性生成多個texture object name,那麼它們沒有必要必須是連續的數字。texture object name是uint類型,而且0已經被預留了,所以肯定是一個大於0的整數。

這個函數沒有輸出參數。當創建成功的時候,會在第二個參數textures中生成n個之前沒有使用過的texture objects的name。然後這些name會被標記爲已使用,而這個標記只對glGenTextures這個API有效,也就是再通過這個API生成更多的texture object name的時候,不會使用之前創建的這些texture objects name。所以回憶一下,這一步其實只是創建了一些texture object name,而沒有真正的創建texture object,所以也不知道這些創建的texture的維度類型等信息,比如我可以先說在OpenGL ES2.0中texture可以分爲2D和cubemap的,在OpenGL ES3.0還會有3D和2D_ARRAY的texture,這些就是texture的維度類型。而只有在這些texture object name被glBindTexture進行bind之後,纔會真正的創建對應的texture object,texture纔有了維度類型。

void glBindTexture(GLenum target, GLuint texture);

在上一個API,我們只創建了一些texture object的name,然後在glBindTexture這個API再創建一個texture object。

這個函數的第一個輸入參數的意思是指定texture object的類型,就好像buffer object分爲VBO和IBO一樣。texture object也是分類型的,剛纔我們已經說了紋理的維度類型,在OpenGL ES2.0中,分爲2D texture和CUBEMAP texture 兩種。2D texture比較容易理解,就是一張2D的紋理圖片,CUBEMAP的texture顧名思義,是用於組成Cube的texture,我們知道cube是立方體的意思,那麼立方體有6個面,所以CUBEMAP texture是由6張2D texture組成的。那麼在這裏,第一個輸入參數必須是GL_TEXTURE_2D或者GL_TEXTURE_CUBE_MAP。其中GL_TEXTURE_2D對應的是2D texture,GL_TEXTURE_CUBE_MAP對應的是cubemap texture。如果傳入其他的參數,就會報INVALID_ENUM的錯誤。第二個輸入參數爲剛纔glGenTextures得到的texture object name。

這個函數沒有輸出參數,假如傳入的texture是剛被創建的texture object name,那麼它還沒有被創建和關聯一個texture object,那麼通過這個API,就會生成一個指定類型的texture object,且與這個texture object name關聯在一起。之後指定某個texture object name的時候,也就相當於指定這個texture object。texture object是一個容器,它持有該紋理被使用時所需要用到的所有數據,這些數據包括圖像像素數據、filter模式、wrapmode等,這些數據我們一會再說。而且新創建的texture object,不管是2D texture還是cubemap texture,它們的狀態都是根據維度類型,GL所初始化好的默認值,這個texture object的維度類型將在其生命週期中保持不變,一直到該texture被刪除。創建和關聯完畢之後,就會把這個texture object當作是當前GPU所使用的2D texture或者cubemap texture。但是有一點需要注意的是,當時我們說buffer object的時候,也說了,讓創建和關聯完畢之後,這個buffer object也是當前GPU所使用的VBO或者IBO了,但是texture會稍微複雜一點,texture的相關知識點中,還有一個叫做紋理單元的東西。紋理單元就好比一個容器,一個GPU中最多可以有8個紋理單元,具體有幾個,需要看GPU的實現。紋理單元是用於盛放紋理的,一個紋理單元中同一個類型的紋理最多只能有一個,我們剛纔已經知道了OpenGL ES中只有兩種類型的紋理,2D texture和cubemap texture。所以一個紋理單元中最多只能有2個紋理。而GPU中同一時間一個thread的一個context中只能有一個紋理單元是處於active狀態,而一個紋理單元中同一時間同一個類型只能有一個texture,所以,我們我們創建和關聯一個texture,只是將該紋理關聯到了目前active的那個紋理單元,然而剛好,該紋理單元是處於被GPU使用狀態,所以才使得該texture是當前GPU所使用的2D texture或者cubemap texture。假設一種情況,我們這個texture依然屬於這個紋理單元,沒有被別的texture替換,但是這個紋理單元,不再被GPU使用了,那麼該texture也就不再是當前GPU所使用的了。這層邏輯非常重要。也就是說,buffer object直接被GPU所使用,可以直接通過glBindBuffer提交給GPU。但是texture object則多了一層,texture object被紋理單元使用,而紋理單元被GPU使用,所以glBindTexture,只是將texture提交給了紋理單元。默認的紋理單元是GL_TEXTURE0,我們是通過glActiveTexture這個API來切換被使用的紋理單元,關於glActiveTexture我們一會再說。如果傳入的texture已經有關聯的texture object了,那麼只是把該texture object指定爲當前GPU所使用的紋理單元的2D texture或者cubemap texture。然後該紋理單元之前使用的2D texture或者cubemap texture就被從紋理單元這個容器中拿出來了,不再屬於該紋理單元了。當然由於GPU正在使用這個紋理單元,所以這個texture object也就被順便當作爲當前GPU所使用的2D texture或者cubemap texture。這種操作在我們寫OpenGL ES代碼的時候經常使用,因爲我們可能會有很多遊離的texture,但是隻有8個紋理單元。所以,如果要使用一個已經創建好的紋理,那麼就需要先將其通過這個方式放入一個紋理單元中。還有另外一種方法,假如一個紋理單元中已經有了一個2D的紋理,但是我們想要用另外一張紋理,那麼可以把已有的紋理中的內容修改成我們想要的內容,通過glTexImage2D/glTexSubImage2D,但是不建議這種方法,因爲牽扯到了CPU到GPU的數據傳輸,佔用了帶寬,會比較耗費資源。而且還有一點需要注意的是,如果這個texture是2D的,那麼target就不能再使用cubemap了,意思就是已經創建好的texture object不能修改類型,如果修改了的話,就會出現GL_INVALID_OPERATION的錯誤。

所以回憶一下,我們通過glGenTextures創建一些texture object name。然後通過glBindTexture,給texture object name創建和關聯一個texture object,同時,通過這個API,還將這個texture object放入了當前被使用的紋理單元中,由於這個紋理單元正在被GPU使用,所以這個texture也就成了GPU所使用的2D texture或者cubemap texture,雖然GPU中可以存放大量的紋理單元,每個紋理單元又都包含着紋理,而且在紋理單元這些容器外面還有很多遊離的紋理,但是同一時間一個thread的一個context中只能有一個紋理單元被使用,以及只能有一個2D texture和一個cubemap texture是被使用着的。之後關於texture的操作,比如查詢texture的狀態等,對texture進行賦值,我們就會直接操作2D texture或者cubemap texture,而不會在使用texture object name了。所以,如果想使用某個texture object,我們就需要先通過glActiveTexture active一個紋理單元,然後通過glBindTexture將texture放入該紋理單元,將該texture設置爲GPU當前的2D texture或者cubemap texture,然後才能對該texture object進行操作。初始狀態下每個紋理單元的2D texture或者cubemap texture都是與texture object name爲0的texture object綁定的,然而其實texture object name爲0的texture object是一個被預留的texture object。

一個texture object可以放在多個紋理單元中,而任何對該texture object的操作,都會影響到所有與其關聯的紋理單元。比如刪除一個2D的texture object,而這個texture object原本可能被放入0、1、4這3個紋理單元中。那麼就相當於把這個紋理從這三個紋理單元中都取出來,也就相當於先把這三個紋理單元分別進行了active,然後進行了glBindTexture(GL_TEXTURE_2D,0)的操作。再比如對這個2D texture object進行修改,那麼在使用這三個紋理單元中的GL_TEXTURE_2D的時候,使用到的都是修改之後的值。

texture object name和對應的texture object和buffer object、shader以及program一樣,都屬於一個namespace,也就是可以被多個share context進行共享。

void glActiveTexture(GLenum texture);

剛纔我們已經說了,GPU中同一時間一個thread的一個context中只能有一個紋理單元是處於被使用狀態,所以想要將所有紋理單元中都放入texture,就需要不停的切換active texture。那麼glActiveTexture這個API,就是用於將某個指定的紋理單元設置爲被使用狀態。

這個函數的輸入參數的意思是指定某個紋理單元被使用。GPU中最多可以有8個紋理單元,但是每個GPU又都不同,所以可以通過glGet這個API,傳入GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS這個參數來獲取到當前GPU最多可以有多少個紋理,GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS不能超過8。所以第一個輸入參數只能是GL_TEXTUREi,其中i從0一直到GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS -1。初始狀態下,GL_TEXTURE0是被active的。如果這裏傳入的值並非GL_TEXTUREi,i從0到GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS -1。那麼就會出現GL_INVALID_ENUM的錯誤。

這個函數沒有輸出參數。

OpenGL ES支持在一次繪製中使用多個紋理,這樣可以增強畫面表現力,也可以在PS中使用特定算法。這個也可以想象到,因爲紋理在shader中只是一個普通的sample變量,或者說是隻是一個普通的uniform變量,那麼傳入多個uniform是沒有問題的,只要不超過uniform的限制即可。使用多重紋理的方式,也就是先通過glActiveTexture依次將紋理單元active,然後給各個紋理單元傳入將要被用到的紋理對象,然後再把要用到的紋理單元,通過glUniform的API傳入shader即可,然後在shader中就可以使用傳入的若干個紋理了。但是需要注意的是,雖然一個紋理單元可以包含多個紋理,其中每個紋理格式對應一個紋理,但是在shader中,一個紋理單元只能使用其中的一個紋理。比如通過glUniform將紋理單元傳給sample2D,那麼在shader中只能使用這個紋理單元中的2D texture,而如果通過glUniform將紋理單元傳給sampleCube,那麼在shader中只能使用這個紋理單元中的Cubemap texture。

void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid * data);

創建了texture object之後,就需要給這個texture object賦予數據了,而glTexImage2D這個API就是通過OpenGL ES,把我們剛纔準備好的數據,從CPU端保存的數據傳遞給GPU端,保存在指定的texture object中。

這個函數的第一個輸入參數的意思是指定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賦值。剛纔我們已經說過了,在bindTexture之後,對texture object的操作,需要先active對應的紋理單元,然後再指定texture target來確定是哪個紋理,而不再通過buffer object name了(除非是對buffer object進行刪除),由於GPU中同一時間一個thread的一個context中只能有一個紋理單元是處於被使用狀態,而一個紋理單元最多只能有一個2D texture和一個cubemap的texture,所以在active了對應的紋理單元之後,在這裏通過target指定我們是操作2D texture還是cubemap texture的哪一面,就能精確的指定到我們實際操作的是哪個texture object。如果傳入其他的參數,就會報INVALID_ENUM的錯誤。第二個是指給該texture的第幾層賦值。上個課時我們也簡單的介紹過,沒有mipmap的texture,就相當於只有一層mipmap,而有mipmap的texture就好比一層一層塔一樣,每一層都需要賦值。所以在這裏需要確認我們是給紋理的第幾層賦值,絕大多數情況是給第一層賦值,因爲即使紋理需要mipmap,我們也經常會使用glGenerateMipmap這個API去生成mipmap信息,而不直接賦值。glGenerateMipmap這個API一會我們再說。mipmap又稱LOD,level 0就是第一層mipmap,也就是圖像的基本層。如果level小於0,則會出現GL_INVALID_VALUE的錯誤。而且level也不能太大,因爲texture是由最大尺寸限制的,而第一層mipmap就是紋理的原始尺寸,而第二層mipmap的尺寸爲原始寬高各除以2,依次類推,最後一層mipmap的尺寸爲寬高均爲1。所以如果level超過了log2(max),則會出現GL_INVALID_VALUE的錯誤。這裏的max,當target爲GL_TEXTURE_2D的時候,指的是GL_MAX_TEXTURE_SIZE,而當target爲其他情況的時候,指的是GL_MAX_CUBE_MAP_TEXTURE_SIZE。這裏的GL_MAX_TEXTURE_SIZE和GL_MAX_CUBE_MAP_TEXTURE_SIZE,都可以通過glGet這個API獲取到。而且還有,假如圖像的寬或者高不是2的冪,那麼有個專業術語叫做NPOT,non power of two。在OpenGL ES2.0中NPOT的texture是不支持mipmap的,所以針對NPOT的texture,如果level大於0,也就會出現GL_INVALID_VALUE的錯誤。第三個參數internalformat,第七個參數format和第八個參數type我們放在一起來說,就是指定原始數據在從CPU傳入GPU之前,在CPU中的格式信息,以及傳入GPU之後,在GPU中的格式。internalformat,是用於指定紋理在GPU端的格式,只能是GL_ALPHA, GL_LUMINANCE, GL_LUMINANCE_ALPHA, GL_RGB, GL_RGBA。GL_ALPHA指的是每個像素點只有alpha通道,相當於RGB通道全爲0。GL_LUMINANCE指的是每個像素點只有一個luminance值,相當於RGB的值全爲luminance的值,alpha爲1。GL_LUMINANCE_ALPHA指的是每個像素點有一個luminance值和一個alpha值,相當於RGB的值全爲luminance的值,alpha值保持不變。GL_RGB指的是每個像素點有一個red、一個green值和一個blue值,相當於RGB的值保持不變,alpha爲1。GL_RGBA指的是每個像素點有一個red、一個green值、一個blue值和一個alpha值,相當於RGBA的值都保持不變。如果internalformat是其他值,則會出現GL_INVALID_VALUE的錯誤。第七個參數format和第八個參數type,用於指定將會生成的紋理在所需要的信息在CPU中的存儲格式,其中format指定通道信息,只能是GL_ALPHA, GL_RGB, GL_RGBA, GL_LUMINANCE, and GL_LUMINANCE_ALPHA。type指的每個通道的位數以及按照什麼方式保存,到時候讀取數據的時候是以byte還是以short來進行讀取。只能是GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT_5_6_5, GL_UNSIGNED_SHORT_4_4_4_4, and GL_UNSIGNED_SHORT_5_5_5_1。當type爲GL_UNSIGNED_BYTE的時候,每一個byte都保存的是一個顏色通道中的值,當type爲GL_UNSIGNED_SHORT_5_6_5, GL_UNSIGNED_SHORT_4_4_4_4, and GL_UNSIGNED_SHORT_5_5_5_1的時候,每個short值中將包含了一個像素點的所有顏色信息,也就是包含了所有的顏色通道的值。從CPU往GPU傳輸數據生成紋理的時候,會將這些格式的信息轉成float值,方法是比如byte,那麼就把值除以255,比如GL_UNSIGNED_SHORT_5_6_5,就把red和blue值除以31,green值除以63,然後再全部clamp到閉區間[0,1],設計這種type使得綠色更加精確,是因爲人類的視覺系統對綠色更敏感。而type爲GL_UNSIGNED_SHORT_5_5_5_1使得只有1位存儲透明信息,使得每個像素要麼透明要麼不透明,這種格式比較適合字體,這樣可以使得顏色通道有更高的精度。如果format和type不是這些值,那麼就會出現GL_INVALID_ENUM的錯誤。

同樣的format在OpenGL ES2.0中,將對應相同的internalformat,比如format GL_RGBA就對應着internalformat GL_RGBA,format GL_ALPHA就對應着internalformat GL_ALPHA,這裏一共有5種format,也對應着5種internalformat,分別是GL_RGBA,GL_RGB,GL_ALPHA,GL_LUMINANCE,GL_LUMINANCE_ALPHA。internalformat和format需要一一對應,而且確定了internalformat和format之後,type的選擇也受到了限制,比如針對internalformat和format爲GL_RGB的時候,type只能是GL_UNSIGNED_SHORT_5_6_5或者GL_UNSIGNED_BYTE。而internalformat和format爲GL_ALPHA的時候,type只能是GL_UNSIGNED_BYTE。internal format、format和type必須要對應着使用。

第四個參數width和第五個參數height就是原始圖片的寬和高,也是新生成紋理的寬和高。因爲兩者是一樣的,圖片信息以數據的形式從CPU傳到GPU,可能每個像素點格式和包含的信息會發生變化,但是圖片的大小,也就是像素點的數量,每行多少個像素點,一共多少行,這個信息是不會發生變化的,這裏我們說的像素點其實在紋理的相關知識中還有一個專業術語叫做紋理像素texels,簡稱紋素。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中一塊指向保存實際數據的內存。如果data不爲null,那麼將會有width*height個像素的data從CPU端的data location開始讀取,然後會被從CPU端傳輸並且更新格式保存到GPU端的texture object中。當然,從CPU讀取數據的時候要遵守剛纔glPixelStorei設置的對齊規則。其中第一個數據對應的是紋理中左下角那個頂點。然後第二個數據對應的是紋理最下面一行左邊第二個點,依次類推,按照從左到右的順序,然後一行完畢,從下往上再賦值下一行的順序,一直到最後一個數據對應紋理中右上角那個頂點。如果data爲null,那麼執行完這個API之後,依然會給texture object分配可以保存width*height那麼多像素信息的內存,但是沒有對這塊內存進行初始化,如果使用這個texture去繪製到圖片上,那麼繪製出來的顏色值爲undefine。可以通過glTexSubImage2D給這塊沒有初始化的內存賦值。

這個函數沒有輸出參數,但是有以下幾種情況會出錯,除了剛纔說的那些參數輸入錯誤之外,還有如果target是CubeMap texture的一個面,但是width和height不相同,則會出現GL_INVALID_VALUE的錯誤。如果format與internalformat不匹配,或者type與format不匹配(比如type爲GL_UNSIGNED_SHORT_5_6_5 但是format 不是GL_RGB,或者type 是 GL_UNSIGNED_SHORT_4_4_4_4 或者 GL_UNSIGNED_SHORT_5_5_5_1 而 format 不是 GL_RGBA),則會出現GL_INVALID_OPERATION的錯誤。

總結一下,這個命令的輸入爲CPU內存中以某種方式保存的像素數據,轉變成閉區間[0,1]的浮點型RGBA像素值,保存在GPU中的texture object內。

一旦該命令被執行,會立即將圖像像素數據從CPU傳輸到GPU的內存中,後續對客戶端數據的修改不會影響服務器中的texture object相關信息。所以在這個API執行之後,客戶端中的圖像數據就可以被刪掉了。

如果一個texture object中已經包含有內容了,那麼依然可以使用glTexImage2D對這個texture object中的內容進行替換。

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

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

這個函數的第一個和第二個輸入參數和glTexImage2D的一樣,用於指定texture object的類型,以及該給texture的第幾層mipmap賦值。錯誤的情況也與glTexImage2D一樣,target傳入不支持的值,則會出現GL_INVALID_ENUM的錯誤,level傳入了錯誤的數字,則會出現GL_INVALID_VALUE的錯誤。 第三個、第四個、第五個和第六個輸入參數的意思是:以texture object的開始爲起點,寬度進行xoffset個位置的偏移,高度進行yoffset個位置的偏移,從這個位置開始,寬度爲width個單位高度爲height的這麼一塊空間,使用data指向的一塊CPU中的內存數據,這塊內存數據的format和type爲第七和第八個參數,將這塊內存數據根據這塊texture的internalformat進行轉換,轉換好了之後,對這塊空間進行覆蓋。OpenGL ES所支持的format和type剛纔已經列舉過了,如果使用其它值則爲GL_INVALID_ENUM。如果xoffset、yoffset、width、height其中有一個爲負,或者xoffset+width大於texture的寬,或者yoffset+height大於texture的高,那麼就會出現INVALID_VALUE的錯誤。

這個函數沒有輸出參數。除了剛纔那些因爲參數問題導致的錯誤,還有,如果target指定的這個texture還沒有被glTexImage2D或者glCopyTexImage2D分配好空間,或者texture的internal format要和傳入的format與type不對應,則會出現GL_INVALID_OPERATION的錯誤。具體的對應關係剛纔我們已經說了。glCopyTexImage2D是另外一種給texture賦值的方法,目的類似glTexImage2D,但是方式卻不同,我們一會再介紹這個API。

void glCopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border);

我們上節課說過,在OpenGL ES中執行繪製命令,可以在繪製buffer的顏色buffer等buffer生成數據。那麼我們想象一下,其實繪製buffer的顏色buffer,也就是一塊方形的內存,裏面保存了一塊方形圖像的顏色信息,由於繪製buffer是有格式的,這個我們在創建surface的時候就已經確定了繪製buffer中顏色buffer的格式,比如是存放RGBA中的哪些通道,每個通道各佔多少位。剛纔已經說了glTexImage2D是從CPU客戶端把數據讀取出來,需要讀取的是數據、format、type、width、height,傳輸到GPU,遵循CPU的格式和GPU端的格式進行轉換,在GPU中生成一個texture。而在繪製buffer中,我們也能獲取到用於生成紋理的這些信息,所以glCopyTexImage2D這個API,就是直接從繪製buffer中讀取一塊數據,根據繪製buffer的格式和目標紋理的格式進行轉換,在GPU中生成一個texture。它的用處也非常廣泛,比如我們遊戲中需要的圖像,很多是不能或者不需要從CPU讀取,比如我們在遊戲中進行了拍照,然後在遊戲中看我們拍的照片,那麼其實就是從前一幀的繪製buffer中讀取一些信息,保存在texture中,在後面的一幀,將這個texture繪製出來。

這個函數的前三個輸入參數和glTexImage2D的一樣,用於指定texture object的類型,該給texture的第幾層mipmap賦值,以及將要生成紋理的格式internal format。錯誤的情況也與glTexImage2D一樣,target傳入不支持的值,則會出現GL_INVALID_ENUM的錯誤,level傳入了錯誤的數字,則會出現GL_INVALID_VALUE的錯誤。internal format傳入了不支持的值,則會出現GL_INVALID_ENUM的錯誤。 第四個、第五個、第六個和第七個輸入參數的意思是:以繪製buffer左下角爲起點,寬度進行x個位置的偏移,高度進行y個位置的偏移,從這個位置開始,寬度爲width個單位,高度爲height的這麼一塊空間,從這塊空間中進行取值,取出來的值用於生成寬爲width、高爲height的一張紋理。width和height不能小於0,也不能當target爲GL_TEXTURE_2D的時候,超過GL_MAX_TEXTURE_SIZE,或者當target爲其他情況的時候,超過GL_MAX_CUBE_MAP_TEXTURE_SIZE否則,就會出現GL_INVALID_VALUE的錯誤。但是還有一種可能性,那就是這4個參數構成的方形,超過了繪製buffer的區域,那麼在區域外讀取到的值就是undefine。而如果width和height都爲0,那麼其實就是創建了一個NULL texture。最後一個參數border,代表着紋理是否有邊線,在這裏必須寫成0,也就是沒有邊線,如果寫成其他值,則會出現GL_INVALID_VALUE的錯誤。

這個函數沒有輸出參數,但是有以下幾種情況會出錯,除了剛纔說的那些參數輸入錯誤之外,還有如果target是CubeMap texture的一個面,但是width和height不相同,則會出現GL_INVALID_VALUE的錯誤。如果繪製buffer的format與internal format不匹配,則會出現GL_INVALID_OPERATION的錯誤,比如繪製buffer只有RGB通道,那麼internalformat就不能使用含A通道的格式,因爲根本沒有讀取到alpha信息。而當匹配的時候,會先將從繪製buffer中讀取出來的數據擴展成RGBA四個通道,然後進行歸一化,將數據clamp到閉區間[0,1],然後再轉成internalformat的格式。我們之前在講繪製命令的時候說過,繪製buffer可以是egl創建的,也可以是OpenGL ES自己創建的,其實用在這個地方,更多的是使用OpenGL ES自己創建的繪製buffer。那麼問題就是,如果使用OpenGL ES自己創建的繪製buffer去讀取數據生成texture,但是假如OpenGL ES創建的繪製buffer有誤,那麼就會出現GL_INVALID_FRAMEBUFFER_OPERATION的錯誤。

所以總結一下,glCopyTexImage2D和glTexImage2D總的來看,其實就是信息來源不同,其它基本一樣。

void glCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);

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

這個函數的第一個和第二個輸入參數和glTexSubImage2D的一樣,用於指定texture object的類型,以及該給texture的第幾層mipmap賦值。錯誤的情況也與glCopyTexImage2D一樣,target傳入不支持的值,則會出現GL_INVALID_ENUM的錯誤,level傳入了錯誤的數字,則會出現GL_INVALID_VALUE的錯誤。 後面6個輸入參數的意思是:以繪製buffer的左下角爲起點,寬度進行x個位置的偏移,高度進行y個位置的偏移,從這個位置開始,寬度爲width個單位高度爲height的這麼一塊空間,使用這塊空間的數據,去改變指定texture object的左下角爲起點,寬度進行xoffset個位置的偏移,高度進行yoffset個位置的偏移,從這個位置開始,寬度爲width個單位高度爲height的這麼一塊區域的信息,它們公用了width和height這兩個參數,原因也很簡單,就是像素點是一一對應的,所以從繪製buffer中取多大區域的信息,就更改了對應texture多大區域的數據。如果width和height爲0也沒關係,只是這樣的話這個命令就沒有任何結果了。如果xoffset、yoffset、width、height其中有一個爲負,或者xoffset+width大於texture的寬,或者yoffset+height大於texture的高,那麼就會出現INVALID_VALUE的錯誤。

這個函數沒有輸出參數。除了剛纔那些因爲參數問題導致的錯誤,還有,如果target指定的這個texture還沒有被glTexImage2D或者glCopyTexImage2D分配好空間,或者如果繪製buffer的format與internal format不匹配,則會出現GL_INVALID_OPERATION的錯誤。如果使用OpenGL ES自己創建的繪製buffer去讀取數據生成texture,但是假如OpenGL ES創建的繪製buffer有誤,那麼就會出現GL_INVALID_FRAMEBUFFER_OPERATION的錯誤。

void glTexParameter*(GLenum target, GLenum pname, GLint param);

通過上面的API,我們已經生成了一張可以使用的紋理,並且,我們還知道了這張紋理的寬和高。下面我們要把這張紋理映射到繪製buffer上,那麼映射的過程,需要用到一個新的概念,紋理座標。顧名思義,紋理座標,意思就是紋理的座標,用於在紋理中限定一塊區域,將這塊區域,顯示到繪製buffer的指定區域上。回憶一下OpenGL ES pipeline,通過之前的課程,我們創建了一套VS和PS,並傳入了頂點信息,在VS中講計算出各個頂點的位置,其實每個頂點還可以包含更多的值,比如頂點顏色、紋理座標、法線信息、切線信息等,這裏我們先說每個頂點對應着的紋理座標,紋理座標是用於指定該頂點對應紋理中的某個點。紋理座標,我們又稱之爲UV座標,以紋理的左下角爲座標原點,有兩種度量形式:一個頂點在紋理中的紋理座標通常用(u,v)表示,u和v的最大值分別是紋理的寬度和高度,通常由開發者來提供,通過attribute的形式將與頂點數量匹配的紋理座標傳入vertex shader,每個紋理座標對應一個頂點,意思就是要將該頂點與紋理中的指定點對應,然後所有頂點確定好之後,在光柵化的時候,會對uv座標進行歸一化生成st座標,取值爲閉區間[0,1]。在光珊化的時候,生成像素點的頂點座標,並使用插值smooth(或者非插值flat)的方法生成每個像素點的紋理座標(顏色、深度等),使得屏幕上的點可以於紋理圖片中對應。然後我們再將紋理以uniform sampler2D/samplerCube的形式傳入PS,在PS中通過紋理座標對紋理進行採樣,得到的紋理中對應的顏色,映射到光珊化產生的像素點上,對像素點進行着色,作爲一部分像素點的顏色信息,用這些顏色計算出當前像素點最終的顏色。當然也有的紋理是用於做爲像素點的深度或者模板信息,但是最多情況,還是做爲顏色信息。理解了紋理座標以及其工作原理之後,我們來舉個例子,比如,我們生成的這張紋理是一個頭像,那麼我們準備將這個頭像顯示在圖片的左上角,作爲主角的頭像,點擊這個頭像,還可以看到放大版的頭像。那麼假如原本的紋理寬和高爲64*64,而我們將頭像放到左上角的時候,寬和高爲32*32,然而點擊放大的時候,頭像的寬和高又變成了128*128。所以在這裏這兩種映射方式的紋理座標是一樣的,都是選定整張紋理去進行映射,但是對應的頂點位置以及由頂點位置確定的大小不一樣。所以究竟是如何把一張64*64的紋理圖片,映射成32*32和128*128的圖片呢,這裏就牽扯到了映射算法,而紋理映射有很多種算法,所以需要通過明確規定紋理屬性的方法,確定映射算法。下面,我們將介紹紋理屬性,同時,介紹紋理屬性對應的映射算法。而glTexParameter*這個API,就是用於設置紋理屬性的。

這個函數的第一個輸入參數用於指定texture object的類型,必須是GL_TEXTURE_2D或者GL_TEXTURE_CUBE_MAP。用於指定當前active的紋理單元中的一張texture,也就是用這個target來確定設置哪張紋理的屬性。所以想要修改一張紋理的屬性,先要通過glActiveTexture,enable一個紋理單元,然後通過glBindTexture,把這個texture綁定到這個紋理單元上。然後保持這個紋理單元處於active的狀態,再調用這個API,來修改指定紋理的屬性。

第二個輸入參數和第三個輸入參數,用於指定修改紋理的什麼屬性,以及修改成什麼值。其中,第二個參數有四種選擇。可以是GL_TEXTURE_MIN_FILTER, GL_TEXTURE_MAG_FILTER, GL_TEXTURE_WRAP_S, or GL_TEXTURE_WRAP_T。

下面分別介紹這4種屬性以及它們可以支持的數值:

第一個屬性,GL_TEXTURE_MIN_FILTER是用於紋理被使用的那塊區域的尺寸小於紋理尺寸,需要將紋理縮小的情況,針對這種情況,有六種映射算法。其中兩種算法是直接將原始紋理上的一個點或者四個點拿去計算,得出映射點的值,另外四種算法需要借用到mipmap。mipmap剛纔我們已經介紹過了,我們說過一個紋理,可以有多層mipmap,每層mipmap寬高以2倍速率遞減,直到寬高均爲1。一張紋理的mipmap可以通過glGenerateMipmap利用算法,根據紋理base level的值生成,也可以通過glTexImage2D、glCopyTexImage2D、以及glCompressedTexImage2D,給指定texture的指定level層賦值。這些API除了glCompressedTexImage2D,我們都在上面說過了,而glCompressedTexImage2D我們將在下節課講紋理優化的時候進行解釋說明。那麼下面詳細介紹一下GL_TEXTURE_MIN_FILTER對應的六種算法。

第一種是GL_NEAREST,就是直接取原始紋理限定區域中最接近的一個像素的信息,作爲映射點的信息,舉個例子,比如將一個5*5的紋理,整張映射到3*3的一張圖片上,那麼理論上,映射圖片的中間點應該就是從紋理的中間點取值。

第二種是GL_LINEAR,就是根據原始紋理限定區域中最接近的四個像素的信息,計算出來一個加權平均值,作爲映射點的信息,舉個例子,還是比如將一個5*5的紋理,整張映射到3*3的一張圖片上,那麼理論上,映射圖片左下角的點的值,應該就是根據紋理左下角四個點的值計算而來。

第三種是GL_NEAREST_MIPMAP_NEAREST,就是先選擇一張與映射圖片大小最接近的mipmap層,然後從這個mipmap層中,取最接近的一個像素的信息,作爲映射點的信息,舉個例子,比如將一個64*64的紋理,整張映射到4*4的一張圖片上,我們知道64*64的紋理有很多mipmap層,第0層的寬高就是64,第1層的寬高爲32,依此類推,第5層的寬高爲4,正好與映射圖片大小一致。那麼理論上,就取這第5層的像素點的信息,直接對應到映射圖片的各個點上即可。

第四種是GL_LINEAR_MIPMAP_NEAREST,就是先選擇一張與映射圖片大小最接近的mipmap層,然後從這個mipmap層中,取最接近的四個像素的信息,計算出來一個加權平均值,作爲映射點的信息,舉個例子,還是比如將一個64*64的紋理,整張映射到4*4的一張圖片上,我們還是會選擇第5層mipmap,然後理論上,映射圖片左下角的點的值,應該就是根據這第5層mipmap左下角四個點的值計算出來的加權平均值。

第五種是GL_NEAREST_MIPMAP_LINEAR,就是先選擇兩張與映射圖片大小最接近的mipmap層,然後從這兩個mipmap層中,分別取最接近的一個像素的信息,計算出來一個加權平均值,作爲映射點的信息,舉個例子,比如將一個64*64的紋理,整張映射到5*5的一張圖片上,我們就會取第4層和第5層mipmap,然後理論上,映射圖片左下角的點的值,應該就是根據第4層mipmap左下角點的值和第5層mipmap左下角點的值計算出來的加權平均值。

第六種是GL_LINEAR_MIPMAP_LINEAR,這種算法最複雜。就是先選擇兩張與映射圖片大小最接近的mipmap層,然後從這兩個mipmap層中,分別取四個最接近的像素的信息,分別計算加權平均值,然後根據這兩個加權平均值,再計算出來一個加權平均值,作爲映射點的信息,舉個例子,還是比如將一個64*64的紋理,整張映射到5*5的一張圖片上,我們還是會選擇第4層和第5層mipmap,然後理論上,映射圖片左下角的點的值,應該就是根據這第4層mipmap左下角四個點的值計算出來的加權平均值與第5層mipmap左下角四個點的值計算出來的加權平均值,算出來的加權平均值。

需要注意的是,如果在shader中註明要使用mipmap,比如texture2D傳入了第三個參數bias,那麼GL_TEXTURE_MIN_FILTER一定要使用帶mipmap的這後面四種映射算法。

在紋理的縮小函數中,基本都是將多個紋理中讀出的點計算出一個映射圖片上的點,所以走樣的機率會偏低,但是由於也會丟失一部分紋素,而丟失的紋素可能包含重要的顏色過渡信息,那樣就會導致貼圖失真,造成鋸齒,在遊戲中表現爲遠景部分,由於物體受到近大遠小的影響,對物體紋理進行了縮小處理,則會出現模糊。然而也可以想象到,這6種算法,肯定是效率和效果不能兼得,其中前兩種不同過mipmap的GL_NEAREST和GL_LINEAR最快,它們只需要通過一張紋理圖片上的點進行採樣即可,但是GL_NEAREST更容易導致比例嚴重的失真,高分辨率的圖像在低分辨率的設備上就會出現一些像素點跳躍比較大的情況,而GL_LINEAR在紋理縮小的時候,像素點過渡比較平滑,雖然會損失一些性能,但是效果會稍微好一點。而默認情況下GL_TEXTURE_MIN_FILTER對應的算法是GL_NEAREST_MIPMAP_LINEAR。

針對紋理的時候,紋素的丟失可能導致的圖片鋸齒問題,爲了消除鋸齒,有一門專門的圖形學技術叫做反鋸齒,英文叫做AA。在OpenGL ES中是通過多重採樣技術實現的反鋸齒。先說單重採樣,比如我們根據紋理映射得到了一張映射圖片,那麼將映射圖片傳入繪製buffer的時候,單重採樣會採用一一對應的方法,但是多重採樣則會利用了多重採樣緩衝區,由該點附近多個位置的顏色、depth、stencil的採樣共同決定繪製buffer上一個像素點的信息,這樣也就使得圖片的邊緣可以比較平滑的過渡,減小視覺上的瑕疵。生成每個像素使用鄰近採樣點的數量,數量越多,抗鋸齒效果越明顯,但相應的也會影響性能。這個最大數量受到硬件支持的限制,可以通過glGet函數,傳入參數GL_MAX_SAMPLES來查詢當前硬件支持的最大數量。多重採樣只是針對多邊形的邊緣進行抗鋸齒處理,所以對應用程序的性能影響比較小。在3D遊戲的開發中,這個技術已經比較普遍了,但是在2D遊戲中,由於大部分元素都是規則且垂直於攝像機的,所以鋸齒現象不是特別明顯,但是如果遊戲中需要繪製一些不規則的線段或者多邊形,則最好開啓多重採樣。

第二個紋理屬性,GL_TEXTURE_MAG_FILTER,與GL_TEXTURE_MIN_FILTER相反,它是用於紋理被使用的那塊區域尺寸大於紋理尺寸,需要將紋理放大的情況,針對這種情況,只有兩種映射算法。可想而知,需要將紋理放大,那麼mipmap這種比原始紋理還小的紋理就沒有意義了,所以可使用的映射算法就是GL_NEAREST和GL_LINEAR。這兩種算法和剛纔一樣,就不介紹具體如何映射的了。但是有一點需要注意,由於將紋理放大,那麼之前紋理上的一個點可能就要對應映射圖片上的多個點,這樣很有可能就會出現大塊的純色區域。特別是GL_NEAREST算法,雖然比GL_LINEAR快,但是由於GL_LINEAR還是會使用相鄰的四個點計算出來加權平均值,這樣的話,映射圖片上相鄰的點顏色就不會完全一樣,會有一個平滑的過渡,但是GL_NEAREST則直接使用一個個像素點,去生成一個個像素塊。所以GL_LINEAR效果會明顯比GL_NEAREST要好一些。GL_TEXTURE_MAG_FILTER對應的默認算法就是GL_LINEAR。

第三個和第四個紋理屬性放在一起來說,GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T。這個屬性與紋理座標息息相關,默認我們理解紋理的uv座標最大值就是寬高,st值就是[0,1]。但是,其實也有超過了[0,1]的情況,意思也就是想通過紋理座標去紋理上取值,但是取到了紋理外面了,那麼我們可以想象到,可以把紋理外面包一層,這一層的內容都是根據紋理的內容設置的,這個不是簡單的把紋理拉大,而是把紋理外面套一層,套的這一層的內容就是根據這兩個屬性指定的算法計算出來的。這兩個屬性支持的算法都是隻有三個,分別是GL_CLAMP_TO_EDGE, GL_REPEAT, 和 GL_MIRRORED_REPEAT。

將GL_TEXTURE_WRAP_S設置爲GL_CLAMP_TO_EDGE的意思就是,假如s座標超過範圍[0,1],那麼紋理外面套的那一層,橫向部分,以紋理的邊界值的顏色進行填充,假如紋理圖片的邊爲黑色,內部爲白色,那麼會橫向填充黑色。

將GL_TEXTURE_WRAP_S設置爲GL_REPEAT的意思就是,假如s座標超過範圍[0,1],那麼紋理外面套的那一層,橫向部分,則對紋理圖片進行復制,將橫向完全填滿。

將GL_TEXTURE_WRAP_S設置爲GL_MIRRORED_REPEAT的話,與設置爲GL_REPEAT類似,假如s座標超過範圍[0,1],那麼紋理外面套的那一層,橫向部分,則對紋理圖片進行鏡像複製,將橫向完全填滿。

GL_TEXTURE_WRAP_S都是進行橫向填充,GL_TEXTURE_WRAP_T則是進行縱向填充。GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T的默認算法都是GL_REPEAT。

這個API的三個參數的值上面已經全部列舉出來了,特別是第二個和第三個參數要搭配使用,如果用錯,則會出現GL_INVALID_ENUM的錯誤。

這個函數沒有輸出參數。如果GL_TEXTURE_MIN_FILTER設置的是需要使用mipmap的四個算法之一,但是紋理爲NPOT的紋理,又或者紋理的mipmap不是通過glGenerateMipmap,而是通過glTexImage2D、glCopyTexImage2D、以及glCompressedTexImage2D,生成的,但是沒有給需要的level賦值或者賦值的格式不對,那麼就相當於本次繪製是用了一張RGBA爲(0,0,0,1)的紋理。而如果紋理爲NPOT的話,但是GL_TEXTURE_WRAP_T、GL_TEXTURE_WRAP_S沒有使用GL_CLAMP_TO_EDGE,也就相當於用了一張RGBA爲(0,0,0,1)的紋理。

總結一下,剛纔我們一共說了紋理的三個屬性,第一個屬性,縮放,也就是假如紋理座標對應的紋理區域與映射區域大小不同,需要對紋理進行放大或者縮小的時候,設定相應的映射算法。設定好的算法,可以平衡好採樣的效率和效果,所以開發者在設置的時候需要根據自己的需要進行設置。

第二個屬性,wrapmode,假如紋理座標st使用到了超過[0,1]的座標,那麼針對橫向和縱向,對紋理的外層進行填充,設定相應的填充算法。

第三個屬性,mipmap,也就是給紋理對象設置一系列的小紋理,用於當映射區域小於紋理座標限定的紋理區域大小的時候,可以藉助小紋理中進行採樣。儘管紋理的filer屬性可以用於處理適當的紋理縮放,但遠遠不能滿足圖形應用程序的需求。由於紋理可能會經過遠大於2倍的縮放,那麼就會很容易造成失真,而由於移動設備的分辨率差異很大,所以不同設備使用同一個分辨率的資源,就會很容易出現縮放超過2倍的情況發生。

mipmap的功能分兩層,第一層就是會在進行縮小的時候,提高採樣效率,因爲從一張小紋理進行採樣肯定會比從一張大紋理中採樣要速度快,效果說不定還會比大紋理採樣的結果好,因爲生成mipmap的時候,我們可以通過glHins選擇GL_NICEST的方式,這樣小紋理不會特別的失真,(下面我們會再介紹glHints這個API)。然而如果使用大紋理再進行GL_NEAREST的話會更容易失真。當然有人可能說那生成mipmap是需要耗費資源的,其實大部分mipmap都是在離線生成好,然後在遊戲不忙的時候將資源傳入,生成紋理的,再不濟也是會選擇在遊戲不忙的時候使用glGeneratemipmap生成,雖然一定程度上影響了應用程序的性能,但是減少了對GPU內存和帶寬佔用,且只需要generate一次,儘量不會讓生成mipmap成爲遊戲性能的瓶頸。第二層功能,就是有時候顯示小圖片的時候其實是使用不一樣的內容,這樣的話就需要mipmap是通過glTexImage2D等API傳入的,而非glGeneratemipmap生成的,這樣的話開發者就可以控制texture的第i層mipmap的內容,比如可以讓mipmap第0層顯示人物的全身照片,而第1層顯示人物的大頭照。這樣,把紋理繪製到繪製buffer上的時候,根據紋理座標大小,導致同一張紋理繪製出來的結果不同。所以mipmap是非常有用的,它只有一點不好,那就是如果離線生成mipmap內容的話,會導致包體要稍微大一些,每張紋理大概會比原來多佔用1/3的空間。

void glGenerateMipmap(GLenum target);

這個API已經被提起很多遍了,我們再總結一下。首先,先介紹一下mipmap。Mipmap又稱多級紋理,每個紋理都可以有mipmap,也都可以沒有mipmap。這個概念我們在上面有接觸過,當時我們說了有mipmap的texture就好比一層一層塔一樣,每一層都需要賦值。紋理texture object就是GPU中一塊內存,這塊內存中保存着一定寬高的顏色信息以及其他屬性信息。而一個texture object中可以包含不止一塊內存,mipmap的texture object就包含着多級內存的。比如,我們創建的texture object的寬和高爲32*32,那麼我們知道,當紋理被準備好的時候,會擁有一塊可以存放32*32個像素點顏色信息的內存。如果我們通過命令使得texture object包含多級內存,第一級內存就是剛纔那塊保存了32*32個像素點顏色信息的內存,而第二級內存就是保存了16*16個像素點顏色信息的內存,依次類推,每降低以及,寬和高都將縮小一倍,一直到第六級內存就是保存了1*1個像素點顏色信息的內存。也就是說,寬高爲32*32的紋理,如果生成多級紋理,就會多出5塊內存,大小分別是16*16,8*8,4*4,2*2,1*1。當生成多級紋理之後,我們使用texture object name指定的texture object,就是這個包含了多級紋理的紋理。多級紋理的用處一會我們再說,我們先說多級紋理是如何生成的。我們在說glTexImage2D這個API的時候,說過紋理的內存是通過這個API生成的,當使用這個API的時候,第二個輸入參數就是制定給紋理的第幾層賦值,當時我們都是給第0層賦值,那麼現在我們就知道了,第0層爲紋理的base level,那麼默認都是給第0層賦值,但是我們也可以通過這個API給紋理的第1層mipmap,第2層mipmap賦值,一直到第N層mipmap。而這個在給第i層mipmap賦值的時候順便也就把需要的內存生成出來。我們也說過每個紋理的mipmap是有限制的,比如32*32的texture只能有6層mipmap,而64*64的texture有7層mipmap,依次類推。但是通過這個方式,一次只能給一層mipmap賦值,將多級紋理的所有層都賦上值,需要調用好多次命令。所以就有了glGenerateMipmap這個函數,這個函數就是將一個base level已經準備好的紋理,生成它的各級mipmap。這兩種方式唯一的區別在於,通過glTexImage2D賦值的時候,對應texture object對應的內存存放的值是由開發者指定的,開發者可以往裏面隨意存入數值,而通過glGenerateMipmap這個函數生成的多級紋理中存儲的值,第i層的值都是直接或者間接根據第0層的值計算出來的。生成算法一會再說。

這個函數的輸入參數用於指定texture object的類型,必須是GL_TEXTURE_2D或者GL_TEXTURE_CUBE_MAP,來指定當前active的紋理單元中的一張texture,也就是用這個target來確定生成哪張紋理的mipmap。所以想要對一張紋理生成mipmap,先要通過glActiveTexture,enable一個紋理單元,然後通過glBindTexture,把這個texture綁定到這個紋理單元上。然後保持這個紋理單元處於active的狀態,再調用這個API,來生成指定紋理的mipmap。如果輸入了錯誤的target,就會出現GL_INVALID_ENUM的錯誤。

這個函數沒有輸出參數。除了剛纔那些因爲參數問題導致的錯誤,還有,如果target是CubeMap texture,但是它的6個面的width、height、format、type並非完全相同,則會出現GL_INVALID_OPERATION的錯誤。而這種6個面的width、height、format、type並非完全相同的CubeMap texture,我們則稱它爲cube non-complete。還有上個課時也說過,在OpenGL ES2.0中NPOT的texture是不支持mipmap的,所以如果對NPOT的texture調用這個API生成mipmap,就會出現GL_INVALID_OPERATION的錯誤。最後還有,如果指定的texture的第0層,是壓縮格式的紋理內容,那麼,就會出現GL_INVALID_OPERATION的錯誤。

經過這個API,會根據第0層的數據產生一組mipmap數據,這組mipmap數據會替換掉texture之前的除了第0層之外的所有層的數據,第0層保持不變。所有層數據的internalformat都與第0層保持一致,每層的寬高都是上一層寬高除以2,一直到寬高均爲1的一層。然後除了第0層之外,每一層的數據都是通過上一層計算出來的,算法也比較簡單,一般都是根據四個像素點的值算出一個像素點的值即可,OpenGL ES並沒有規定使用什麼算法,不過OpenGL ES會通過glHint這個API,建議使用什麼算法,glHint這個API一會我們再說。

void glHint(GLenum target, GLenum mode);

我們知道,OpenGL ES的Spec規定了OpenGL ES API的功能,但是具體如何實現的,則是GPU driver的開發人員根據自己的想法實現的,比如剛纔我們說到的glGenerateMipmap的功能就是給指定texture生成mipmap數據,但是具體的生成算法,就是根據開發人員自己來定了。但是開發人員可以設計多種生成算法,然後再通過glHints來選擇一種作爲生成多級紋理的算法。

這個函數的第一個輸入參數用於指定一種GPU行爲,在這裏只能輸入GL_GENERATE_MIPMAP_HINT,否則的話,則會出現GL_INVALID_ENUM的錯誤。也就是說在OpenGL ES2.0中,只有生成多級紋理的算法,可以通過這個API來選擇。第二個輸入參數也就是針對剛纔指定的GPU的行爲,使用哪種方式去實現。這裏有三種選擇,默認是GL_DONT_CARE,也就是隨意選擇一種算法。另外兩種分別是GL_FASTEST,顧名思義,就是選擇一種最有效率的算法。還有GL_NICEST,就是選擇一種生成紋理最正確,質量最高的算法。如果輸入了其他值,則會出現GL_INVALID_ENUM的錯誤。雖然這裏做出了看似正確的選擇,但是哪個算法是最有效率的,而哪個算法是最正確、質量最高的,這種還是需要在GPU中寫算法的時候就指定出來,指定好哪個算法是最有效率的,哪個算法是最正確、質量最高的。當然,也可以不指定,然後在GPU driver中選擇忽略這個API。

這個函數沒有輸出參數。

void glDeleteTextures(GLsizei n, const GLuint * textures);

紋理一旦被傳輸至GPU,就會一直留在GPU管理的內存中。因此我們應該留意那些不再被使用的紋理,及時的從GL內存中刪除它們,以減少應用程序內存的佔用。所以當texture不再被需要的時候,則可以通過glDeleteTextures這個API把texture object name刪除。

這個函數的輸入參數的意思是該API會刪除n個texture object,當n小於0的時候,出現INVALID_VALUE的錯誤。textures保存的就是這些texture object的變量名。如果傳入的變量名爲0,或者對應的不是一個合法的texture object,那麼API就會被忽略掉。

這個函數沒有輸出參數。當texture object被刪除之後,其中的內容也會被刪掉,名字也會被釋放,可以被glGenTextures重新使用。如果被刪除的texture正在處於bind狀態,那麼就相當於先將該texture關聯的紋理單元active,然後執行了一次glBindTexture把對應的binding變成binging 0,也就相當於什麼都沒有bind了。

生成紋理的相關OpenGL ES API已經串講完畢,總結一下這些API的使用流程。

先通過glGenTextures給生成一個新的texture object name。然後選擇一個紋理單元,把這個texture object name生成紋理並綁定到這個紋理單元上。所以通過glActiveTexture把紋理單元enable,然後再通過glBindTexture把這個texture與GL_TEXTURE_2D綁定,我們知道這個texture object name是剛創建的,所以在執行這個命令的時候,我們知道,會先生成一個2D的texture object,然後把這個texture object放入剛纔enable的那個紋理單元中。下面再通過glTexParameter對紋理設置參數。之後會使用對應的API去生成紋理內容,比如通過glTexImage2D,傳入了target GL_TEXTURE_2D,傳入了第幾層mipmap,傳入了準備好的internalformat、寬、高、format、type和data信息。紋理圖片經歷瞭解包、歸一劃、轉換爲RGB格式、歸一劃的過程,存儲到了GPU中,在內存中,需要按照UNPACK_ALIGNMENT對齊。當CPU的紋理圖片unpack到了GPU,開發者可以把CPU端的紋理圖片刪除。如果數據來源自CPU的JPG、PNG圖片,這些格式圖片不包含mipmap信息,而如果來自於pvr、etc之類的用於生成壓縮紋理的原始圖片,纔可能會有mipmap信息。如果需要的話,再通過glHint設置生成mipmap的算法,然後通過glGenerateMipmap生成mipmap信息。這樣的話一張紋理就生成了。當紋理使用完畢後,就要通過glDeleteTextures這個API把原來對應的紋理刪除,並且把texture object name reset爲0。

截至到現在,OpenGL ES非常非常重要的一個模塊,紋理,我們已經將基礎部分講完了。首先,先介紹瞭如何從原始圖片中獲取到我們生成紋理所需要的信息,然後介紹瞭如何根據獲取的信息,通過一系列的OpenGL ES API生成紋理,並設置紋理的屬性,介紹了紋理映射到繪製buffer上的機制和原理。由於紋理是應用程序中,最影響包體大小,以及最佔用內存大小的,所以下節課,我們將講述如何對紋理進行優化。

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

謝謝大家,再見!

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