OpenGL-入門-紋理

紋理是一個(gè)2D圖片(甚至也有1D和3D的紋理), 它可以用來添加物體的細(xì)節(jié);你可以想象紋理是一張繪有磚塊圖案的紙片,無縫折疊貼合到你的3D房子上,這樣你的房子看起來就像有著磚塊的外表。因?yàn)槲覀兛梢栽谝粡垐D片上插入非常多的細(xì)節(jié),這樣就可以讓物體非常精細(xì)而不用指定額外的頂點(diǎn)。

除了圖像以外,紋理也可以被用來儲(chǔ)存大量的數(shù)據(jù),這些數(shù)據(jù)可以發(fā)送到著色器上,但是這不是我們現(xiàn)在的主題。例如后續(xù)的法線貼圖,金屬貼圖等。

下圖中的三角形貼上了一張磚墻的圖片。

image.png

為了能夠把紋理映射到三角形上,我們需要指定三角形的每個(gè)頂點(diǎn)各自對(duì)應(yīng)紋理的哪個(gè)部分。這樣每個(gè)頂點(diǎn)就會(huì)關(guān)聯(lián)著一個(gè)紋理坐標(biāo), 用來指明著色器該從紋理圖像的哪個(gè)部分采樣。之后在圖形的其他片段進(jìn)行片段插值。

紋理坐標(biāo)在x和y軸上,范圍為0到1之間(2D紋理)。使用紋理坐標(biāo)獲取紋理顏色叫做采樣。紋理坐標(biāo)是典型的笛卡爾坐標(biāo),左下角為(0, 0),(1, 1)使用右上角。


image.png

我們?yōu)槿切沃付?個(gè)紋理坐標(biāo)點(diǎn)。如上圖所示,我們希望三角形的左下角對(duì)應(yīng)紋理的左下角,因此我們把三角形左下角頂點(diǎn)的紋理坐標(biāo)設(shè)置為(0, 0);三角形的上頂點(diǎn)對(duì)應(yīng)于圖片的上中位置所以我們把它的紋理坐標(biāo)設(shè)置為(0.5, 1.0);同理右下方的頂點(diǎn)設(shè)置為(1, 0)。我們只要給頂點(diǎn)著色器傳遞這三個(gè)紋理坐標(biāo)就行了,接下來它們會(huì)被傳片段著色器中,它會(huì)為每個(gè)片段進(jìn)行紋理坐標(biāo)的插值。

紋理坐標(biāo)看起來就像這樣:

float texCoords[] = {
    0.0f, 0.0f, // 左下角
    1.0f, 0.0f, // 右下角
    0.5f, 1.0f // 上中
};

對(duì)紋理采樣有多種插值方法,所以我們需要自己告訴OpenGL該怎么對(duì)紋理進(jìn)行采樣。

紋理環(huán)繞方式


紋理坐標(biāo)的范圍通常是(0, 0)到(1, 1),那如果我們把紋理坐標(biāo)設(shè)置在范圍之外會(huì)發(fā)生什么?OpenGL默認(rèn)的行為是重復(fù)這個(gè)紋理圖形,但OpenGL提供了更多的選擇:

環(huán)繞方式 描述
GL_REPEAT 對(duì)紋理的默認(rèn)行為。重復(fù)紋理圖像。
GL_MIRRORED_REPEAT 和GL_REPEAT一樣,但每次重復(fù)圖片是鏡像放置的。
GL_CLAMP_TO_EDGE 紋理坐標(biāo)會(huì)被約束在0到1之間,超出的部分會(huì)重復(fù)紋理坐標(biāo)的邊緣,產(chǎn)生一種邊緣被拉伸的效果。
GL_CLAMP_TO_BORDER 超出的坐標(biāo)為用戶指定的邊緣顏色。

當(dāng)紋理坐標(biāo)超出默認(rèn)范圍時(shí),每個(gè)選項(xiàng)都有不同的視覺效果輸出。


image.png

上面提及的每個(gè)選項(xiàng)都可以使用glTexParameter*函數(shù)對(duì)單獨(dú)的一個(gè)坐標(biāo)軸設(shè)置s、t(如果是使用3D紋理那么還有一個(gè)r)它們和x、y、z是等價(jià)的):

glTexParamateri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParamateri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);

