紋理 :
紋理也是頂點數據的一種. 紋理的添加也是處理VAO
配置紋理的操作方式
使用glTexParamete* 函數對單獨的一個坐標軸設置 (s, t (如果使用3D紋理, 那么還有一個r), 它們和x,y,z是等價的)
glTexParameteri(GLenum target, GLenum pname, GLint param)
第一個參數target : 指定了紋理目標, 我們使用的是2D的內容, 所以這里使用的是GL_TEXTURE_2D
第二個參數pname : 需要我們指定設置的選項與應用的紋理軸. 我們打算配置的是WRAP,并且指定s和t.
第三個參數param : 選擇紋理環(huán)繞方式.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,GL_MIRRORED_REPEAT);

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

如果我們選擇GL_CLAMP_TO_BOARD, 我們需要指定一個邊緣顏色
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, {1.0f,1.0f, 0.0f, 1.0f} );
紋理過濾
紋理過濾很多選項,這里討論重要的兩種 : GL_NEAREST 和 GL_LINEAR
鄰近過濾
GL_NEAREST 是OpenGL默認的紋理過濾方式.當設置為GL_NEAREST的時候 : OpenGL會選擇中心點最接近紋理坐標的那個像素.

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

GL_NEAREST 會產生顆粒感.
GL_LINEAR 會羽化.產生更平滑圖案.

當進行放大和縮小操作的時候, 可以設置紋理過濾的選項, 比如
放大時 , 選擇GL_LINEAR
縮小時 , 選擇GL_NEAREST
通過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會使用不同的多級漸遠紋理,即最適合物體的距離的那個。 由于距離遠,解析度不高也不會被用戶注意到。 同時,多級漸遠紋理另一加分之處是它的性能非常好。讓我們看一下多級漸遠紋理是什么樣子的:
它簡單來說就是一系列的紋理圖像,后一個紋理圖像是前一個的二分之一。

手工為每個紋理圖像創(chuàng)建一系列多級漸遠紋理很麻煩,幸好OpenGL有一個glGenerateMipmap函數,
在創(chuàng)建完一個紋理后調用它OpenGL就會承擔接下來的所有工作了。后面的教程中你會看到該如何使用它。
glGenerateMipmap(GLenum target) 只有一個參數. 過濾方式
| GL_NEAREST_MIPMAP_NEAREST | 使用最鄰近的多級漸遠紋理來匹配像素大小,并使用鄰近插值進行紋理采樣 |
| GL_LINEAR_MIPMAP_NEAREST | 使用最鄰近的多級漸遠紋理級別,并使用線性插值進行采樣 |
| GL_NEAREST_MIPMAP_LINEAR | 在兩個最匹配像素大小的多級漸遠紋理之間進行線性插值,使用鄰近插值進行采樣 |
| GL_LINEAR_MIPMAP_LINEAR | 在兩個鄰近的多級漸遠紋理之間使用線性插值,并使用線性插值進行采樣 |
glGenerateMipmap(GL_NEAREST_MIPMAP_NEAREST);
一個常見的錯誤是,將放大過濾的選項設置為多級漸遠紋理過濾選項之一。這樣沒有任何效果,因為多級漸遠紋理主要是使用在紋理被縮小的情況下的:紋理放大不會使用多級漸遠紋理,為放大過濾設置多級漸遠紋理的選項會產生一個GL_INVALID_ENUM錯誤代碼。
紋理過濾加載與創(chuàng)建紋理
創(chuàng)建紋理之前解決一個問題, 就是怎么把圖片資源轉成紋理數據傳給OpenGL
STB
先添加一個庫 stb ,
把對應的源文件加載到項目中

