紋理(Texture)是一個 2D 的圖片,可以用來增添物體的細節,可以想像 Texture 就像是個有圖案的紙,貼合在 3D 的物體上,這樣就可以讓物體不用增加頂點就增加細節。
題外話,Texture 除了圖像之外還能用來儲存大量資訊發送到 Shader 上。
上個文章中的三角形貼上一個磚頭的 Texture:
為了將紋理(Texture)映射(Map)到頂點上,需要指定頂點分邊對應到紋理的哪個部分,所以每個頂點都會關聯著一個紋理座標(Texture Coordinate),用來表示該從 Texture 的哪個地方採樣(Sampling)(fragment 的顏色),之後其他的 Fragment 會插值其 Texture Coordinate。
我們希望三角形的左下角對應 Texture 的左下角,因此我們將左下角頂點的 Texture 座標設置為 $(0,0)$ 上方設置為 $(0.5, 1)$ 右下設置為 $(1, 0)$。只要給定這三個座標就可以了,接下來他們會被傳入到 Fragment Shader 中進行處裡。
float texCoords[] = {
0.0f, 0.0f, // bottom left
1.0f, 0.0f, // bottom right
0.5f, 1.0f // middle
};
float vertices[] = {
// 位置
0.5f, -0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
座標的範圍是$[0, 1]$(第一象限),如果超出這個座標之外預設的行為是重複,但其實 OpenGL 提供了更多的選擇:
GL_REPEAT
GL_MIRRORED_REPEAT
GL_CLAMP_TO_EDGE
GL_CLAMP_TO_BORDER
這些選項都可以對單獨的材質座標軸設定,s, t 和 r 軸(如果是 3D 的話)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_MIRRORED_REPEAT);
GL_CLAMP_TO_BORDER
則還需要指定一個顏色紋理座標跟解析度無關,所以 OpenGL 要知道怎麼將紋理像素(Texture Pixel, Texel)映射到紋理座標(Texture Coordinate)。
如果一個很大的物體,可是紋理解析度很低,就會有明顯的瑕疵出現。
OpenGL 有許多紋理過濾的選項,但是現在我們只討論重要的兩個:
GL_NEAREST
(Nearest Neighbor Filtering)
GL_LINEAR
((Bi)linear Filtering)
比較遺下兩種方法吧:
GL_NEAREST
產生了顆粒狀的圖案,我們能夠清晰看到組成紋理的像素,而 GL_LINEAR
能夠產生更平滑的圖案,很難看出單個的紋理像素
假如說場景在開闊的地方,有上千個物體,每個物體上都有紋理,近處的物體的紋理解析度很高,但遠處的物體可能只產生很小的片段(Fragment),在高解析度的紋理中這種物體的顏色通常都不正確,並且也浪費記憶體。
多級漸遠紋理(Mipmap)是一系列的紋理圖像,後一個的大小是前個的二分之一。當觀察者的距離超過一定的閾值(threshold),自動切換至適合該距離的紋理。距離遠用解析度比較小的,也不容易被察覺。
Mipmap 可以自己手動產生,不過 OpenGL 可以幫我們自動產生。
_MIPMAP_NEAREST
_MIPMAP_LINEAR
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
:::info
放大過濾不可以使用 Mipmap ,因為 Mipmap 是給縮小紋理使用的
:::
前面我們已經知道 SFML
可以用來載入圖片了,所以這裡我們就不用其他方法來載入圖片了
// 載入圖片
sf::Image wall;
wall.loadFromFile("../src/1.getting_started/4.1.texture/wall.jpg");
wall.flipVertically();
// 設定材質及參數
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, wall.getSize().x, wall.getSize().y, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)wall.getPixelsPtr());
glGenerateMipmap(GL_TEXTURE_2D);
和之前 OpenGL Texture 一樣,要產生獨一無二的 ID
uint32_t texture;
glGenTexture(1, &texture);
接著需要綁定他
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);
完整的流程大概長這樣
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);
//
sf::Image wall;
wall.loadFromFile("wall.jpg");
wall.flipVertically();
uint32_t width = wall.getSize().x;
uint32_t height = wall.getSize().y;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
這面我們會使用 glDrawElements
繪製前面 Hello Traingle
中後面教的矩形。我們要告訴 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 新的頂點格式。
uint32_t aPos = glGetAttribLocation(program, "aPos");
glVertexAttribPointer(aPos, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)0);
glEnableVertexAttribArray(aPos);
uint32_t aVertColor = glGetAttribLocation(program, "aVertColor");
glVertexAttribPointer(aVertColor, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(3 * sizeof(float)));
glEnableVertexAttribArray(aVertColor);
uint32_t aTexCoord = glGetAttribLocation(program, "aTexCoord");
glVertexAttribPointer(aTexCoord, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6*sizeof(float)));
glEnableVertexAttribArray(aTexCoord);
接著調整 Shader
Vertex Shader
#version 450 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aVertColor;
layout (location = 2) in vec2 aTexCoord;
out vec3 VertColor;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
TexCoord = aTexCoord;
VertColor = aVertColor;
}
Fragment Shader
Sampler
Sampler1D
, Sampler2D
, Sampler3D
#version 450 core
out vec4 FragColor;
in vec2 TexCoord;
in vec3 VertColor;
uniform sampler2D texture1;
uniform vec4 fColor;
void main()
{
FragColor = texture(texture1, TexCoord) * fColor;
}
GLSL 內建 texture()
函數來採樣,第一個參數是採樣的紋理,第二個參數是紋理座標,輸出去紋理座標(插值後)並且經過 Filter 後的顏色。
之後使用之前記得綁定就好了。
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
在 Fragment Shader 中如果想要使用多個紋理,則要給紋理一個位置值,稱作紋理單元(Texture Unit)。
但是在上個範例中並沒有用 glUniform 給值(因為預設是 0)
glActiveTexture(GL_TEXTURE0); // 啟用第 0 號 texture
glBindTexture(GL_TEXTURE_2D, texture); // 將紋理綁定上 GL_TEXTURE0
OpenGL 保證有 16 個紋理單元可以使用,且紋理單元的編號是連續的,意味著可以用 GL_TEXTURE0 + n
來存取(也可以直接用 GL_TEXTUREn
)
例子:混和兩張紋理
#version 450 core
uniform sampler2D tex1;
uniform sampler2D tex2;
void main()
{
FragColor = mix(texture(tex1, TexCoord), texture(tex2, TexCoord), 0.2);
}
mix(a, b, alpha)
混和(線性插值)
在開始渲染之前要設定 texture 的編號
tex1
, tex2
才會參考到綁定在紋理單元的 TextureglUseProgram(program);
glUniform1i(glGetUniformLocation(program, "tex1"), 0);
glUniform1i(glGetUniformLocation(program, "tex2"), 1);
渲染流程會變成:
/* Create, Load Texture */
/* ... */
glUseProgram(program);
// Set Texture units
glUniform1i(glGetUniformLocation(program, "tex1"), 0);
glUniform1i(glGetUniformLocation(program, "tex2"), 1);
while(...)
{
/* ... */
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);
}
那成功的話大概會長這樣
完整 code 請看 這裡
根據時間改變 mix
https://learnopengl.com/Getting-started/Textures