opengl紋理


關於紋理

可以為每個頂點添加顏色來增加圖形的細節,從而創建出豐富的圖像。想讓圖形看起來更真實,我們就必須有足夠多的頂點,從而指定足夠多的顏色。這將會產生很多額外開銷.紋理是一個2D圖片,它可以用來添加物體的細節,這樣就可以讓物體非常精細而不用指定額外的頂點。

為了能夠把紋理映射(Map)到三角形上,我們需要指定三角形的每個頂點各自對應紋理的哪個部分。這樣每個頂點就會關聯着一個紋理坐標(Texture Coordinate),用來標明該從紋理圖像的哪個部分采樣。之后在圖形的其它片段上進行片段插值(Fragment Interpolation)。

紋理坐標在x和y軸上,范圍為0到1之間(注意我們使用的是2D紋理圖像)。使用紋理坐標獲取紋理顏色叫做采樣(Sampling)。紋理坐標起始於(0, 0),也就是紋理圖片的左下角,終始於(1, 1),即紋理圖片的右上角。下面的圖片展示了我們是如何把紋理坐標映射到三角形上的。

img

我們為三角形指定了3個紋理坐標點。如上圖所示,我們希望三角形的左下角對應紋理的左下角,因此我們把三角形左下角頂點的紋理坐標設置為(0, 0);三角形的上頂點對應於圖片的上中位置所以我們把它的紋理坐標設置為(0.5, 1.0);同理右下方的頂點設置為(1, 0)。我們只要給頂點着色器傳遞這三個紋理坐標就行了,接下來它們會被傳片段着色器中,它會為每個片段進行紋理坐標的插值

紋理坐標看起來就像這樣:

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

紋理環繞方式

紋理坐標的范圍通常是從(0, 0)到(1, 1),那如果我們把紋理坐標設置在范圍之外會發生什么?OpenGL默認的行為是重復這個紋理圖像,其他的環繞方式

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

紋理選項都可以使用glTexParameter*函數對單獨的一個坐標軸設置(str)它們和xyz是等價的):

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

第一個參數指定了紋理目標;我們使用的是2D紋理,因此紋理目標是GL_TEXTURE_2D。第二個參數需要我們指定設置的選項與應用的紋理軸。我們打算配置的是WRAP選項,並且指定ST軸。最后一個參數需要我們傳遞一個環繞方式(Wrapping),在這個例子中OpenGL會給當前激活的紋理設定紋理環繞方式為GL_MIRRORED_REPEAT。

如果我們選擇GL_CLAMP_TO_BORDER選項,我們還需要指定一個邊緣的顏色。這需要使用glTexParameter函數的fv后綴形式,用GL_TEXTURE_BORDER_COLOR作為它的選項,並且傳遞一個float數組作為邊緣的顏色值:

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

紋理過濾

紋理坐標不依賴於分辨率(Resolution),它可以是任意浮點值,所以OpenGL需要知道怎樣將紋理像素(Texture Pixel)映射到紋理坐標。當你有一個很大的物體但是紋理的分辨率很低的時候這就變得很重要了。OpenGL也有對於紋理過濾(Texture Filtering)的選項。紋理過濾有很多個選項,但是現在我們只討論最重要的兩種:GL_NEAREST和GL_LINEAR。

GL_NEAREST(也叫鄰近過濾,Nearest Neighbor Filtering)是OpenGL默認的紋理過濾方式。當設置為GL_NEAREST的時候,OpenGL會選擇中心點最接近紋理坐標的那個像素。下圖中你可以看到四個像素,加號代表紋理坐標。左上角那個紋理像素的中心距離紋理坐標最近,所以它會被選擇為樣本顏色:

img

GL_LINEAR(也叫線性過濾,(Bi)linear Filtering)它會基於紋理坐標附近的紋理像素,計算出一個插值,近似出這些紋理像素之間的顏色。一個紋理像素的中心距離紋理坐標越近,那么這個紋理像素的顏色對最終的樣本顏色的貢獻越大。下圖中你可以看到返回的顏色是鄰近像素的混合色:

img

那么這兩種紋理過濾方式有怎樣的視覺效果呢?讓我們看看在一個很大的物體上應用一張低分辨率的紋理會發生什么吧(紋理被放大了,每個紋理像素都能看到):

img

GL_NEAREST產生了顆粒狀的圖案,我們能夠清晰看到組成紋理的像素,而GL_LINEAR能夠產生更平滑的圖案,很難看出單個的紋理像素。GL_LINEAR可以產生更真實的輸出,但有些開發者更喜歡8-bit風格,所以他們會用GL_NEAREST選項。

當進行放大(Magnify)和縮小(Minify)操作的時候可以設置紋理過濾的選項,比如你可以在紋理被縮小的時候使用鄰近過濾,被放大時使用線性過濾。我們需要使用glTexParameter*函數為放大和縮小指定過濾方式。這段代碼看起來會和紋理環繞方式的設置很相似:

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

