本文主要解決一個(gè)問題:
在OpenGL中如何使用紋理兔毙?
一、什么是紋理骆撇?
紋理瞒御,英文是texture,中文可以翻譯成紋理神郊、紋理圖肴裙、紋理映射等等一堆東西。不過不管翻譯成啥涌乳,講的都是一個(gè)東西蜻懦。我們通常說的紋理,指的是一張二維的圖片夕晓,把它像貼紙一樣貼在什么東西上面宛乃,讓那個(gè)東西看起來像我們貼紙所要表現(xiàn)的東西那樣。
舉例來說,假如我們想繪制一面磚墻征炼,我們該怎么辦析既?根據(jù)我們已經(jīng)掌握的知識(shí)來看,我們需要用成千上萬的點(diǎn)來模擬它的顏色谆奥,我的天眼坏,這要搞到猴年馬月才能搞出來?顯然不現(xiàn)實(shí)酸些!于是聰明的程序員們想出了一個(gè)好方法宰译,就是用一張圖“貼”到物體的表面上,讓它看起來像是一面磚墻的樣子魄懂,省時(shí)省力省心沿侈。
用一句話來總結(jié),紋理就是一張貼到物體上的2維圖像市栗。
二缀拭、映射方式
既然是要把圖貼到物體上,自然就要想怎么貼才行肃廓。我們可以橫貼智厌、豎貼、斜貼怎么貼都行盲赊,但是怎樣貼才能達(dá)到我們想要的效果呢铣鹏?總要有個(gè)規(guī)章制度來規(guī)定一下怎么貼才行吧。這個(gè)貼法哀蘑,就稱為映射诚卸。
規(guī)則是:以左下角為原點(diǎn),向右伸展到1.0的位置绘迁,向上伸展到1.0的位置合溺,表示一整張的紋理圖像。
使用紋理圖的時(shí)候棠赛,我們需要在頂點(diǎn)數(shù)據(jù)中添加一個(gè)紋理坐標(biāo)的數(shù)據(jù),標(biāo)明我們是如何將紋理上的元素映射到頂點(diǎn)上的膛腐,這個(gè)我們在后面的實(shí)現(xiàn)環(huán)節(jié)再詳細(xì)說明睛约。
三、紋理環(huán)繞方式(Texture Wrapping)
通常哲身,紋理坐標(biāo)的范圍在(0,0)到(1,1)之間辩涝,但是如果我們制定的坐標(biāo)在這之外呢?OpenGL會(huì)如何做出反應(yīng)勘天?默認(rèn)情況下怔揩,OpenGL會(huì)重復(fù)繪制紋理圖捉邢,不過,OpenGL也提供了更多的選擇方案:
- GL_REPEAT: 默認(rèn)方案商膊,重復(fù)紋理圖片伏伐。
- GL_MIRRORED_REPEAT:類似于默認(rèn)方案,不過每次重復(fù)的時(shí)候進(jìn)行鏡像重復(fù)翘狱。
- GL_CLAMP_TP_EDGE:將坐標(biāo)限制在0到1之間秘案。超出的坐標(biāo)會(huì)重復(fù)繪制邊緣的像素,變成一種擴(kuò)展邊緣的圖案潦匈。(通常很難看)
- GL_CLAMP_TO_BORDER:超出的坐標(biāo)將會(huì)被繪制成用戶指定的邊界顏色。
每種方案的顯示效果截然不同赚导,不必?fù)?dān)心你會(huì)搞混了茬缩,看看效果就知道了。
怎么樣吼旧,只要視力在1000度之內(nèi)凰锡,都能看出明顯的區(qū)別吧。
設(shè)置紋理環(huán)繞方式的方法是調(diào)用glTexParameteri
函數(shù)圈暗,具體方式如下:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); //橫坐標(biāo)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); //縱坐標(biāo)
至于當(dāng)你設(shè)定了GL_CLAMP_TO_BORDER的環(huán)繞方式掂为,想要指定邊界顏色,就需要使用glTexParameterfv
函數(shù)了员串,像這樣:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f }; //指定成黃色
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
記得在設(shè)置了環(huán)繞方式之后用勇哗!
四、紋理過濾(Texture Filtering)
紋理坐標(biāo)采用了浮點(diǎn)數(shù)的形式寸齐,表明了它和分辨率無關(guān)欲诺。OpenGL需要非常精確的計(jì)算出紋理像素(通常被稱為紋素)和紋理坐標(biāo)之間的對應(yīng)關(guān)系。當(dāng)你有一張低分辨率的紋理圖渺鹦,但是需要用到一個(gè)非常大的物體上時(shí)扰法,這種操作的重要性就更加明顯了。聰明的你可能已經(jīng)猜到了毅厚,沒錯(cuò)塞颁,OpenGL提供了幾種不同的方案來解決這個(gè)問題,我們只討論最重要的兩種:GL_NEAREST和GL_LINEAR吸耿。
- GL_NEAREST
最近點(diǎn)過濾祠锣。指的是紋理坐標(biāo)最靠近哪個(gè)紋素,就用哪個(gè)紋素珍语。這是OpenGL默認(rèn)的過濾方式锤岸,速度最快,但是效果最差板乙。 - GL_LINEAR
(雙)線性過濾是偷。指的是紋理坐標(biāo)位置附近的幾個(gè)紋素值進(jìn)行某種插值計(jì)算之后的結(jié)果拳氢。這是應(yīng)用最廣泛的一種方式,效果一般蛋铆,速度較快馋评。
不過,你一定有個(gè)疑問刺啦,這些方法使用后留特,顯示上有啥區(qū)別?別急玛瘸,我們來看兩張圖就知道了蜕青。
什么,你問我那張美女圖呢糊渊?這個(gè)右核,不是我不想用,是那張圖實(shí)在是太不明顯了渺绒,怕你看不出來贺喝。我們還是來看這張圖,效果杠杠的宗兼。
很明顯躏鱼,最近點(diǎn)過濾的像素痕跡非常明顯,看著就跟“屎大棒”似的殷绍。而線性過濾的方式效果就好上很多了染苛,雖然感覺很模糊,但我們完全能理解一張小圖放大之后會(huì)模糊這件事篡帕。
設(shè)置過濾方式還是使用glTexParameteri
殖侵,像這樣:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); //縮小時(shí)的過濾方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //放大時(shí)的過濾方式
五、Mipmaps
多級(jí)漸進(jìn)紋理镰烧。雖然有這樣的翻譯拢军,但是這種翻譯太小眾了,還是決定使用英文來表示怔鳖。(筆者對自己的英文能力還是有信心的茉唉,不然也不會(huì)去看英文資料,嘿嘿~)
言歸正傳结执!想象這樣一個(gè)場景度陆,有非常多的同種物體,距離觀察者的遠(yuǎn)近各不相同献幔,我們可以對這種物體使用同一張紋理貼圖懂傀,但是問題來了,對于那些離觀察者比較遠(yuǎn)的物體蜡感,真的有必要用原始的紋理貼圖去映射嗎蹬蚁?答案當(dāng)然是否定的恃泪。太遠(yuǎn)的物體我們看不清楚,顯示的非常精細(xì)沒有意義犀斋,而且使用原始貼圖計(jì)算映射起來太麻煩贝乎,所以,我們使用一種mipmaps的方式來進(jìn)行處理叽粹。
所謂的mipmaps览效,就是一系列的紋理圖片,每一張紋理圖的大小都是前一張的1/4虫几,直到剩最后一個(gè)像素為止锤灿。看起來就像是這一個(gè)樣子:
當(dāng)物體越離越遠(yuǎn)的時(shí)候持钉,就可以選用較小紋理去映射衡招,這樣不僅效果好,而且速度也快每强。
你可能會(huì)有疑問,這個(gè)圖片是程序弄的還是美術(shù)弄的呢州刽?對于這個(gè)問題空执,我的回答是:美術(shù)弄不弄我不知道,但是程序肯定可以弄穗椅,而且弄起來還非常方便辨绊。我們只要調(diào)用一個(gè)函數(shù)就行了,那就是glGenerateMipmaps
匹表。
那我們開始用吧门坷!哦偶,還不行袍镀,還有一個(gè)問題沒解決默蚌。
疑問:
看上去很好,可是我物體的大小剛好在兩張mipmaps之間怎么辦呢苇羡?
對于剛好在兩張圖片之間的物體绸吸,我們可以參考前面兩種過濾方式,最近點(diǎn)(采用最接近的圖)或者線性(采用兩張圖的加權(quán)平均)设江。這樣锦茁,我們就有了四種不同的過濾方案。
- GL_NEAREST_MIPMAP_NEAREST:采用最近的mipmap圖叉存,在紋理采樣的時(shí)候使用最近點(diǎn)過濾采樣码俩。
- GL_LINEAR_MIPMAP_NEAREST:采用最近的mipmap圖,紋理采樣的時(shí)候使用線性過濾采樣歼捏。
- GL_NEAREST_MIPMAP_LINEAR:采用兩張mipmap圖的線性插值紋理圖稿存,紋理采樣的時(shí)候采用最近點(diǎn)過濾采樣笨篷。
- GL_LINEAR_MIPMAP_LINEAR:采用兩張mipmap圖的線性插值紋理圖,紋理采樣的時(shí)候采用線性過濾采樣挠铲。
好了冕屯,這次是真的可以用了。使用的函數(shù)還是一樣拂苹,glTexParameteri
安聘。像這樣:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); //都是線性過濾
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
這里要注意一點(diǎn),mipmaps是用來處理物體變小時(shí)候如何進(jìn)行貼圖的問題的,所以不需要設(shè)置GL_TEXTURE_MAG_FILTER成mipmap方式。如果強(qiáng)行使用了脓杉,會(huì)報(bào)錯(cuò)赂韵。(不信的話去試試!)
六谤草、使用
準(zhǔn)備
我們要做的第一件事就是將紋理圖加載到我們的應(yīng)用之中,但是,圖片的格式不計(jì)其數(shù)榴芳,難道我們要對每一種格式都寫一個(gè)加載的模塊嗎?這顯然不是我們想要的跺撼。最好能有一個(gè)庫來加載所有的圖片格式窟感,讓我們可以直接用,這樣我們就可以專注在映射上了歉井。
幸運(yùn)的是柿祈,確實(shí)有這樣一個(gè)庫我們可以直接用(鏈接)。打包下載之后哩至,將其中的stb_image.h文件包含到我們的項(xiàng)目中去躏嚎,我們要使用這個(gè)文件。
使用方式
我們使用stbi_load
函數(shù)來加載圖片菩貌,并且將圖片格式信息保存起來卢佣。像這樣:
int width, height, nrChannels;
unsigned char *data = stbi_load("beauty.jpg", &width, &height, &nrChannels, 0);
width,height和nrChannels變量中分別會(huì)保存圖片的寬度菜谣、高度和顏色通道數(shù)量的信息珠漂,這些都是在之后有用的數(shù)據(jù)。
頂點(diǎn)數(shù)據(jù)
到這里尾膊,我們的頂點(diǎn)已經(jīng)有許多相關(guān)聯(lián)的數(shù)據(jù)了媳危,比如顏色,比如紋理坐標(biāo)冈敛,是時(shí)候該給我們的頂點(diǎn)數(shù)組升升級(jí)了待笑。
我們的頂點(diǎn)數(shù)組中需要三樣?xùn)|西:位置、顏色抓谴、紋理坐標(biāo)暮蹂。每個(gè)頂點(diǎn)都需要這三組數(shù)據(jù)寞缝,在數(shù)組中依次排下去。于是仰泻,頂點(diǎn)數(shù)組就成了現(xiàn)在這個(gè)樣子:
float vertices[] = {
//位置 // 顏色 //紋理坐標(biāo)
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, //右上角
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, //右下角
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, //左下角
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f //左上角
};
可以看到荆陆,我們的跨度變成了32,也就是8sizeof(float)集侯,顏色的起始偏移值為3sizeof(float)被啼,紋理的起始偏移值為6*sizeof(float)。這樣棠枉,我們指定頂點(diǎn)屬性的時(shí)候自然需要指定相應(yīng)的位置和偏移浓体。
顏色屬性:
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
紋理屬性:
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
頂點(diǎn)著色器中,我們需要采用輸入的顏色和紋理坐標(biāo)進(jìn)行操作辈讶,所以命浴,在著色器中添加兩個(gè)輸入是非常好的選擇。
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;
然后贱除,我們的頂點(diǎn)著色器代碼就成了這個(gè)樣子:
//頂點(diǎn)著色器代碼
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;
out vec3 ourColor;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
TexCoord = aTexCoord;
}
代碼都是自解釋的生闲,非常容易理解,就不再浪費(fèi)口舌了月幌。相應(yīng)的跪腹,我們的片元著色器也就成了這個(gè)樣子:
//片元著色器代碼
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord); //對紋理指定位置進(jìn)行采樣
}
創(chuàng)建紋理
想OpenGL里的其他東西那樣,紋理也需要一個(gè)唯一ID飞醉,我們來創(chuàng)建一個(gè):
unsigned int texture;
glGenTextures(1, &texture);
glGenTextures
的第一個(gè)參數(shù)是要?jiǎng)?chuàng)建的紋理數(shù)量,后面的參數(shù)就是保存這么多數(shù)量的整型數(shù)數(shù)組屯阀。當(dāng)然缅帘,創(chuàng)建完了之后我們也需要綁定到OpenGL的環(huán)境里才能操作。
glBindTexture(GL_TEXTURE_2D, texture);
綁定完成后难衰,我們就可以把之前加載的圖片數(shù)據(jù)放到紋理中去了钦无。
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
又是一個(gè)擁有很多參數(shù)的龐大函數(shù)。我們一點(diǎn)點(diǎn)啃死它盖袭!
- 參數(shù)一:指定目標(biāo)紋理失暂。GL_TEXTURE_2D就表示當(dāng)前的操作會(huì)對綁定的2D紋理產(chǎn)生作用(GL_TEXTURE_1D和GL_TEXTURE_3D里的東西就不會(huì)受影響)
- 參數(shù)二:mipmap層級(jí)。我們之后會(huì)調(diào)用glGenerateMipmap來創(chuàng)建鳄虱,這里只需要?jiǎng)?chuàng)建原始圖就行了弟塞。(或者你也可以手動(dòng)的一次次調(diào)用這個(gè)函數(shù)來創(chuàng)建,(壞笑~))
- 參數(shù)三:我們需要保存的紋理格式拙已。我們的圖片只有RGB信息决记,所以用GL_RGB格式。
- 參數(shù)四和參數(shù)五:紋理圖片的寬高倍踪。之前保存的那個(gè)系宫。
- 參數(shù)六:一定要設(shè)置成0(有一些遺留的工作)
- 參數(shù)七和參數(shù)八:源圖片的格式和數(shù)據(jù)類型索昂。我們加載的圖片中有RGB值,并且以字節(jié)的方式保存扩借。所以我們傳遞了這兩個(gè)參數(shù)椒惨。
- 參數(shù)九:加載的圖片數(shù)據(jù)。
調(diào)用glTexImage2D
之后潮罪,當(dāng)前的紋理對象就和我們的紋理圖綁定起來了康谆。然后,我們再調(diào)用glGenerateMipmap
函數(shù)創(chuàng)建mipmaps错洁,非常流暢~
操作完成之后秉宿,最好要把圖片釋放掉,因?yàn)閿?shù)據(jù)已經(jīng)都保存到紋理對象中了屯碴,這樣干:
stbi_image_free(data);
整個(gè)代碼塊就是這個(gè)樣子:
//紋理
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
//設(shè)置紋理包裝和過濾的方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
int width, height, nrChannels;
//stbi_set_flip_vertically_on_load(true);
unsigned char* data = stbi_load("beauty.jpg", &width, &height, &nrChannels, 0);
if (data) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
std::cout << "無法加載問題描睦,請檢查代碼或資源是否有誤。" << std::endl;
stbi_image_free(data);
七导而、運(yùn)行
好忱叭,咱就運(yùn)行起來。
嗯今艺?不對啊韵丑,怎么是倒的?
這是因?yàn)镺penGL期待原點(diǎn)(0,0)位于左下角虚缎,而通常一張圖片的原點(diǎn)位于左上角撵彻。不過,這不是問題实牡,stb庫早就已經(jīng)為我們準(zhǔn)備了解決方案陌僵。在加載圖片之前調(diào)用stbi_set_flip_vertically_on_load(true);
哈哈,是不是注意到了我之前注釋掉的那行代碼创坞,沒錯(cuò)碗短,就是為了顯示出倒圖而故意注掉的。
好题涨,再次編譯運(yùn)行偎谁,這次的效果才對!
八纲堵、結(jié)語
呼~ 好長的一篇巡雨,不過總算是弄了點(diǎn)東西,辛苦沒有白費(fèi)~
參考資料:
www.learningopengl.com(非常好的網(wǎng)站婉支,建議也去學(xué)習(xí))