OpenGL入門1.4:紋理/貼圖Texture


每一個小步驟的源碼都放在了Github

的內容為插入注釋,可以先跳過

前言

游戲玩家對Texture這個詞應該不陌生,我們已經知道了怎么為每個頂點添加顏色來增加圖形的細節,但,如果想讓圖形看起來更真實,顏色更多,就必須有足夠多的頂點,從而指定足夠多的顏色,就會產生很多額外開銷,所以現在我們需要紋理(Texture),也可翻譯做貼圖,下面統稱紋理

什么是Texture

紋理是一個2D圖片(也有1D和3D的Texture),它可以用來添加物體的細節,我們可以在一張圖片上插入非常多的細節,這樣就可以讓物體非常精細而不用指定額外的頂點

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

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

下圖是把紋理坐標映射到三角形上

我們為三角形指定了3個紋理坐標點:(0,0)(0.5,1.0)(1,0) ,如上圖

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

紋理坐標:

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

對紋理的采樣是一個很模糊的概念,它可以采用幾種不同的插值方式,所以你需要自己告訴OpenGL該怎樣對紋理采樣

紋理環繞方式

紋理坐標的范圍通常是從(0, 0)到(1, 1),那如果我們把紋理坐標設置在范圍之外會發生什么?默認情況下OpenGL會重復貼上這個紋理圖像,同時OpenGL也提供了更多的選擇:

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

前面提到的每個選項都可以使用glTexParameter*函數對單獨的一個坐標軸設置(st(如果是使用3D紋理那么還有一個r)它們和xyz是等價的):

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
  1. 第一個參數指定了紋理目標;我們使用的是2D紋理,因此紋理目標是GL_TEXTURE_2D
  2. 第二個參數需要我們指定設置的選項與應用的紋理軸,我們打算配置的是WRAP選項,並且指定ST
  3. 最后一個參數需要我們傳遞一個環繞方式(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)映射到紋理坐標

Texture Pixel也叫Texel,你可以想象你打開一張.jpg格式圖片,不斷放大你會發現它是由無數像素點組成的,這個點就是紋理像素;注意不要和紋理坐標搞混了,紋理坐標是你給模型頂點設置的那個數組,OpenGL以這個頂點的紋理坐標數據去查找紋理圖像上的像素,然后進行采樣提取紋理像素的顏色,當你有一個很大的物體但是紋理的分辨率很低的時候這就變得很重要了

OpenGL當然也有對於紋理過濾(Texture Filtering)的選項,選項有多個,但是現在我們只討論最重要的兩種:GL_NEARESTGL_LINEAR

GL_NEAREST

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

GL_LINEAR

GL_LINEAR(線性過濾,(Bi)linear Filtering)它會基於紋理坐標附近的紋理像素,計算出一個插值,近似出這些紋理像素之間的顏色,一個紋理像素的中心距離紋理坐標越近,那么這個紋理像素的顏色對最終的樣本顏色的貢獻越大:

讓我們來看看在一個很大的物體上應用一張低分辨率的紋理會發生什么:

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會使用不同的多級漸遠紋理,即最適合物體的距離的那個

由於距離遠,解析度不高也不會被用戶注意到,同時它的性能非常好,就如下圖:

當然手工為每個紋理圖像創建一系列多級漸遠紋理絕對很麻煩,幸好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錯誤代碼

加載與創建Texture

使用紋理之前要做的第一件事是把它們加載到我們的應用中

圖片可能被儲存.jpg .png等為各種各樣的格式,每種都有自己的數據結構和排列,我們要怎樣讀進來呢?最直接的辦法是選一個需要的文件格式,比如.png,然后自己寫一個圖像加載器,把圖像轉化為字節序列,雖然不難,但仍然挺麻煩的,而且如果要支持更多文件格式呢?當然這不是我們現在應該深究的,所以我們將使用一個第三方的圖片庫來解決這個問題

stb_image.h

stb_image.hSean Barrett的一個非常流行的單頭文件圖像加載庫,它能夠加載大部分流行的文件格式,並且能夠很簡單得整合到你的工程之中,stb_image.h可以在這里下載,下載這一個頭文件,將它以stb_image.h的名字加入你的工程,並另創建一個新的C++文件,輸入以下代碼:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