第一個(gè)參數(shù): 代表紋理目標(biāo)類型,我們使用的是2D紋理,因此紋理目標(biāo)是GL_TEXTURE_2D
第二個(gè)參數(shù): 我們指定設(shè)置的選項(xiàng)與應(yīng)用的紋理軸。s為水平方向,t為垂直方向。
第三個(gè)參數(shù): 我們傳遞一個(gè)環(huán)繞方式,在這個(gè)例子中OpenGL會(huì)給當(dāng)前激活的紋理設(shè)定紋理環(huán)繞方式為GL_MIRRORED_REPEAT。
如果我們選擇GL_CLAMP_TO_BORDER選項(xiàng),我們還需要指定一個(gè)邊緣的顏色。這需要使用glTexParameter函數(shù)的fv后綴形式,用GL_TEXTURE_BORDER_COLOR作為它的選項(xiàng),并且傳遞一個(gè)float數(shù)組作為邊緣的顏色值:

float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

紋理過濾


紋理坐標(biāo)是不依賴分辨率的,它可以是任意浮點(diǎn)數(shù),所以O(shè)penGL需要知道如何計(jì)算一個(gè)紋理坐標(biāo)對(duì)應(yīng)的紋理像素。你可能已經(jīng)猜到了,OpenGL也有對(duì)于紋理過濾的選項(xiàng)。紋理過濾有很多個(gè)選項(xiàng),但是現(xiàn)在我們只討論最重要的兩種: GL_NEAREST和GL_LINEAR。

常見的紋理過濾方式如下四種:
鄰近過濾,線性過濾(雙線性), 三線性過濾以及各向異性過濾。不同的過濾方式的計(jì)算開銷存在著差異,異性過濾效果最好但其開銷最大。

GL_NEAREST(鄰近過濾)是OpenGL默認(rèn)的紋理過濾方式。設(shè)置為GL_NEAREST后,OpenGL會(huì)選擇中心點(diǎn)最接近紋理坐標(biāo)的那個(gè)像素。下圖中你可以看到四個(gè)像素,加號(hào)代表紋理坐標(biāo)。左上角那個(gè)紋理像素的中心距離紋理坐標(biāo)最近,所以它會(huì)被選擇為樣本顏色:

image.png

GL_LINEAR(線性過濾),它會(huì)基于紋理坐標(biāo)附近的紋理像素, 計(jì)算出一個(gè)插值,近似出這些紋理像素之間得顏色。一個(gè)紋理像素的中心距離紋理坐標(biāo)越近,那么這個(gè)紋理像素對(duì)最終的樣本顏色的貢獻(xiàn)越大。下圖中你可以看到返回的顏色是鄰近像素的混合色:

image.png

那么這兩種紋理過濾方式會(huì)有怎樣的視覺效果呢?讓我們看看在一個(gè)很大的物體上應(yīng)用一張低分辨率的紋理會(huì)發(fā)生什么吧。


image.png

GL_NEAREST產(chǎn)生了顆粒狀的圖案,我們能夠清晰看到組成紋理的像素,而GL_LINEAR能夠產(chǎn)生更加平滑的圖案,很難看出單個(gè)的紋理像素。GL_LINEAR可以產(chǎn)生更真實(shí)的輸出,但有些開發(fā)者更喜歡8-bit風(fēng)格,所以他們會(huì)用GL_NEAREST選項(xiàng)。
當(dāng)進(jìn)行放大和縮小操作時(shí)可以設(shè)置紋理過濾的選項(xiàng),比如你可以在紋理被縮小時(shí)使用鄰近過濾,而放大時(shí)使用線性過濾。我們需要使用glTexParameter*函數(shù)為放大和縮小指定過濾方式。這段代碼看起來會(huì)和紋理環(huán)繞方式的設(shè)置很相似:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

多級(jí)漸遠(yuǎn)紋理

想象一下,假設(shè)我們有一個(gè)包含著上千個(gè)物體的大房間,每個(gè)物體上都有紋理。有些物體會(huì)距離攝像機(jī)很遠(yuǎn),但其紋理會(huì)擁有與近處物體同樣高的分辨率。此時(shí)若還是采用高分辨率的紋理進(jìn)行采用,就會(huì)出現(xiàn)失真的現(xiàn)象。例如為一個(gè)44的像素片段應(yīng)用512512的紋理進(jìn)行采樣,線性過濾模式下,每個(gè)像素僅會(huì)使用到4個(gè)紋素,顯然這樣采樣的結(jié)果不真實(shí)的??梢岳斫鉃槟阆胗靡粋€(gè)像素點(diǎn)來表達(dá)一張貼圖里8*8或10 * 10個(gè)點(diǎn)的顏色信息。

OpenGL使用了一種叫做多級(jí)漸遠(yuǎn)紋理的概念來解決這個(gè)問題,它簡單來說就是一系列的紋理圖像。后一個(gè)紋理圖像是前一個(gè)分辨率的1/2。其背后的理念很簡單: 距離觀察者的距離超過一定的閾值后,OpenGL會(huì)使用不同的多級(jí)漸遠(yuǎn)紋理,即最適合物體距離的那個(gè)。