這里使用這個stbi_load 加載在項目中的紋理資源
int width, height, nrChannels;
unsigned char *data = stbi_load("本地的紋理圖片資源地址", &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);
stbi_load 第一個參數是獲取文件地址, 這里直接去一個本地的地址就好, 也可以根據自己的方式把圖片資源路徑設置成在項目里面的.
值得注意 :
stb_image.h 使用前需要 #define STB_IMAGE_IMPLEMENTATION
Do this:
#define STB_IMAGE_IMPLEMENTATION
before you include this file in *one* C or C++ file to create the implementation.
在#include "stb_image.h"中的函數都是STBIDEF開頭. 這里定義了兩種方式, static或extern.
#ifndef STBIDEF
#ifdef STB_IMAGE_STATIC
#define STBIDEF static
#else
#define STBIDEF extern
#endif
#endif
如果在多個文件中導入了stb_image.h的頭文件. 需要STB_IMAGE_STATIC, 不然可能有編譯不通過的情況.
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_STATIC
#include "stb_image.h"
關于SOIL
SOIL是簡易OpenGL圖像庫(Simple OpenGL Image Library)的縮寫,它支持大多數流行的圖像格式,使用起來也很簡單,你可以從他們的主頁下載。像其它庫一樣,你必須自己生成.lib。你可以使用/projects文件夾內的任意一個解決方案(Solution)文件(不用擔心他們的Visual Studio版本太老,你可以把它們轉變?yōu)樾碌陌姹?,這一般是沒問題的。譯注:用VS2010的時候,你要用VC8而不是VC9的解決方案,想必更高版本的情況亦是如此)來生成你自己的.lib文件。你還要添加src文件夾里面的文件到你的includes文件夾;對了,不要忘記添加SOIL.lib到你的鏈接器選項,并在你代碼文件的開頭加上#include <SOIL.h>。
PS : 在LearnOpenGL里面介紹添加一個SOIL的庫, 并沒有很好的介紹怎么配置在項目中 , 這里配置的方式好像不是特別的方便, 所以我這里加載紋理的數據使用上面的stb文件內容. 相當于源文件吧.
下面的教程中,我們會使用一張木箱的圖片。要使用SOIL加載圖片,我們需要使用它的SOIL_load_image函數:
int width, height;
unsigned char* image = SOIL_load_image("container.jpg", &width, &height, 0, SOIL_LOAD_RGB);
函數首先需要輸入圖片文件的路徑。然后需要兩個int指針作為第二個和第三個參數,SOIL會分別返回圖片的寬度和高度到其中。后面我們在生成紋理的時候會用圖像的寬度和高度。第四個參數指定圖片的通道(Channel)數量,但是這里我們只需留為0。最后一個參數告訴SOIL如何來加載圖片:我們只關注圖片的RGB值。結果會儲存為一個很大的char/byte數組。
注意 : 雖然代碼中用的是stbi_load函數, 但是實現的效果是一樣的
實踐
頂點數據
MyTexturesVertices.hpp
#ifndef MyTexturesVertices_h
#define MyTexturesVertices_h
//紋理坐標 : 以左下角 (0.0, 0.0) 為原點
static float MyTextureVertices[] = {
// ---- 位置 ---- ---- 顏色 ---- - 紋理坐標 -
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 // 左上
};
//索引
static unsigned int MyTextureVerticesIndices[] = { // 注意索引從0開始!
0, 1, 2, // 第一個三角形
0, 3, 2 // 第二個三角形
};
#endif /* MyTexturesVertices_h */
由于我們添加了一個額外的頂點屬性,我們必須告訴OpenGL我們新的頂點格式:

頂點數據和之前的有點不一樣, 所以這里需要調整一下Shader程序
Shader程序
MyTexturesShader.hpp
#ifndef MyTexturesShader_h
#define MyTexturesShader_h
#define STRINGIZE(x) #x
#define SHADER(shader) STRINGIZE(shader)
/// 著色器程序之間的數據傳遞
static char *MyTextureVertexShaderStr = SHADER(
\#version 330 core\n
layout (location = 0) in vec3 position; //頂點數據源輸入
layout (location = 1) in vec3 color; //顏色數據源輸入
layout (location = 2) in vec2 texCoords; //紋理數據源輸入(2D)
out vec2 vertexTexCoords;//紋理輸出
out vec4 vertexColor;//顏色輸出
void main()
{
gl_Position = vec4(position, 1.0f);//坐標位置
vertexColor = vec4(color, 1.0f); //輸出給片元著色器
vertexTexCoords = texCoords;
}
);
//片元著色器程序
static char *MyTextureFragmentShaderSrc = SHADER(
\#version 330 core\n
in vec2 vertexTexCoords;//紋理輸入(從頂點)
in vec4 vertexColor;//顏色輸入(從頂點)
uniform sampler2D myTexture;//全局myTexture
out vec4 color;//顏色輸出
out vec4 FragColor;//紋理輸出
void main()
{
color = vertexColor;
FragColor = texture(myTexture, vertexTexCoords);
}
);
#endif /* MyTexturesShader_h */
因為需要加載紋理, 在頂點著色器Shader程序中添加紋理的輸入輸出
layout (location = 2) in vec2 texCoords; //紋理數據源輸入(2D)
out vec2 vertexTexCoords;//紋理輸出
main {
vertexTexCoords = texCoords; //
}
在紋理著色器SHader程序中也需要添加處理
in vec2 vertexTexCoords;//紋理輸入(從頂點)
uniform sampler2D myTexture;//全局myTexture
out vec4 FragColor;//紋理輸出
main {
FragColor = texture(myTexture, vertexTexCoords);
}
注意 : uniform sampler2D myTexture; 這里創(chuàng)建了一個uniform修飾的myTexture , 暫時先忽略. 窗口程序中暫時沒有用到
窗口程序
MyTextures.cpp
#include <iostream>
#include "MyTextures.hpp"
#include "MyProgram.hpp"
#include "MyTexturesVertices.hpp"
#include "MyTexturesShader.hpp"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
int runMyTextureOpenGlWindow() {
int result = glfwInit();
if (result == GL_FALSE) {
printf("glfwInit 初始化失敗");
return -1;
}
//這里的宏不好提示出來, 根據LearnOpenGL的文檔提示, 用這三個
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
//創(chuàng)建一個Window
GLFWwindow *window = glfwCreateWindow(600, 400, "My Opengl Window", NULL, NULL);
if(!window) {
printf("window 創(chuàng)建失敗");
}
glfwMakeContextCurrent(window);
gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);
//----------------------------------------------------------------------
MyProgram myProgram = MyProgram(MyTextureVertexShaderStr, MyTextureFragmentShaderSrc);
GLuint VBO , VAO , EBO;
unsigned int squareIndicesCount = 0;
//創(chuàng)建VBO
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(MyTextureVertices), MyTextureVertices, GL_STATIC_DRAW);
//創(chuàng)建VAO
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
//配置紋理的VAO
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
//創(chuàng)建EBO, 這里的EBO相當于索引的作用
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(MyTextureVerticesIndices), MyTextureVerticesIndices, GL_STATIC_DRAW);
//解綁VAO
glBindVertexArray(0);
squareIndicesCount = sizeof(MyTextureVerticesIndices)/sizeof(MyTextureVerticesIndices[0]);
//生成紋理
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 為當前綁定的紋理對象設置環(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("/Users/lumi/Desktop/LearnOpengl/LearnOpenGl/LearnOpenGl/Common/Sources/dizhuan.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);
//進行繪制
while(!glfwWindowShouldClose(window)){
//檢查事件
glfwPollEvents();
//渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(myProgram.program);
glBindVertexArray(VAO);
glBindTexture(GL_TEXTURE_2D, texture);//加載紋理
glDrawElements(GL_TRIANGLES, squareIndicesCount, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
//交換緩沖
glfwSwapBuffers(window);
}
//程序銷毀
glfwTerminate();
return 1;
}
最終效果 :

更多效果
因為著色器程序本身有顏色. 顏色跟紋理做一個混合 , 修改片元著色器程序
FragColor = texture(myTexture, vertexTexCoords) * color;

PS : 留個思考怎么做一些前面介紹的效果的處理.
- 紋理過濾
- 紋理的環(huán)繞方式
紋理單元
你可能會奇怪為什么sampler2D變量是個uniform,我們卻不用glUniform給它賦值。使用glUniform1i,我們可以給紋理采樣器分配一個位置值,這樣的話我們能夠在一個片段著色器中設置多個紋理。一個紋理的位置值通常稱為一個紋理單元(Texture Unit)。一個紋理的默認紋理單元是0,它是默認的激活紋理單元,所以教程前面部分我們沒有分配一個位置值。
紋理單元的主要目的是讓我們在著色器中可以使用多于一個的紋理。通過把紋理單元賦值給采樣器,我們可以一次綁定多個紋理,只要我們首先激活對應的紋理單元。就像glBindTexture一樣,我們可以使用glActiveTexture激活紋理單元,傳入我們需要使用的紋理單元:
glActiveTexture(GL_TEXTURE0); //在綁定紋理之前先激活紋理單元
glBindTexture(GL_TEXTURE_2D, texture);
激活紋理單元之后,接下來的glBindTexture函數調用會綁定這個紋理到當前激活的紋理單元,紋理單元GL_TEXTURE0默認總是被激活,所以我們在前面的例子里當我們使用glBindTexture的時候,無需激活任何紋理單元
修改著色器程序
//片元著色器程序
static char *MyTextureFragmentShaderSrc = SHADER(
\#version 330 core\n
in vec2 vertexTexCoords;//從頂點著色器中拿紋理的值
in vec4 vertexColor;//從頂點著色器中拿color的值
uniform sampler2D myTexture0;//全局myTexture
uniform sampler2D myTexture1;//全局myTexture
out vec4 color;
out vec4 FragColor;
void main()
{
FragColor = mix(texture(myTexture0, vertexTexCoords), texture(myTexture1, vertexTexCoords), 0.5);
}
);
修改窗口程序
//生成紋理0
unsigned int texture0, texture1;
unsigned char *data0 ;
int width0, height0, nrChannels0;
glGenTextures(1, &texture0);
glBindTexture(GL_TEXTURE_2D, texture0);
// 為當前綁定的紋理對象設置環(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);
data0 = stbi_load("/Users/lumi/Desktop/LearnOpengl/LearnOpenGl/LearnOpenGl/Common/Sources/dizhuan.jpg", &width0, &height0, &nrChannels0, 0);
if (data0)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width0, height0, 0, GL_RGB, GL_UNSIGNED_BYTE, data0);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data0);
//生成紋理1
unsigned char *data1 ;
int width1, height1, nrChannels1;
glGenTextures(1, &texture1);
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);
data1 = stbi_load("/Users/lumi/Desktop/LearnOpengl/LearnOpenGl/LearnOpenGl/Common/Sources/smile.png", &width1, &height1, &nrChannels1, 0);
if (data1)
{
//這里用了PNG, 有透明度, 使用GL_RGBA.
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width1, height1, 0, GL_RGBA, GL_UNSIGNED_BYTE, data1);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data1);
//進行繪制
while(!glfwWindowShouldClose(window)){
//檢查事件
glfwPollEvents();
//渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//激活紋理0
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture0);
glUniform1i(glGetUniformLocation(myProgram.program, "myTexture0"), 0);
//激活紋理1
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(glGetUniformLocation(myProgram.program, "myTexture1"), 1);
glUseProgram(myProgram.program);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, squareIndicesCount, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
//交換緩沖
glfwSwapBuffers(window);
}

最后, 重疊的效果有點奇怪. 先這樣吧, 先理解這個紋理單元的概念.
注意找的素材的格式是PNG還是JPG, PNG有Alpha通道
這里還是有很多需要弄明白的細節(jié), 比如著色器程序的texture() 和mix()
體會這里的Uniform和之前的Uniform的區(qū)別
PS : 思考, 怎么改變這個紋理貼圖的大小.
PS : 思考, 實現紋理題圖大小的改變之后, 怎么設置紋理過濾和紋理環(huán)繞方式