通過定義STB_IMAGE_IMPLEMENTATION,預處理器會修改頭文件,讓其只包含相關的函數定義源碼,等於是將這個頭文件變為一個 .cpp 文件了,現在只需要在你的程序中包含stb_image.h並編譯就可以了

現在我們要先下載一張木箱的圖片用作測試

然后使用stb_image.h加載圖片,調用stbi_load函數:

int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);

這個函數首先接受一個圖像文件的路徑作為輸入,接下來它需要三個int作為它的第二、三、四個參數,stb_image.h將會用圖像的寬度高度顏色通道的個數填充這三個變量。我們之后生成紋理的時候會用到的圖像的寬度和高度的。

生成紋理

和之前生成的OpenGL對象一樣,紋理也是使用ID引用的,應該不難讓你想起創建VAO和VBO的過程

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

glGenTextures函數首先需要輸入生成紋理的數量,然后把它們儲存在第二個參數的unsigned int數組中(我們的例子中只是單獨的一個unsigned int),就像其他對象一樣,我們需要綁定它,讓之后任何的紋理指令都可以配置當前綁定的紋理:

glBindTexture(GL_TEXTURE_2D, texture);

現在紋理已經綁定了,我們可以使用前面載入的圖片數據生成一個紋理了

紋理可以通過glTexImage2D來生成:

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

我們逐個看參數:

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

當調用glTexImage2D時,當前綁定的紋理對象就會被附加上紋理圖像

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

glGenerateMipmap(GL_TEXTURE_2D);

生成了紋理和相應的多級漸遠紋理后,釋放圖像的內存是一個很好的習慣

stbi_image_free(data);

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

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;
}
stbi_image_free(data);

應用紋理1.4.0

后面的這部分我們會使用glDrawElements繪制OpenGL入門3:渲染管線簡介,三角形最后一部分的矩形

我們需要告知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    // 左上
};
unsigned int indices[] = {
	0, 1, 3, // first triangle
	1, 2, 3  // second triangle
};

注意初始化和渲染循環也要改:

// 初始化+
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// 渲染循環
ourShader.use();
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

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

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

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

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

// vertex shader
#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

// fragment shader
#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;

void main()
{
    FragColor = texture(ourTexture, TexCoord);//texture(紋理采樣器,對應的紋理坐標)
}

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

現在我們要在調用glDrawElements之前綁定紋理,你不需要手動更改我們在片段着色器定義的uniform sampler2D ourTexture,它會自動把紋理賦值給片段着色器的采樣器ourTexture

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

沒有意外的話結果就是:

參考源碼:

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>

#include "shader.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

// callback
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

int main()
{

	#pragma region 窗口
	// 實例化GLFW窗口
	glfwInit();//glfw初始化
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本號
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本號
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
	//(寬,高,窗口名)返回一個GLFWwindow類的實例:window
	if (window == NULL)
	{
		// 生成錯誤則輸出錯誤信息
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);

	// 告訴GLFW我們希望每當窗口調整大小的時候調用改變窗口大小的函數
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	#pragma endregion GLFW

	#pragma region 函數指針
	// glad管理opengl函數指針,初始化glad
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		// 生成錯誤則輸出錯誤信息
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}
	#pragma endregion GLAD

	Shader ourShader("1.4.0vertex.txt", "1.4.0fragment.txt");

	#pragma region 頂點數據
	//頂點數據
	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    // 左上
	};
	unsigned int indices[] = {
		0, 1, 3, // first triangle
		1, 2, 3  // second triangle
	};
	#pragma endregion vertices[],indices[]

	#pragma region 緩存對象
	// 初始化緩存對象
	unsigned int VBO;
	glGenBuffers(1, &VBO);
	unsigned int VAO;
	glGenVertexArrays(1, &VAO);
	unsigned int EBO;
	glGenBuffers(1, &EBO);
	// 1. 綁定頂點數組對象
	glBindVertexArray(VAO);
	// 2. 把我們的頂點數組復制到一個頂點緩沖中,供OpenGL使用
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	// 3. 復制我們的索引數組到一個索引緩沖中,供OpenGL使用
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
	// 4. 設定頂點屬性指針
	// 位置屬性
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	// 顏色屬性
	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);
	#pragma endregion VAO,VBO,EBO

	#pragma region 材質
	// 加載材質
	unsigned int texture1;//紋理也是使用ID引用的
	glGenTextures(1, &texture1);//glGenTextures先輸入要生成紋理的數量,然后把它們儲存在第二個參數的`unsigned int`數組中
	glBindTexture(GL_TEXTURE_2D, texture1);
	// 為當前綁定的紋理對象設置環繞、過濾方式
	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;
	}
	stbi_image_free(data);
	#pragma endregion 加載材質

	#pragma region 渲染

	// 渲染循環
	while (!glfwWindowShouldClose(window))
	{
		// 輸入
		processInput(window);

		// 渲染指令
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);
		// 綁定材質
		glBindTexture(GL_TEXTURE_2D, texture1);
		// 渲染箱子
		ourShader.use();
		glBindVertexArray(VAO);
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

		// 檢查並調用事件,交換緩沖
		glfwSwapBuffers(window);

		// 檢查觸發什么事件,更新窗口狀態
		glfwPollEvents();
	}

	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	glDeleteBuffers(1, &EBO);

	// 釋放之前的分配的所有資源
	glfwTerminate();
	#pragma endregion Rendering
	
	return 0;
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	// 每當窗口改變大小,GLFW會調用這個函數並填充相應的參數供你處理
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window)
{
	// 返回這個按鍵是否正在被按下
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回鍵
		glfwSetWindowShouldClose(window, true);
}