image.png

手工為每個(gè)紋理圖像創(chuàng)建一系列的多級(jí)漸遠(yuǎn)紋理很麻煩,OpenGL提供了一個(gè)glGenerateMipmaps函數(shù),在創(chuàng)建完一個(gè)紋理后調(diào)用它,OpenGL就會(huì)承擔(dān)創(chuàng)建多級(jí)漸遠(yuǎn)紋理的工作。

在渲染中切換多級(jí)漸遠(yuǎn)紋理級(jí)別時(shí),OpenGL在兩個(gè)不同級(jí)別的多級(jí)漸遠(yuǎn)紋理層之間會(huì)產(chǎn)生不真實(shí)的生硬邊界。就像普通的紋理過濾一樣,切換多級(jí)漸遠(yuǎn)紋理級(jí)別時(shí)你也可以在兩個(gè)不同的多級(jí)漸遠(yuǎn)紋理級(jí)別之間使用NEAREST和LINEAR過濾。為了指定不同多級(jí)漸遠(yuǎn)紋理級(jí)別之間的過濾方式,你可以使用下面四個(gè)選項(xiàng)中的一個(gè)代替原有的過濾方式:

過濾方式 描述
GL_NEAREST_MIPMAP_NEAREST 使用最鄰近的多級(jí)漸遠(yuǎn)紋理來匹配像素大小,并使用鄰近插值進(jìn)行紋理采樣
GL_LINEAR_MIPMAP_NEAREST 使用最鄰近的多級(jí)漸遠(yuǎn)紋理級(jí)別,并使用線性插值進(jìn)行采樣
GL_NEAREST_MIPMAP_LINEAR 在兩個(gè)最匹配像素大小的多級(jí)漸遠(yuǎn)紋理之間進(jìn)行線性插值,使用鄰近插值進(jìn)行采樣
GL_LINEAR_MIPMAP_LINEAR 在兩個(gè)鄰近的多級(jí)漸遠(yuǎn)紋理之間使用線性插值,并使用線性插值進(jìn)行采樣

就像紋理過濾一樣,我們可以使用glTexParameteri將過濾方式設(shè)置為前面四種中提到的方法之一:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

代碼實(shí)踐


加載與創(chuàng)建紋理


紋理圖像可能被存儲(chǔ)為各種各樣的格式,每種都有自己的數(shù)據(jù)結(jié)構(gòu)和排列,所以我們?nèi)绾尾拍馨堰@些圖像加載到應(yīng)用中呢?我們使用的是一個(gè)支持多種流行格式的圖像加載庫來解決這個(gè)問題, stb_image.h庫。
具體文件配置見LearnOpenGL。
生成一個(gè)紋理的過程大致如下:

unsigned int texture;
glGenTexture(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 為當(dāng)前綁定的紋理對(duì)象設(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);
// 加載并生成紋理
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
   // 為當(dāng)前綁定的紋理自動(dòng)生成所有所需的多級(jí)漸遠(yuǎn)紋理。 
    glGenerateMipmap(GL_TEXTURE_2D);
}
else{
   std::cout << "Failed to load texture" << std::endl;
}
// 生成了紋理和相應(yīng)的多級(jí)漸遠(yuǎn)紋理后,釋放圖像的內(nèi)存是一個(gè)很好的習(xí)慣。
stbi_image_free(data);

應(yīng)用紋理


我們需要為頂點(diǎn)數(shù)據(jù)中添加頂點(diǎn)對(duì)應(yīng)的紋理坐標(biāo),告知OpenGL如何進(jìn)行采樣。

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    // 左上
};

頂點(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;
}

片段著色器接下來把輸出變量TexCoord作為輸入變量。
如何將紋理對(duì)象傳遞給片段著色器?GLSL有一個(gè)供紋理對(duì)象使用的內(nèi)建數(shù)據(jù)類型,叫做采樣器,它以紋理類型作為后綴,如sampler1D, sampler3D等。在我們的例子中可以簡單聲明為一個(gè)uniform samper2D。

#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
    FragColor = texture(ourTexture, TexCoord);
}

最后只剩下在調(diào)用glDrawElements之前綁定紋理了,它會(huì)自動(dòng)把紋理賦值給片段著色器的采樣器:

glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

你會(huì)看到下面的圖像:


image.png

紋理單元