多級紋理

再提個大場景中,每個物體上都有紋理。有些物體會很遠,但其紋理會擁有與近處物體同樣高的分辨率。由於遠處的物體可能只產生很少的片段,OpenGL從高分辨率紋理中為這些片段獲取正確的顏色值就很困難,因為它需要對一個跨過紋理很大部分的片段只拾取一個紋理顏色。在小物體上這會產生不真實的感覺,對它們使用高分辨率紋理浪費內存。

OpenGL使用一種叫做多級漸遠紋理(Mipmap)的概念來解決這個問題,它簡單來說就是一系列的紋理圖像,后一個紋理圖像是前一個的二分之一。多級漸遠紋理背后的理念很簡單:距觀察者的距離超過一定的閾值,OpenGL會使用不同的多級漸遠紋理,即最適合物體的距離的那個。由於距離遠,解析度不高也不會被用戶注意到。同時,多級漸遠紋理另一加分之處是它的性能非常好。讓我們看一下多級漸遠紋理是什么樣子的:

img

手工為每個紋理圖像創建一系列多級漸遠紋理很麻煩,幸好OpenGL有一個glGenerateMipmaps函數,在創建完一個紋理后調用它OpenGL就會承擔接下來的所有工作了。后面的教程中你會看到該如何使用它。

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

過濾方式 描述
GL_NEAREST_MIPMAP_NEAREST 使用最鄰近的多級漸遠紋理來匹配像素大小,並使用鄰近插值進行紋理采樣
GL_LINEAR_MIPMAP_NEAREST 使用最鄰近的多級漸遠紋理級別,並使用線性插值進行采樣
GL_NEAREST_MIPMAP_LINEAR 在兩個最匹配像素大小的多級漸遠紋理之間進行線性插值,使用鄰近插值進行采樣
GL_LINEAR_MIPMAP_LINEAR 在兩個鄰近的多級漸遠紋理之間使用線性插值,並使用線性插值進行采樣

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

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

一個常見的錯誤是,將放大過濾的選項設置為多級漸遠紋理過濾選項之一。這樣沒有任何效果,因為多級漸遠紋理主要是使用在紋理被縮小的情況下的:紋理放大不會使用多級漸遠紋理,為放大過濾設置多級漸遠紋理的選項會產生一個GL_INVALID_ENUM錯誤代碼。

生成紋理

創建紋理對象,使用id類記錄對象

unsigned int texture;
glGenTextures(1, &texture);

glGenTextures函數首先需要輸入生成紋理的數量,然后把它們儲存在第二個參數的unsigned int數組中(我們的例子中只是單獨的一個unsigned int),

綁定對象,讓之后任何的紋理指令都可以配置當前綁定的紋理:

glBindTexture(GL_TEXTURE_2D, texture);

使用圖片數據生成一個紋理了:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);

函數很長,參數也不少,所以我們一個一個地講解:

  • 第一個參數指定了紋理目標(Target)。設置為GL_TEXTURE_2D意味着會生成與當前綁定的紋理對象在同一個目標上的紋理(任何綁定到GL_TEXTURE_1D和GL_TEXTURE_3D的紋理不會受到影響)。
  • 第二個參數為紋理指定多級漸遠紋理的級別,如果你希望單獨手動設置每個多級漸遠紋理的級別的話。這里我們填0,也就是基本級別。
  • 第三個參數告訴OpenGL我們希望把紋理儲存為何種格式。我們的圖像只有RGB值,因此我們也把紋理儲存為RGB值。
  • 第四個和第五個參數設置最終的紋理的寬度和高度。我們之前加載圖像的時候儲存了它們,所以我們使用對應的變量。
  • 下個參數應該總是被設為0(歷史遺留的問題)。
  • 第七第八個參數定義了源圖的格式和數據類型。我們使用RGB值加載這個圖像,並把它們儲存為char(byte)數組,我們將會傳入對應值。
  • 最后一個參數是真正的圖像數據。

當調用glTexImage2D時,當前綁定的紋理對象就會被附加上紋理圖像。然而,目前只有基本級別(Base-level)的紋理圖像被加載了,如果要使用多級漸遠紋理,我們必須手動設置所有不同的圖像(不斷遞增第二個參數)。或者,直接在生成紋理之后調用glGenerateMipmap。這會為當前綁定的紋理自動生成所有需要的多級漸遠紋理。

生成一個紋理的過程應該看起來像這樣:

unsigned int texture;
//生成並且綁定紋理對象
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
//為當前綁定的紋理對象設置環繞方式
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);
    glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
    std::cout << "Failed to load texture" << std::endl;
}

應用紋理

使用glDrawElements繪制,我們需要告知OpenGL如何采樣紋理,所以我們必須使用紋理坐標更新頂點數據:

float vertices[] = {
//     ---- 位置 ----       ---- 顏色 ----     - 紋理坐標 -
     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    // 左上
};

