
??在前面的學(xué)習(xí)過程中,我們已經(jīng)了解到可以在頂點(diǎn)數(shù)據(jù)中置入各頂點(diǎn)的顏色數(shù)據(jù),讓其每個頂點(diǎn)都呈現(xiàn)不同的顏色,但讓我們自己去指定頂點(diǎn)的顏色來還原現(xiàn)實(shí)場景終究是不現(xiàn)實(shí)的,因?yàn)檫@樣做我們需要足夠多的的頂點(diǎn),那么就要指定足夠多的的顏色,這顯然是一件繁雜且浪費(fèi)效能的一件事情。
??為了代替手動地指定顏色,紋理(Texture)應(yīng)運(yùn)而生。紋理是一張2D圖片,我們要做的就是把它無縫地貼合到3D的模型上去,這樣我們的3D模型就有了外表。為了還原真實(shí),我們可以給紋理圖添加很多細(xì)節(jié),而不是去給模型增加額外的頂點(diǎn)。
??以我們之前創(chuàng)建的三角形為例,要是我們想給三角形貼上一張磚墻的圖片:

??原圖如上,而效果圖如下:

??這個過程叫映射,就是把紋理圖映射到三角形上,要想實(shí)現(xiàn)這個映射就要指定三角形的坐標(biāo)在紋理圖的哪個位置,這就要用到兩種坐標(biāo),一個是三角形自己的頂點(diǎn)坐標(biāo),一個是紋理圖的坐標(biāo)(紋理坐標(biāo)(Texture Coordinate)),只要這兩個坐標(biāo)對上了,才知道三角形要的是紋理圖的哪個部分。
紋理坐標(biāo)
??紋理坐標(biāo)是指明在紋理圖某個位置的坐標(biāo),坐標(biāo)系(xy軸/uv軸/st軸)在圖片的左下角,xy的范圍都是0到1之間,例如磚墻的紋理坐標(biāo)圖就是:

??某個頂點(diǎn)坐標(biāo)要想獲取紋理圖某個位置的顏色就要使用位置對應(yīng)的紋理坐標(biāo),這個過程叫采樣(Sampling)。下圖展示了如何把紋理坐標(biāo)映射到三角形上的:

??
??我們指定了3個紋理坐標(biāo)點(diǎn),我們可以把這3個坐標(biāo)包含在頂點(diǎn)數(shù)據(jù)中傳遞給頂點(diǎn)著色器,接下來它們會被傳片段著色器中,它會為每個片段進(jìn)行紋理坐標(biāo)的插值。但在考慮實(shí)現(xiàn)之前,我們應(yīng)該先去考慮更加重要的東西,這涉及到一些原理上知識。
紋理環(huán)繞方式
??紋理坐標(biāo)的范圍設(shè)定在了0到1之間,如果把紋理坐標(biāo)設(shè)置在范圍之外,那會發(fā)生什么情況,于OpenGL而言,默認(rèn)的行為是把這個紋理圖重復(fù)往外擴(kuò)展,直至紋理坐標(biāo)處,那么無論把坐標(biāo)設(shè)置在哪,實(shí)際上這個點(diǎn)都落在這幅紋理圖內(nèi)。除了這個處理方式外,OpenGL還提供了更多的選擇:
| 環(huán)繞方式 | 描述 |
|---|---|
GL_REPEAT |
對紋理的默認(rèn)行為。重復(fù)紋理圖像。 |
GL_MIRRORED_REPEAT |
和GL_REPEAT一樣,但每次重復(fù)圖片是鏡像放置的。 |
GL_CLAMP_TO_EDGE |
紋理坐標(biāo)會被約束在0到1之間,超出的部分會重復(fù)紋理坐標(biāo)的邊緣,產(chǎn)生一種邊緣被拉伸的效果。 |
GL_CLAMP_TO_BORDER |
超出的坐標(biāo)為用戶指定的邊緣顏色。 |
??上述4種環(huán)繞方式的視角效果如下:

??在此我想在拓展一下,在Unity里關(guān)于紋理文件的紋理環(huán)繞方式也是有相似的選擇:

??其中Repeat就是
GL_REPEAT,Clamp就是GL_CLAMP_TO_EDGE,Mirror就是GL_MIRRORED_REPEAT,而對于GL_CLAMP_TO_BORDER我的Unity版本并沒有這個環(huán)繞方式。而在Unity中還能設(shè)置紋理坐標(biāo)的縮放倍數(shù):
??只要大于(1, 1)紋理圖就會根據(jù)選擇的環(huán)繞方式進(jìn)行范圍外的平鋪。
??現(xiàn)在我知道如何在Unity里選擇紋理環(huán)繞方式,那OpenGL呢?OpenGL提供了
glTexParameter*函數(shù)來設(shè)置紋理環(huán)繞方式,且還能單獨(dú)對一個坐標(biāo)軸設(shè)置,即兩個坐標(biāo)軸可以有不同的環(huán)繞方式。例如:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
glTexParameteri(GLenum target, GLenum pname, GLint param):第一個參數(shù)指定紋理目標(biāo),我們使用的是2D紋理,所以是GL_TEXTURE_2D,除此之外還有3D和1D紋理;第二個參數(shù)指定設(shè)置紋理的哪個屬性項(xiàng)和應(yīng)用于哪個軸,我們要設(shè)置的是紋理的環(huán)繞方式(WRAR)以及S軸和T軸,所以是GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T;第三個參數(shù)指定環(huán)繞方式,我們選擇了GL_MIRRORED_REPEAT鏡像重復(fù)方式。
??另外要注意的是,在使用這個函數(shù)之前,要先綁定好紋理對象。在后面我們會討論到如何創(chuàng)建和綁定一個紋理對象,而紋理對象其實(shí)是一個管理存儲了紋理圖所有信息的緩沖區(qū)(與VBO管理頂點(diǎn)數(shù)據(jù)緩沖區(qū)類似)的對象。在綁定紋理對象時,要先說明綁定什么紋理目標(biāo)給當(dāng)前紋理對象,然后接下來的設(shè)置紋理圖各種屬性項(xiàng)時依舊要指定紋理目標(biāo),在綁定對象時已經(jīng)指定好的紋理目標(biāo),而后為什么還要重復(fù)一遍呢?至此我的理解是,一個紋理對象其實(shí)是可以容納多種紋理目標(biāo)的,即GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D等等都可以有,且可以同時綁定,我使用了綁定函數(shù),使用了GL_TEXTURE_2D參數(shù),然后我再次調(diào)用綁定函數(shù),使用了GL_TEXTURE_3D參數(shù),不會發(fā)生沖突。而在后續(xù)的設(shè)置紋理對象屬性的時候指定紋理坐標(biāo),也是為了區(qū)分這種情況。
紋理過濾
??如果一張低分辨率(像素少)的紋理圖要貼在一個很大的平面上,紋理坐標(biāo)(任意浮點(diǎn)值)的數(shù)量還多過像素的數(shù)量,那么OpenGL該如何將紋理像素(Texture Pixel)映射到紋理坐標(biāo)上呢?OpenGL有紋理過濾(Texture Filtering)的功能來解決這個問題。紋理過濾有很多種,這了只討論最重要的兩種:GL_NEAREST和GL_LINEAR。
??Texture Pixel也叫Texel,你可以想象你打開一張.jpg格式圖片,不斷放大你會發(fā)現(xiàn)它是由無數(shù)像素點(diǎn)組成的,這個點(diǎn)就是紋理像素;注意不要和紋理坐標(biāo)搞混,紋理坐標(biāo)是你給模型頂點(diǎn)設(shè)置的那個數(shù)組,OpenGL以這個頂點(diǎn)的紋理坐標(biāo)數(shù)據(jù)去查找紋理圖像上的像素,然后進(jìn)行采樣提取紋理像素的顏色。
-
GL_NEAREST鄰近過濾,Nearest Neighbor Filtering。是OpenGL默認(rèn)的紋理過濾方式。當(dāng)采用這個過濾方式時,OpenGL會選擇中心點(diǎn)最接近紋理坐標(biāo)的像素作為該紋理坐標(biāo)的像素值。下圖中有4個像素點(diǎn),加號是紋理坐標(biāo)。左上角那個紋理像素的中心距離紋理坐標(biāo)最近,所以它會被選擇為樣本顏色:
-
GL_LINEAR線性過濾, Bilinear Filtering。 采用該過濾方式時,會基于紋理坐標(biāo)附近的紋理像素,計(jì)算出一個插值(取平均值),一個紋理像素的中心距離紋理坐標(biāo)越近,那么這個紋理像素的顏色對最終的樣本顏色的貢獻(xiàn)越大。這個值作為樣本顏色。
??我們可以看看兩種過濾方式作用于同一張低分辨率的圖會有什么不同的效果:
??可以看到鄰近過濾后的圖能看到顆粒狀的像素,有點(diǎn)鋸齒的感覺;而線性過濾后能看到比較平滑的圖。
??對于怎么設(shè)置紋理圖的過濾方式,其操作與設(shè)置環(huán)繞方式類似,使用相同的函數(shù),設(shè)置不同的參數(shù)而已:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
??除了縮小(Minify)的時候要進(jìn)行紋理過濾操作,放大(Magnify)也需要進(jìn)行同樣操作,那么就要通過TEXTURE_MIN_FILTER和GL_TEXTURE_MAG_FILTER參數(shù)為其設(shè)置各自的過濾方式。
多級漸遠(yuǎn)紋理
??觀察現(xiàn)象,提出問題?,F(xiàn)在我有一個平面,平鋪了一張紋理圖上去,如圖所示:

??能看出它是通過Repeat的方式鋪滿整個平面的?,F(xiàn)在我把鏡頭拉到無限遠(yuǎn)處:

??原本鋪滿整個屏幕的平面現(xiàn)在只是一個小方格。那么OpenGL該如何為紋理坐標(biāo)采樣呢?
??一個游戲場景有著很多的物體,每個物體上都有紋理,且物體有遠(yuǎn)有近。遠(yuǎn)處物體的紋理與近處物體的紋理一樣有著高分辨率(高像素),但遠(yuǎn)處的物體只會產(chǎn)生很少的片段(如上圖),OpenGL從高分辨率紋理中為這些片段獲取正確的顏色值就很困難,因?yàn)樗枰獙σ粋€跨過紋理很大部分的片段只拾取一個紋理顏色。在小物體上這會產(chǎn)生不真實(shí)的感覺,更不用說對它們使用高分辨率紋理浪費(fèi)內(nèi)存的問題了。
??為此,OpenGL使用多級漸遠(yuǎn)紋理(Mipmap)來解決這個問題。即作出相同圖案但不同大小的一系列圖像,后一個紋理圖是前一個的二分之一。

??在觀察著超過物體一定距離時,就會使用切換物體當(dāng)前的紋理圖像,換成適合當(dāng)前距離的那個。由于距離遠(yuǎn),解析度不高也不會被用戶注意到。同時,多級漸遠(yuǎn)紋理另一加分之處是它的性能非常好。
??另外OpenGL非常貼心地幫我們準(zhǔn)備好了代替手動創(chuàng)建多級紋理圖的方法,
glGenerateMipmaps函數(shù)能夠在我們導(dǎo)入了紋理圖后自動為其生成一系列的多級紋理圖。就是說我們只需導(dǎo)入上面磚墻的第一張,后面的幾張它會幫我們生成。??在切換多級紋理圖時,OpenGL在兩個不同級別的多級漸遠(yuǎn)紋理層之間會產(chǎn)生不真實(shí)的生硬邊界。即切換的過程很突兀,由其是觀察者在距離閾值邊界來回走動時,這種突兀就更明顯。不過我們可以像紋理過濾一樣,在切換時做鄰近過濾或線性過濾的處理,這樣切換就會顯得平滑流暢。OpenGL提供了同時設(shè)置紋理過濾和不同多級漸遠(yuǎn)紋理級別之間過濾的參數(shù):
| 過濾方式 | 描述 |
|---|---|
GL_NEAREST_MIPMAP_NEAREST |
使用最鄰近的多級漸遠(yuǎn)紋理來匹配像素大小,并使用鄰近插值進(jìn)行紋理采樣 |
GL_LINEAR_MIPMAP_NEAREST |
使用最鄰近的多級漸遠(yuǎn)紋理級別,并使用線性插值進(jìn)行采樣 |
GL_NEAREST_MIPMAP_LINEAR |
在兩個最匹配像素大小的多級漸遠(yuǎn)紋理之間進(jìn)行線性插值,使用鄰近插值進(jìn)行采樣 |
GL_LINEAR_MIPMAP_LINEAR |
在兩個鄰近的多級漸遠(yuǎn)紋理之間使用線性插值,并使用線性插值進(jìn)行采樣 |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
??需要注意的是放大(Magnify)過濾是不需要考慮多級遠(yuǎn)紋理過濾的,因?yàn)槎嗉墲u遠(yuǎn)紋理主要是使用在紋理被縮小的情況下的。
實(shí)踐
??討論了這么多關(guān)于紋理的相關(guān)知識,是時候來實(shí)現(xiàn)把紋理圖加載到我們創(chuàng)建的圖案中了。
1.加載紋理圖
??紋理圖像有可能是各種存儲格式,例如.png或.jpg等,每種都有自己的數(shù)據(jù)結(jié)構(gòu)和排列,例如png圖片有Alpha通道,而jpg沒有。讀取不同格式的紋理圖就需要不同的圖片加載器,把圖片的數(shù)據(jù)轉(zhuǎn)為字節(jié)序列,如果我們自己編寫圖片加載器那勢必是一個非常復(fù)雜的工作,且已經(jīng)偏離了學(xué)習(xí)OpenGL的重點(diǎn)。那么最好的解決方法是使用前輩造好的輪子——一個支持多種流行格式的圖像加載庫來為我們解決這個問題。比如說我們要用的stb_image.h庫。
stb_image.h
??stb_image.h是Sean Barrett的一個非常流行的單頭文件圖像加載庫,它能夠加載大部分流行的文件格式,并且能夠很簡單得整合到你的工程之中。我們要做的就是下載它,并導(dǎo)入到自己的工程中:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
??通過定義STB_IMAGE_IMPLEMENTATION預(yù)處理器會修改頭文件,讓其只包含相關(guān)的函數(shù)定義源碼,等于是將這個頭文件變?yōu)橐粋€ .cpp 文件了。現(xiàn)在只需要在你的程序中包含stb_image.h并編譯就可以了。
??我們先把要用到的圖放入自己的項(xiàng)目中,一張container是木箱圖,一張awesomeface是笑臉圖:

??我們使用該庫的stbi_load函數(shù)來導(dǎo)入我們的木箱圖,原圖如下:

//讀取紋理圖片
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
??該函數(shù)能獲取圖片的寬、高、顏色通道數(shù)和各種數(shù)據(jù)(存放在字符數(shù)組中),這些數(shù)據(jù)我們之后都會用到。
2.創(chuàng)建紋理對象
??在上面已經(jīng)對紋理對象稍作討論,得知紋理對象其實(shí)與VBO類似,都是管理緩沖區(qū)的對象,所以其創(chuàng)建的操作也與創(chuàng)建VBO大同小異:
//木箱
unsigned int textureBuffer1;
glGenTextures(1, &textureBuffer1);
??當(dāng)然也少不了綁定操作:
glBindTexture(GL_TEXTURE_2D, textureBuffer1);
??在綁定之后設(shè)置該紋理對象的紋理環(huán)繞方式和過濾方式:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
??最后是把剛加載的紋理圖像數(shù)據(jù)灌進(jìn)紋理對象管理的緩沖區(qū)中,并生成多級漸遠(yuǎn)紋理圖:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexImage2D(GLenum targe, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels):
- target: 第一個參數(shù)同樣是指定紋理目標(biāo)。設(shè)置為
GL_TEXTURE_2D,意味著目前加載的紋理圖像數(shù)據(jù)是針對目前綁定對象的GL_TEXTURE_2D目標(biāo),即使現(xiàn)在同時綁定了同一個紋理對象GL_TEXTURE_3D目標(biāo),這個目標(biāo)的緩沖區(qū)不會受影響。 - level: 第二個參數(shù)是指定多級漸遠(yuǎn)紋理的層級,0是基本級別。
- internalformat: 第三個參數(shù)是指定該紋理圖的存儲格式。我們的圖像只有RGB值,所以就存儲為RGB格式。
- width:設(shè)置最終紋理的寬度,在加載時我們已經(jīng)保存了它,使用它就是了。
- height:設(shè)置最終紋理的高度,同上。
- border:應(yīng)該總是被設(shè)為0(歷史遺留的問題)
- format:指定源圖的格式,由于我們的圖像只有RGB值所以指定為RGB格式。與 internalformat不同,這個是用什么格式讀取,而不是存儲為什么格式。
- type:指定源圖的數(shù)據(jù)類型,我們在加載時用的是字符(BYTE)數(shù)組,所以是
GL_UNSIGNED_BYTE。 - pixels:最后一個參數(shù)是真正的圖像數(shù)據(jù)。
??生成了紋理和相應(yīng)的多級漸遠(yuǎn)紋理后,釋放圖像的內(nèi)存是一個很好的習(xí)慣。
stbi_image_free(data);
應(yīng)用紋理
??現(xiàn)在我們的紋理數(shù)據(jù)已經(jīng)存儲在了紋理對象管理的緩沖區(qū)內(nèi),是時候修改頂點(diǎn)數(shù)據(jù)和著色器源碼來讀取它們了。
- 頂點(diǎn)數(shù)據(jù)
??因?yàn)閷?dǎo)入的紋理圖是木箱,所以我們打算繪制一個矩形來對整個木箱圖進(jìn)行采樣。
float vertices[] = {
//坐標(biāo) //顏色 //紋理
0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
0.5f, -0.5f,0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f
};
??因?yàn)槲覀兘壎薊BO,所以把索引了一并修改了:
unsigned int indices[] = { // 注意索引從0開始!
0, 1, 2,// 第一個三角形
2, 3, 0
};
??頂點(diǎn)數(shù)據(jù)已經(jīng)發(fā)生了變化,此時要相應(yīng)作出調(diào)整肯定還有鏈接頂點(diǎn)屬性函數(shù)的參數(shù)值:

??可以看到現(xiàn)在一個頂點(diǎn)數(shù)據(jù)組包含了3個坐標(biāo)值、3個顏色值、兩個紋理坐標(biāo)值,相應(yīng)每個數(shù)據(jù)的步長都應(yīng)紋理坐標(biāo)值的加入而發(fā)生了變化:
//鏈接頂點(diǎn)屬性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 8, (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 8, (void*)(sizeof(float) * 3));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 8, (void*)(sizeof(float) * 6));
glEnableVertexAttribArray(2);
??接下來是兩個著色器的源碼,首先是頂點(diǎn)著色器。要添加一個頂點(diǎn)屬性來接收頂點(diǎn)數(shù)據(jù)里的紋理坐標(biāo)值,然后直接輸出給片段著色器:
#version 330 core
layout (location = 0) in vec3 aPos; // 位置變量的屬性位置值為 0
layout (location = 1) in vec3 aColor; // 顏色變量的屬性位置值為 1
layout (location = 2) in vec2 aTextCoord;
out vec3 ourColor; // 向片段著色器輸出一個顏色
out vec2 textCoord;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0f);
ourColor = aColor; // 將ourColor設(shè)置為我們從頂點(diǎn)數(shù)據(jù)那里得到的輸入顏色
textCoord = aTextCoord;
}
??然后在片段著色器把輸出變量aTextCoord作為輸入變量。片段著色器作為給片段計(jì)算顏色的存在,那么它必須知道紋理對象管理的紋理數(shù)據(jù),不然只有紋理坐標(biāo)是沒法給頂點(diǎn)采樣的。那么我們怎樣把紋理對象傳遞給片段著色器呢?GLSL提供一種數(shù)據(jù)類型叫采樣器(Sampler),我們可以通過創(chuàng)建該類型的uniform變量,這樣在綁定紋理對象的時候,就會自動把數(shù)據(jù)賦值給該Sampler變量,至于為什么,稍后我們會討論。
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 textCoord;
uniform sampler2D ourTexture1;
void main()
{
//FragColor = vec4(ourColor, 1.0f);
FragColor = texture(ourTexture1, textCoord);
}
??在片段著色器獲得紋理坐標(biāo)和紋理數(shù)據(jù)后,使用GLSL內(nèi)建的texture函數(shù)來進(jìn)行采樣。第一個參數(shù)就是包含了紋理數(shù)據(jù)的紋理采樣器,第二個參數(shù)是對應(yīng)的紋理坐標(biāo)。該函數(shù)會根據(jù)之前設(shè)置的紋理屬性(使用glTexParameteri函數(shù)進(jìn)行的相關(guān)設(shè)置),對紋理坐標(biāo)進(jìn)行采樣。
??由于在此之前已經(jīng)綁定好了紋理對象,如若沒有解綁,即可直接在渲染循環(huán)中調(diào)用繪制函數(shù),進(jìn)行繪制:
while (!glfwWindowShouldClose(window))
{
//處理輸入
processInput(window);
//渲染指令
glClearColor(1.0f, 0, 0, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glBindVertexArray(VAO[0]);
shader.use();
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
//接收輸入,交換緩沖
glfwSwapBuffers(window);
glfwPollEvents();
}
??如果運(yùn)行沒有錯誤,那么就是見到如下圖像:

??在片段著色器的源碼內(nèi),我接受了來自頂點(diǎn)著色器的顏色數(shù)據(jù)ourColor但是沒有使用,現(xiàn)在我們可以來使用一下,讓這個木箱變得稍微炫酷起來:

紋理單元
??你現(xiàn)在或許會產(chǎn)生當(dāng)初我學(xué)習(xí)紋理時的疑問,就是明明沒有對這個uniform變量使用glUniform函數(shù)進(jìn)行賦值啊,它是怎么獲取到紋理數(shù)據(jù)的。因?yàn)樵贠penGL中存在紋理單元這么一個概念,一個片段著色器是可有多個紋理單元的(最多16個),一個紋理單元就是存儲同一組紋理數(shù)據(jù)的位置,16個紋理單元對應(yīng)0-15的位置,第一個紋理(Sampler變量)默認(rèn)的紋理單元是位置0,位置0是默認(rèn)激活的紋理單元。所以在前面我們并沒有給紋理分配位置。在位置0激活的情況下,函數(shù)glBindTexture就會把紋理數(shù)據(jù)綁定到激活的紋理單元下,然后就不用刻意調(diào)用glUniform函數(shù)對Sampler變量賦值。
??但是如果有多于1組的紋理數(shù)據(jù)要傳遞給片段著色器,那么就要在綁定前告知你要放在哪個紋理單元(位置)上,當(dāng)然你可以不用按順序從0到1,你可以第一個綁定5的位置,第二個綁定3的位置,只要你在調(diào)用激活函數(shù)時指定好位置就行了。
??現(xiàn)在我打算再導(dǎo)入一張紋理圖,是一張笑臉圖(awesomeface.png),看看有多組紋理數(shù)據(jù)時是怎么進(jìn)行傳遞給片段著色器的。

//笑臉
data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0);
unsigned int textureBuffer2;
glGenTextures(1, &textureBuffer2);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, textureBuffer2);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
stbi_image_free(data);
??可以看到在綁定紋理對象前要激活紋理單元,說明該紋理對象對應(yīng)某指定紋理單元。上面是激活1的位置。接下來需要修改片段著色器來接收另一組紋理數(shù)據(jù):
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 textCoord;
uniform sampler2D ourTexture1;
uniform sampler2D ourTexture2;
void main()
{
//FragColor = vec4(ourColor, 1.0f);
FragColor = mix(texture(ourTexture1, textCoord),texture(ourTexture2, textCoord), 0.2);
//FragColor = texture(ourTexture1, textCoord) * vec4(ourColor, 1.0);
}
??在輸出顏色處使用了mix函數(shù)對兩個紋理的顏色進(jìn)行混合。mix函數(shù)需要接受兩個值作為參數(shù),并對它們根據(jù)第三個參數(shù)進(jìn)行線性插值。如果第三個值是0.0,它會返回第一個輸入;如果是1.0,會返回第二個輸入值。0.2會返回80%的第一個輸入顏色和20%的第二個輸入顏色,即返回兩個紋理的混合色。
??現(xiàn)在雖然不需要使用glUniform函數(shù)來傳遞紋理數(shù)據(jù),但是需要使用該函數(shù)來告知指定采樣器(Sampler變量)是對標(biāo)哪個紋理單元的,如果只有一個紋理單元和1個采樣器就可以不用告知。就是說你可以把第一個宣告的采樣器指定為位置1的紋理單元而第二個宣告的采樣器指定為位置0的采樣器。
shader.use();
shader.setInt("ourTexture1",0);
shader.setInt("ourTexture2", 1);
??通過使用glUniform1i設(shè)置采樣器,我們保證了每個uniform采樣器對應(yīng)著正確的紋理單元。你應(yīng)該能得到下面的結(jié)果:

??誒,怎么這個笑臉是上下顛倒的?這是因?yàn)镺penGL要求y軸0.0坐標(biāo)是在圖片的底部的,但是圖片的y軸0.0坐標(biāo)通常在頂部。很幸運(yùn),stb_image.h能夠在圖像加載時幫助我們翻轉(zhuǎn)y軸,只需要在加載任何圖像前加入以下語句即可:
stbi_set_flip_vertically_on_load(true);

??現(xiàn)在笑臉的方向就正常了。

