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