由於我們添加了一個額外的頂點屬性,我們必須告訴OpenGL我們新的頂點格式:

img

glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);

注意,我們同樣需要調整前面兩個頂點屬性的步長參數為8 * sizeof(float)

接着我們需要調整頂點着色器使其能夠接受頂點坐標為一個頂點屬性,並把坐標傳給片段着色器:

#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作為輸入變量。

片段着色器也應該能訪問紋理對象,但是我們怎樣能把紋理對象傳給片段着色器呢?GLSL有一個供紋理對象使用的內建數據類型,叫做采樣器(Sampler),它以紋理類型作為后綴,比如sampler1Dsampler3D,或在我們的例子中的sampler2D。我們可以簡單聲明一個uniform sampler2D把一個紋理添加到片段着色器中,稍后我們會把紋理賦值給這個uniform。

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;

void main()
{
    FragColor = texture(ourTexture, TexCoord);
}

我們使用GLSL內建的texture函數來采樣紋理的顏色,它第一個參數是紋理采樣器,第二個參數是對應的紋理坐標。texture函數會使用之前設置的紋理參數對相應的顏色值進行采樣。這個片段着色器的輸出就是紋理的(插值)紋理坐標上的(過濾后的)顏色

現在只剩下在調用glDrawElements之前綁定紋理了,它會自動把紋理賦值給片段着色器的采樣器:

//glBindTexture中textture是紋理對象的id
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

完成之后你會看到下面的圖像:

img

我們還可以把得到的紋理顏色與頂點顏色混合,來獲得更有趣的效果。我們只需把紋理顏色與頂點顏色在片段着色器中相乘來混合二者的顏色:

FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);

最終的效果應該是頂點顏色和紋理顏色的混合色:

img

我猜你會說我們的箱子喜歡跳70年代的迪斯科。

紋理單元

你可能會奇怪為什么sampler2D變量是個uniform,我們卻不用glUniform給它賦值。使用glUniform1i,我們可以給紋理采樣器分配一個位置值,這樣的話我們能夠在一個片段着色器中設置多個紋理。一個紋理的位置值通常稱為一個紋理單元(Texture Unit)。一個紋理的默認紋理單元是0,它是默認的激活紋理單元,所以教程前面部分我們沒有分配一個位置值。

紋理單元的主要目的是讓我們在着色器中可以使用多於一個的紋理。通過把紋理單元賦值給采樣器,我們可以一次綁定多個紋理,只要我們首先激活對應的紋理單元。就像glBindTexture一樣,我們可以使用glActiveTexture激活紋理單元,傳入我們需要使用的紋理單元:

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

激活紋理單元之后,接下來的glBindTexture函數調用會綁定這個紋理到當前激活的紋理單元,紋理單元GL_TEXTURE0默認總是被激活,所以我們在前面的例子里當我們使用glBindTexture的時候,無需激活任何紋理單元。

OpenGL至少保證有16個紋理單元供你使用,也就是說你可以激活從GL_TEXTURE0到GL_TEXTRUE15。它們都是按順序定義的,所以我們也可以通過GL_TEXTURE0 + 8的方式獲得GL_TEXTURE8,這在當我們需要循環一些紋理單元的時候會很有用。

我們仍然需要編輯片段着色器來接收另一個采樣器。這應該相對來說非常直接了:

#version 330 core
...

uniform sampler2D texture1;
uniform sampler2D texture2;

void main()
{
    FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}

最終輸出顏色現在是兩個紋理的結合。GLSL內建的mix函數需要接受兩個值作為參數,並對它們根據第三個參數進行線性插值。如果第三個值是0.0,它會返回第一個輸入;如果是1.0,會返回第二個輸入值。0.2會返回80%的第一個輸入顏色和20%的第二個輸入顏色,即返回兩個紋理的混合色。

我們現在需要載入並創建另一個紋理;你應該對這些步驟很熟悉了。記得創建另一個紋理對象,載入圖片,使用glTexImage2D生成最終紋理。

為了使用第二個紋理(以及第一個),我們必須改變一點渲染流程,先綁定兩個紋理到對應的紋理單元,然后定義哪個uniform采樣器對應哪個紋理單元

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設置每個采樣器的方式告訴OpenGL每個着色器采樣器屬於哪個紋理單元。我們只需要設置一次即可,所以這個會放在渲染循環的前面:

ourShader.use(); // 別忘記在激活着色器前先設置uniform!
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 手動設置
ourShader.setInt("texture2", 1); // 或者使用着色器類設置

while(...) 
{
    [...]
}

通過使用glUniform1i設置采樣器,我們保證了每個uniform采樣器對應着正確的紋理單元。你應該能得到下面的結果:

img

你可能注意到紋理上下顛倒了!這是因為OpenGL要求y軸0.0坐標是在圖片的底部的,但是圖片的y軸0.0坐標通常在頂部。在圖像加載時幫助我們翻轉y軸。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM