http://blog.csdn.net/wangdingqiaoit/article/details/51457675
前面兩節介紹了向量和矩陣,以及坐標和轉換相關的數學,再繼續討論模型變換等其他包含數學內容的部分之前,本節介紹二維紋理映射,為后面學習做一個準備。紋理映射本身也是比較大的主題,本節只限于討論二維紋理的基本使用,對于紋理映射的其他方法,后面會繼續學習??梢詮?a target="_blank" rel="nofollow">我的github下載本節代碼。
通過本節可以了解到
紋理映射的概念和原理
二維紋理映射的處理方法
使用紋理增加物體表面細節
要使渲染的物體更加逼真,一方面我們可以使用更多的三角形來建模,通過復雜的模型來逼近物體,但是這種方法會增加繪制流水線的負荷,而且很多情況下不是很方便的。使用紋理,將物體表面的細節映射到建模好的物體表面,這樣不僅能使渲染的模型表面細節更豐富,而且比較方便高效。紋理映射就是這樣一種方法,在程序中通過為物體指定紋理坐標,通過紋理坐標獲取紋理對象中的紋理,最終顯示在屏幕區域上,已達到更加逼真的效果。
紋素(texel)和紋理坐標
使用紋素這個術語,而不是像素來表示紋理對象中的顯示元素,主要是為了強調紋理對象的應用方式。紋理對象通常是通過紋理圖片讀取到的,這個數據保存到一個二維數組中,這個數組中的元素稱為紋素(texel),紋素包含顏色值和alpha值。紋理對象的大小的寬度和高度應該為2的整數冪,例如16, 32, 64, 128, 256。要想獲取紋理對象中的紋素,需要使用紋理坐標(texture coordinate)指定。
紋理坐標應該與紋理對象大小無關,這樣指定的紋理坐標當紋理對象大小變更時,依然能夠工作,比如從256x256大小的紋理,換到512x256時,紋理坐標依然能夠工作。因此紋理坐標使用規范化的值,大小范圍為[0,1],紋理坐標使用uv表示,如下圖所示(來自:Basic Texture Mapping):這里有錯誤,紋理坐標在左上角
u軸從左至右,v軸從底向上指向。右上角為(1,1),左下角為(0,0)。
通過指定紋理坐標,可以映射到紋素。例如一個256x256大小的二維紋理,坐標(0.5,1.0)對應的紋素即是(128,256)。(256x0.5 = 128, 256x1.0 = 256)。
紋理映射時只需要為物體的頂點指定紋理坐標即可,其余部分由片元著色器插值完成,如下圖所示(來自A textured cube):
模型變換和紋理坐標
所謂模型變換,就是對物體進行縮放、旋轉、平移等操作,后面會著重介紹。當對物體進行這些操作時,頂點對應的紋理坐標不會進行改變,通過插值后,物體的紋理也像緊跟著物體發生了變化一樣。如下圖所示為變換前物體的紋理坐標(來自:Basic Texture Mapping):
經過旋轉等變換后,物體和對應的紋理坐標如下圖所示,可以看出上面圖中紋理部分的房子也跟著發生了旋轉。(來自:Basic Texture Mapping):
注意有一些技術可以使紋理坐標有控制地發生改變,本節不深入討論,這里我們的紋理坐標在模型變換下保持不變。
創建紋理對象
創建紋理對象的過程同前面講述的創建VBO,VAO類似:
GLuint textureId;glGenTextures(1, &textureId);glBindTexture(GL_TEXTURE_2D, textureId);
1
2
3
這里我們綁定到GL_TEXTURE_2D目標,表示二維紋理。
WRAP參數
上面提到紋理坐標(0.5, 1.0)到紋素的映射,恰好為(128,256)。如果紋理坐標超出[0,0]到[1,1]的范圍該怎么處理呢? 這個就是wrap參數由來,它使用以下方式來處理:
GL_REPEAT:坐標的整數部分被忽略,重復紋理,這是OpenGL紋理默認的處理方式.
GL_MIRRORED_REPEAT: 紋理也會被重復,但是當紋理坐標的整數部分是奇數時會使用鏡像重復。
GL_CLAMP_TO_EDGE: 坐標會被截斷到[0,1]之間。結果是坐標值大的被截斷到紋理的邊緣部分,形成了一個拉伸的邊緣(stretched edge pattern)。
GL_CLAMP_TO_BORDER: 不在[0,1]范圍內的紋理坐標會使用用戶指定的邊緣顏色。
當紋理坐標超出[0,1]范圍后,使用不同的選項,輸出的效果如下圖所示(來自Textures objects and parameters):
在OpenGL中設置wrap參數方式如下:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
1
2
上面的幾個選項對應的都是整數,因此使用glTexParameteri來設置。
Filter參數
當使用紋理坐標映射到紋素數組時,正好得到對應紋素的中心位置的情況,很少出現。例如上面的(0.5,1.0)對應紋素(128,256)的情況是比較少的。如果紋理坐標映射到紋素位置(152.34,745.14)該怎么辦呢 ?
一種方式是對這個坐標進行取整,使用最佳逼近點來獲取紋素,這種方式即點采樣(point sampling),也就是最近鄰濾波( nearest neighbor filtering)。這種方式容易導致走樣誤差,明顯有像素塊的感覺。最近鄰濾波方法的示意圖如下所示(來自A Textured Cube):
圖中目標紋素位置,離紅色這個紋素最近,因此選擇紅色作為最終輸出紋素。
另外還存在其他濾波方法,例如線性濾波方法(linear filtering),它使用紋素位置(152.34,745.14)附近的一組紋素的加權平均值來確定最終的紋素值。例如使用 ( (152,745), (153,745), (152,744) and (153,744) )這四個紋素值的加權平均值。權系數通過與目標點(152.34,745.14)的距離遠近反映,距離(152.34,745.14)越近,權系數越大,即對最終的紋素值影響越大。線性濾波的示意圖如下圖所示(來自A Textured Cube):
圖中目標紋素位置周圍的4個紋素通過加權平均計算出最終輸出紋素。
還存在其他的濾波方式,如三線性濾波(Trilinear filtering)等,感興趣的可以參考texture filtering wiki。最近鄰濾波和線性濾波的對比效果如下圖所示(來自Textures objects and parameters):
可以看出最近鄰方法獲取的紋素看起來有明顯的像素塊,而線性濾波方法獲取的紋素看起來比較平滑。兩種方法各自有不同的應用場合,不能說線性濾波一定比最近鄰濾波方法好,例如要制造8位圖形效果(8 bit graphics,每個像素使用8位字節表示)需要使用最近鄰濾波。作為一個興趣了解,8位圖形效果看起來也是很酷的(可以查看Welcome 8-bit, Pixel-Art Images Gallery!)獲得更多8位圖形),例如下面這張使用Excel制作的8位圖(來自Excel is a great for making 8 bit graphics!):
另外一個問題是,紋理應用到物體上,最終要繪制在顯示設備上,這里存在一個紋素到像素的轉換問題。有三種情形(參考自An Introduction to Texture Filtering):
一個紋素最終對應屏幕上的多個像素 這稱之為放大(magnification)
一個紋素對應屏幕上的一個像素 這種情況不需要濾波方法
一個紋素對應少于一個像素,或者說多個紋素對應屏幕上的一個像素 這個稱之為縮小(minification)
放大和縮小的示意圖如下:
在OpenGL中通過使用下面的函數,為紋理的放大和縮小濾波設置相關的控制選項:
glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MAG_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MIN_FILTER, GL_NEAREST);
1
2
3
4
其中GL_LINEAR對應線性濾波,GL_NEAREST對應最近鄰濾波方式。
使用Mipmaps
考慮一個情景:當物體在場景中離觀察者很遠,最終只用一個屏幕像素來顯示時,這個像素該如何通過紋素確定呢?如果使用最近鄰濾波來獲取這個紋素,那么顯示效果并不理想。需要使用紋素的均值來反映物體在場景中離我們很遠這個效果,對于一個 256×256的紋理,計算平均值是一個耗時工作,不能實時計算,因此可以通過提前計算一組這樣的紋理用來滿足這種需求。這組提前計算的按比例縮小的紋理就是Mipmaps。Mipmaps紋理大小每級是前一等級的一半,按大小遞減順序排列為:
原始紋理 256×256
Mip 1 = 128×128
Mip 2 = 64×64
Mip 3 = 32×32
Mip 4 = 16×16
Mip 5 = 8×8
Mip 6 = 4×4
Mip 7 = 2×2
Mip 8 = 1×1
OpenGL會根據物體離觀察者的距離選擇使用合適大小的Mipmap紋理。Mipmap紋理示意圖如下所示(來自wiki Mipmap):
OpenGL中通過函數glGenerateMipmap(GL_TEXTURE_2D);來生成Mipmap,前提是已經指定了原始紋理。原始紋理必須自己通過讀取紋理圖片來加載,這個后面會介紹。
如果直接在不同等級的MipMap之間切換,會形成明顯的邊緣,因此對于Mipmap也可以同紋素一樣使用濾波方法在不同等級的Mipmap之間濾波。要在不同等級的MipMap之間濾波,需要將之前設置的GL_TEXTURE_MIN_FILTER選項更改為以下選項之一:
GL_NEAREST_MIPMAP_NEAREST: 使用最接近像素大小的Mipmap,紋理內部使用最近鄰濾波。
GL_LINEAR_MIPMAP_NEAREST: 使用最接近像素大小的Mipmap,紋理內部使用線性濾波。
GL_NEAREST_MIPMAP_LINEAR: 在兩個最接近像素大小的Mipmap中做線性插值,紋理內部使用最近鄰濾波。
GL_LINEAR_MIPMAP_LINEAR: 在兩個最接近像素大小的Mipmap中做線性插值,紋理內部使用線性濾波。
Mipmap使用注意使用使用glGenerateMipmap(GL_TEXTURE_2D)產生Mipmap的前提是你已經加載了原始的紋理對象。使用MipMap時設置GL_TEXTURE_MIN_FILTER選項才能起作用,設置GL_TEXTURE_MAG_FILTER的Mipmap選項將會導致無效操作,OpenGL錯誤碼為GL_INVALID_ENUM。
設置Mipmap選項如下代碼所示:
glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
1
2
加載原始紋理
從圖片加載紋理這部分工作不是OpenGL函數完成的,可以通過外部庫實現。這里我們使用SOIL(Simple OpenGL Image Library)庫完成。下載完這個庫后,你需要編譯到本地平臺對應版本。你可以從我的github處下載已經編譯好的32位庫。
使用SOIL加載紋理的代碼如下:
GLubyte *imageData =NULL;intpicWidth, picHeight;imageData = SOIL_load_image("wood.png",? ? &picWidth, &picHeight,0, SOIL_LOAD_RGB);// 讀取圖片數據glTexImage2D(GL_TEXTURE_2D,0, GL_RGB,? ? picWidth,picHeight,0, GL_RGB,? ? GL_UNSIGNED_BYTE, imageData);// 定義紋理圖像
1
2
3
4
5
6
7
其中glTexImage2D函數定義紋理圖像的格式,寬度和高度等信息,具體參數如下:
APIvoidglTexImage2D( GLenum target,
GLint level,
GLint internalFormat,
GLsizei width,
GLsizei height,
GLint border,
GLenum format,
GLenum type,
const GLvoid * data);
1.target參數指定設置的紋理目標,必須是GL_TEXTURE_2D, GL_PROXY_TEXTURE_2D等參數。
2.level指定紋理等級,0代表原始紋理,其余等級對應Mipmap紋理等級。
3.internalFormat指定OpenGL存儲紋理的格式,我們讀取的圖片格式包含RGB顏色,因此這里也是用RGB顏色。
4.width和height參數指定存儲的紋理大小,我們之前利用SOIL讀取圖片時已經獲取了圖片大小,這里直接使用即可。
5. border 參數為歷史遺留參數,只能設置為0.
6. 最后三個參數指定原始圖片數據的格式(format)和數據類型(type,為GL_UNSIGNED_BYTE, GL_BYTE等值),以及數據的內存地址(data指針)。
使用紋理的完整過程
Step1首先要指定紋理坐標,這個坐標和頂點位置、頂點顏色一樣處理,使用索引繪制,代碼如下所示:
// 指定頂點屬性數據 頂點位置 顏色 紋理GLfloat vertices[] = {? ? ? ? -0.5f, -0.5f,0.0f,1.0f,0.0f,0.0f,0.0f,0.0f,// 00.5f,? -0.5f,0.0f,0.0f,1.0f,0.0f,1.0f,0.0f,// 10.5f,0.5f,0.0f,0.0f,0.0f,1.0f,1.0f,1.0f,// 2-0.5f,0.5f,0.0f,1.0f,1.0f,0.0f,0.0f,1.0f// 3};? ? GLushort indices[] = {0,1,2,// 第一個三角形0,2,3// 第二個三角形};
1
2
3
4
5
6
7
8
9
10
11
同頂點位置和顏色一樣,需要指定紋理坐標的解析方式。上面的數據格式如下圖所示(來自www.learnopengl.com):
這個格式的說明在OpenGL學習腳印: 繪制一個三角形已經講過,如果不清楚,可以回過頭去查看。通過查看上圖,我們按照如下方式設置glVertexAttribPointer,讓OpenGL知道如何解析上述數據:
// 頂點位置屬性glVertexAttribPointer(0,3, GL_FLOAT, GL_FALSE,8*sizeof(GL_FLOAT), (GLvoid*)0);glEnableVertexAttribArray(0);// 頂點顏色屬性glVertexAttribPointer(1,3, GL_FLOAT, GL_FALSE,8*sizeof(GL_FLOAT), (GLvoid*)(3 *sizeof(GL_FLOAT)));glEnableVertexAttribArray(1);// 頂點紋理坐標glVertexAttribPointer(2,2, GL_FLOAT, GL_FALSE,8*sizeof(GL_FLOAT), (GLvoid*)(6 *sizeof(GL_FLOAT)));glEnableVertexAttribArray(2);
1
2
3
4
5
6
7
8
9
10
11
12
對應的頂點著色器如下:
#version 330layout(location =0)invec3position;layout(location =1)invec3color;layout(location =2)invec2textCoord;// 紋理坐標outvec3VertColor;outvec2TextCoord;voidmain(){gl_Position=vec4(position,1.0);? ? VertColor = color;? ? TextCoord = textCoord;}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Step2:然后需要設置OpenGL紋理參數;最后通過讀取紋理圖片,定義紋理圖像格式等信息。紋理數據最終傳遞到了顯卡中存儲。
// Section3 準備紋理對象// Step1 創建并綁定紋理對象GLuint textureId;? ? glGenTextures(1, &textureId);? ? glBindTexture(GL_TEXTURE_2D, textureId);// Step2 設定wrap參數glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);? ? glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);// Step3 設定filter參數glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);? ? glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,? ? ? ? GL_LINEAR_MIPMAP_LINEAR);// 為MipMap設定filter方法// Step4 加載紋理GLubyte *imageData =NULL;intpicWidth, picHeight;? ? imageData = SOIL_load_image("wood.png",? ? ? ? &picWidth, &picHeight,0, SOIL_LOAD_RGB);? ? glTexImage2D(GL_TEXTURE_2D,0, GL_RGB, picWidth, picHeight,0, GL_RGB, GL_UNSIGNED_BYTE, imageData);? ? glGenerateMipmap(GL_TEXTURE_2D);// Step5 釋放紋理圖片資源SOIL_free_image_data(imageData);? ? glBindTexture(GL_TEXTURE_2D,0);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
注意圖片資源在創建完紋理后就可以釋放了,使用SOIL_free_image_data完成。
Step3著色器中使用紋理對象
在頂點著色器中我們傳遞了紋理坐標,有了紋理坐標,獲取最終的紋素使用過在片元著色器中完成的。由于紋理對象通過使用uniform變量來像片元著色器傳遞,實際上這里傳遞的是對應紋理單元(texture unit)的索引號。紋理單元、紋理對象對應關系如下圖所示:
著色器通過紋理單元的索引號索引紋理單元,每個紋理單元可以綁定多個紋理到不同的目標(1D,2D)。OpenGL可以支持的紋理單元數目,一般至少有16個,依次為GL_TEXTURE0 到GL_TEXTURE15,紋理單元最大支持數目可以通過查詢GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS常量獲取。這些常量值是按照順序定義的,因此可以采用 GL_TEXTURE0 + i 的形式書寫常量,其中整數i在[0, GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS)范圍內。
作為一個了解,紋理對象不僅包含紋理數據,還包含采樣參數,這些采樣參數稱之為采樣狀態(sampling state)。而采樣對象(sampler object)就是只包含采樣參數的對象,將它綁定到紋理單元時,它會覆蓋紋理對象中的采樣狀態,從而重新配置采樣方式。這里不再繼續討論采樣對象的使用了。
要使用紋理必須在使用之前激活對應的紋理單元,默認狀態下0號紋理單元是激活的,因此即使沒有顯式地激活也能工作。激活并使用紋理的代碼如下:
// 使用0號紋理單元glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, textureId);glUniform1i(glGetUniformLocation(shader.programId,"tex"),0);
1
2
3
4
上述glUniform1i將0號紋理單元作為整數傳遞給片元著色器,片元著色器中使用uniform變量對應這個紋理采樣器,使用變量類型為:
uniformsampler2Dtex;
1
uniform變量與attribute變量uniform變量與頂點著色器中使用的屬性變量(attribute variables)不同,
屬性變量首先進入頂點著色器,如果要傳遞給片元著色器,需要在頂點著色器中定義輸出變量輸出到片元著色器。而uniform變量則類似于全局變量,在整個著色器程序中都可見。
完整的片元著色器代碼為:
#version 330invec3VertColor;invec2TextCoord;uniformsampler2Dtex;outvec4color;voidmain(){? ? color =texture(tex, TextCoord);}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
其中texture函數根據紋理坐標,獲取紋理對象中的紋素。
運行程序,效果如下圖所示:
這里為繪制的矩形添加了紋理,可以從我的github下載程序完整代碼。
重構代碼
將上面處理紋理部分的代碼整理成一個函數,放在textureHelper類里,可以從我的github查看這個類的代碼。使用textureHelper類加載紋理的代碼為:
GLint textureId = TextureHelper::load2DTexture("wood.png");
1
在上面的頂點著色器中,我們也傳遞了頂點顏色屬性,將頂點顏色和紋理混合,修改片元著色器中代碼為:
color =texture(tex, TextCoord) *vec4(VertColor,1.0f);
1
使用多個紋理單元
上面介紹了一個紋理單元支持多個紋理綁定到不同的目標,一個程序中也可以使用多個紋理單元加載多個2D紋理。使用多個紋理單元的代碼如下:
shader.use();// 使用0號紋理單元glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, textureId1);glUniform1i(glGetUniformLocation(shader.programId,"tex1"),0);// 使用1號紋理單元glActiveTexture(GL_TEXTURE1);glBindTexture(GL_TEXTURE_2D, textureId2);glUniform1i(glGetUniformLocation(shader.programId,"tex2"),1);
1
2
3
4
5
6
7
8
9
在著色器中,對兩個紋理的顏色進行混合:
#version 330invec3VertColor;invec2TextCoord;uniformsampler2Dtex1;uniformsampler2Dtex2;uniformfloatmixValue;outvec4color;voidmain(){vec4color1 =texture(tex1, TextCoord);vec4color2 =texture(tex2, TextCoord);? ? color =mix(color1, color2, mixValue);}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
其中mix函數完成顏色插值,函數原型為:
APIgenType mix( genType x,
genType y,
genType a);
最終值得計算方法為:x×(1?a)+y×a。
mixValue通過程序傳遞,可以通過鍵盤上的A和S鍵,調整紋理混合值,改變混合效果。
運行效果如下:
畫面中這只貓是倒立的,主要原因是加載圖片時,圖片的(0,0)位置一般在左上角,而OpenGL紋理坐標的(0,0)在左下角,這樣y軸順序相反。有的圖片加載庫提供了相應的選項用來翻轉y軸,SOIL沒有這個選項。我們可以修改頂點數據中的紋理坐標來達到目的,或者對于我們這里的簡單情況使用如下代碼實現y軸的翻轉:
vec4color2 =texture(tex2,vec2(TextCoord.s,1.0- TextCoord.t));
1
2
修改后的運行效果如下所示:
上述程序完整的代碼可以從我的github下載。
說明限于時間關系,文中的示例圖片部分來源于網絡,均注明了出處,向原作者表示感謝。
參考資料
Android Lesson Six: An Introduction to Texture Filtering
www.learnopengl.comTextures
Textures objects and parameters
推薦閱讀
關于Texture filteringShawn Hargreaves Blog-Texture filtering