我們還可以把得到的紋理顏色與頂點顏色混合,來獲得更有趣的效果

我們只需把紋理顏色與頂點顏色在片段着色器中相乘來混合二者的顏色:

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;

void main()
{
	FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
    //紋理顏色與頂點顏色相乘
}

不過效果有點emmmmm

紋理單元1.4.1

你可能會問為什么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
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D texture1;
uniform sampler2D texture2;

void main()
{
	//FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
	FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}

我們需要輸出顏色現在是兩個紋理的結合,這里用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采樣器對應着正確的紋理單元。你應該能得到下面的結果:

你可能注意到紋理上下顛倒了!這是因為OpenGL要求y軸0.0坐標是在圖片的底部的,但是圖片的y軸0.0坐標通常在頂部。很幸運,stb_image.h能夠在圖像加載時幫助我們翻轉y軸,只需要在加載任何圖像前加入以下語句即可:

stbi_set_flip_vertically_on_load(true);

在讓stb_image.h在加載圖片時翻轉y軸之后你就應該能夠獲得下面的結果了:

參考代碼:

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>

#include "shader.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

// callback
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

int main()
{

	#pragma region 窗口
	// 實例化GLFW窗口
	glfwInit();//glfw初始化
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本號
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本號
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
	//(寬,高,窗口名)返回一個GLFWwindow類的實例:window
	if (window == NULL)
	{
		// 生成錯誤則輸出錯誤信息
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);

	// 告訴GLFW我們希望每當窗口調整大小的時候調用改變窗口大小的函數
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
	#pragma endregion GLFW

	#pragma region 函數指針
	// glad管理opengl函數指針,初始化glad
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		// 生成錯誤則輸出錯誤信息
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}
	#pragma endregion GLAD

	Shader ourShader("1.4.1vertex.txt", "1.4.1fragment.txt");

	#pragma region 頂點數據
	//頂點數據
	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    // 左上
	};
	unsigned int indices[] = {
		0, 1, 3, // first triangle
		1, 2, 3  // second triangle
	};
	#pragma endregion vertices[],indices[]

	#pragma region 緩存對象
	// 初始化緩存對象
	unsigned int VBO;
	glGenBuffers(1, &VBO);
	unsigned int VAO;
	glGenVertexArrays(1, &VAO);
	unsigned int EBO;
	glGenBuffers(1, &EBO);
	// 1. 綁定頂點數組對象
	glBindVertexArray(VAO);
	// 2. 把我們的頂點數組復制到一個頂點緩沖中,供OpenGL使用
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	// 3. 復制我們的索引數組到一個索引緩沖中,供OpenGL使用
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
	// 4. 設定頂點屬性指針
	// 位置屬性
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	// 顏色屬性
	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);
	#pragma endregion VAO,VBO,EBO

	#pragma region 材質
	// 加載材質
	unsigned int texture1, texture2;//紋理也是使用ID引用的
	// texture1
	glGenTextures(1, &texture1);//glGenTextures先輸入要生成紋理的數量,然后把它們儲存在第二個參數的`unsigned int`數組中
	glBindTexture(GL_TEXTURE_2D, texture1);
	// 為當前綁定的紋理對象設置環繞、過濾方式
	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;
	stbi_set_flip_vertically_on_load(true);
	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;
	}
	stbi_image_free(data);
	// texture2
    glGenTextures(1, &texture2);
    glBindTexture(GL_TEXTURE_2D, texture2);
	// 為當前綁定的紋理對象設置環繞、過濾方式
    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);
	// 加載並生成紋理
	data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0);
	if (data)
	{
		// note that the awesomeface.png has transparency and thus an alpha channel, so make sure to tell OpenGL the data type is of GL_RGBA
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);
	}
	else
	{
		std::cout << "Failed to load texture" << std::endl;
	}
	stbi_image_free(data);
	#pragma endregion 加載材質

	#pragma region 渲染

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

	// 渲染循環
	while (!glfwWindowShouldClose(window))
	{
		// 輸入
		processInput(window);

		// 渲染指令
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);
		// 綁定材質
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, texture1);
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, texture2);
		// 渲染箱子
		ourShader.use();
		glBindVertexArray(VAO);
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

		// 檢查並調用事件,交換緩沖
		glfwSwapBuffers(window);

		// 檢查觸發什么事件,更新窗口狀態
		glfwPollEvents();
	}

	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	glDeleteBuffers(1, &EBO);

	// 釋放之前的分配的所有資源
	glfwTerminate();
	#pragma endregion Rendering
	
	return 0;
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	// 每當窗口改變大小,GLFW會調用這個函數並填充相應的參數供你處理
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window)
{
	// 返回這個按鍵是否正在被按下
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回鍵
		glfwSetWindowShouldClose(window, true);
}