這里你肯定很奇怪為什么sampler2D是個(gè)uniform變量,而我們卻不用glUniform方法給它賦值。使用glUniform1i,我們可以給紋理采樣器分配一個(gè)位置值,這樣的話我們能夠在一個(gè)片段中設(shè)置多個(gè)紋理。一個(gè)紋理的位置值通常被稱為紋理單元。一個(gè)紋理的默認(rèn)紋理單元是0,它是默認(rèn)的激活紋理單元,所以教程前面部分我們沒有分配一個(gè)位置值。
紋理單元的主要目的是讓我們在著色器中可以使用多于一個(gè)的紋理。通常把紋理單元賦值給采樣器,我們可以一次綁定多個(gè)紋理,只要我們首先激活對(duì)應(yīng)的紋理單元。就像glBindTexture一樣,我們可以使用glActiveTexture激活紋理單元,傳入我們需要使用的紋理單元:

glActiveTexture(GL_TEXTURE0); // 在綁定紋理之前先激活紋理單元
glBindTexture(GL_TEXTURE_2D, texutre);

激活紋理之后,接下來的glBindTexture函數(shù)調(diào)用會(huì)綁定這個(gè)紋理當(dāng)前激活的紋理單元。紋理單元GL_TEXTURE0默認(rèn)總是被激活,所以我們在前面的例子里當(dāng)我們使用glBindTexture的時(shí)候,無需激活任何紋理單元。

OpenGL至少保證你有16個(gè)紋理單元可用,也就是說你可以激活從GL_TEXUTRE0到GL_TEXTURE15。
更新片段著色器代碼:

#version 330 core
...

uniform sampler2D texture1;
uniform sampler2D texture2;

void main()
{
    // 0.2會(huì)返回第一個(gè)80%輸入的顏色和20%第二個(gè)輸入顏色。
    FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}

當(dāng)使用多個(gè)紋理時(shí)(超出一個(gè)), 我們必須改變部分渲染流程,在綁定兩個(gè)紋理到對(duì)應(yīng)的紋理單元后,再定義哪個(gè)uniform采樣器對(duì)應(yīng)哪個(gè)紋理單元:

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);

glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
//通過使用glUniform1i設(shè)置每個(gè)采樣器的方式來告訴OpenGL每個(gè)著色器采樣器屬于哪個(gè)紋理單元。
// 只需要設(shè)置一次即可,所以可以放在渲染循環(huán)之前:

ourShader.use();
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1), 0); // 手動(dòng)設(shè)置
while(...) 
{
    [...]
}

通過使用glUniform1i設(shè)置采樣器,我們保證了每個(gè)uniform采樣器對(duì)應(yīng)著正確的紋理單元。你應(yīng)該能得到下面的結(jié)果:

image.png

你可能注意到了最終的笑臉圖案是上下顛倒了的!這是因?yàn)?strong>圖片在存儲(chǔ)像素?cái)?shù)據(jù)時(shí)通常以左上角為原點(diǎn)的,而紋理坐標(biāo)系是以左下角為原點(diǎn)。

修復(fù)這個(gè)問題,可以有以下兩個(gè)常見方法:
1.將頂點(diǎn)數(shù)據(jù)中的紋理坐標(biāo)的y值翻轉(zhuǎn)。(1.0 -y值)
2.可以編輯頂點(diǎn)著色器來自動(dòng)翻轉(zhuǎn)y坐標(biāo),將Texcoord的值變?yōu)門exCoord = vec2(texCoord.x, 1.0f - texCoord.y);。

參考

紋理 - LearnOpenGL CN (learnopengl-cn.github.io)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 今天,我們來了解了解紋理的知識(shí),該篇文章轉(zhuǎn)載自這里。 紋理 我們已經(jīng)了解到,我們可以為每個(gè)頂點(diǎn)添加顏色來增加圖形的...
    Daniel_Harvey閱讀 2,516評(píng)論 0 4
  • 前言 本文是關(guān)于OpenGL ES的系統(tǒng)性學(xué)習(xí)過程,記錄了自己在學(xué)習(xí)OpenGL ES時(shí)的收獲。這篇文章的目標(biāo)是學(xué)...
    秦明Qinmin閱讀 10,647評(píng)論 7 22
  • 前言 OpenGL的紋理實(shí)際上運(yùn)用十分廣泛,是OpenGL中的重點(diǎn)。如果你有看過Android底層的繪制原理,能夠...
    yjy239閱讀 4,220評(píng)論 5 7
  • 一、什么是紋理? 藝術(shù)家和程序員更喜歡使用紋理(Texture)。紋理是一個(gè)2D圖片(甚至也有1D和3D的紋理),...
    Sheisone閱讀 369評(píng)論 0 0
  • 前言 紋理貼圖是在光柵話的模型表面覆蓋圖像的技術(shù)。他是為渲染場景添加真是感的最基本最重要的方法之一。 紋理貼圖非常...
    朽木自雕也閱讀 3,958評(píng)論 0 3

友情鏈接更多精彩內(nèi)容