擴展練習

  • 修改片段着色器,讓笑臉圖案朝另一個方向看

    #version 330 core
    out vec4 FragColor;
    
    in vec3 ourColor;
    in vec2 TexCoord;
    
    uniform sampler2D texture1;
    uniform sampler2D texture2;
    
    void main()
    {
    	//FragColor = texture(texture1, TexCoord) * vec4(ourColor, 1.0);
    	//FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
    	FragColor = mix(texture(texture1, TexCoord), 
    					texture(texture2, vec2(1.0 - TexCoord.x, TexCoord.y)), 0.2);
    }
    

  • 嘗試用不同的紋理環繞方式,設定一個從0.0f2.0f范圍內的(而不是原來的0.0f1.0f)紋理坐標,在箱子的角落放置4個笑臉(嘗試不同的紋理環繞方式)

  • 在矩形上只顯示紋理圖像的中間一部分,修改紋理坐標,達到能看見單個的像素的效果

    嘗試使用GL_NEAREST的紋理過濾方式讓像素顯示得更清晰

  • 使用一個uniform變量作為mix函數的第三個參數來改變兩個紋理可見度,使用上和下鍵來改變箱子或笑臉的可見度

    先新建一個float型變量mixValue

    然后改下我們的輸入

    void processInput(GLFWwindow *window)
    {
    	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回鍵
    		glfwSetWindowShouldClose(window, true);
    
    	if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS)
    	{
    		mixValue += 0.001f;
    		if (mixValue >= 1.0f)
    			mixValue = 1.0f;
    	}
    	if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS)
    	{
    		mixValue -= 0.001f;
    		if (mixValue <= 0.0f)
    			mixValue = 0.0f;
    	}
    }
    

    記得在渲染循環中加入:

    ourShader.setFloat("mixValue", mixValue);
    

    ok了,當然我們也可以用之前讀取時間來生成漸變效果的小把戲

    float timeValue = glfwGetTime();
    float mixValue = sin(timeValue) + 1.0f;
    ourShader.setFloat("mixValue", mixValue);
    

    fu5NeecNvD


免責聲明!

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



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