1.紋理
紋理是一個2D圖片,甚至有可能是1D 和3D紋理,紋理是用來添加物體的細節,可以想象紋理是一張繪有磚塊的紙,貼在一面白色的牆上,這樣看起來牆就有磚塊外表了。
紋理除了圖像以外,紋理可以被用來存儲大量的數據,這些數據可以發送到着色器上
紋理座標在x和y軸上,範圍爲[0,1](用的2D紋理圖像),使用紋理座標獲取紋理顏色叫做採樣,紋理座標起始於(0,0),也就是紋理圖片的左下角,終於(1,1),即紋理圖片的右上角。
假如,我們現在要把一張磚塊的紙,貼到之前畫的三角形上。。。
爲了能夠把紋理映射到三角形上,需要指定三角形的每個頂點各自對應紋理的哪個部分,這樣每個頂點就會關聯着一個紋理座標(Texture Coordinate),用來標明從紋理圖像哪個部分採樣,之後再圖形的其他片段上進行插值。
如上圖,我們爲紅色的三角形指定3個紋理座標,
float texCoords[] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f // 上中
};
2.紋理環繞方式
紋理座標的範圍是從(0,0)到(0,1),那如果我們把紋理座標設置在範圍之外呢?OpenGL默認的行爲是重複這個紋理圖像
環繞類型:
(1)GL_REPEAT 對紋理的默認,重複紋理圖像
(2)GL_MIRRORED_REPEAT 和GL_REPEAT一樣,但是重複的圖片是鏡像放置的
(3)GL_CLAMP_TO_EDGE 紋理座標會被餘數在0和1之間,超出的部分會重複紋理座標邊緣,產生一種被拉伸效果
(4)GL_CLAMP_TO_BORDER 超出的座標爲用戶指定的邊緣顏色
使用glTexParameter 函數對單獨的一個座標軸設置(s,t,r)*
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 第二個參數指定設置的選項與應用的紋理軸,並且指定s和t軸
第三個參數紋理環繞方式
如果我們選擇GL_CLAMP_TO_BORDER,還需要指定一個邊緣顏色,使用glTexParameterfv,
使用GL_TEXTURE_BORDER_COLOR
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
3.紋理過濾
紋理座標不依賴於分辨率,可以是任意浮點值,所以 OpenGL需要知道怎樣將紋理像素映射到紋理座標?
我們主要對OpenGL的GL_NEAREST和GL_LINEAR兩種紋理過濾
紋理像素:當我們打開一張.jpg格式圖片,不斷放大我們就會發現圖片是由無數像素點組成的,這些點就是紋理像素
紋理座標是給模型頂點設置的數組,OpenGL以這個頂點的紋理座標數據去查找紋理圖像上的像素,然後進行採樣提取紋理像素的顏色
GL_NEAREST(鄰近過濾)是OpenGL默認的紋理過濾方式,當設置爲鄰近過濾的時候,OpenGL會選擇中心點最接近紋理座標的那個像素,如下圖:加號代表紋理座標,可以看到四個像素,左上角那個紋理像素的中心距離紋理座標最近,所以會被選擇爲樣本顏色
GL_LINEAR(線性過濾),線性過濾會基於紋理座標附近的紋理像素,計算出一個插值,近似出這些紋理像素之間的顏色。一個紋理像素的中心距離紋理座標越近,那麼這個紋理像素的顏色對最終的顏色貢獻越大
當我們進行放大和縮小操作的時候可以設置紋理過濾選項,在縮小的時候使用鄰近過濾,被放大的時候使用線性過濾,我們需要使用glTexParameter*函數爲放大和縮小指定過濾方式。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
GL_LINEAR效果
GL_NEAREST效果
4.多級漸遠紋理
當我們處在一個房間中,這個房間包含着上千的物品,每個物品都有紋理,有些物體會很遠,但是紋理擁有與近處物品同樣高的分辨率,遠處的物體可能只產生很少的片段,OpenGL想從高分辨率紋理中爲這些片段獲取正確的顏色就很困難,會使小物體產生不真實的感覺。。。
另外,使用高分辨率的紋理浪費內存
OpenGL使用多級漸遠紋理來解決,簡單來說就是一系列的紋理圖像,後一個紋理圖像是前一個的二分之一
理念:距離觀察者的距離超過一定的閾值,OpenGL會使用不同的多級漸遠紋理,即最適合物體的距離的那個
OpenGL中用glGenerateMipmaps函數,在創建完一個紋理後調用,接下去的工作OpenGL會承擔
注意:在渲染中切換多級漸遠紋理的時候,OpenGL在兩個不同級別的多級漸遠紋理之間會產生不真實的便捷,可以在不同的多級漸遠紋理級別之間使用NEAREST和LINEAR過濾。
GL_NEAREST_MIPMAP_NEAREST:使用最鄰近的多級漸遠紋理來匹配像素大小,並使用鄰近插值進行紋理採樣
GL_LINEAR_MIPMAP_NEAREST:使用最鄰近的多級漸遠紋理級別,並使用線性插值進行採樣
GL_NEAREST_MIPMAP_LINEAR:在兩個最匹配像素大小的多級漸遠紋理之間進行線性插值,使用鄰近插值進行採樣
GL_LINEAR_MIPMAP_LINEAR:在兩個鄰近的多級漸遠紋理之間使用線性插值,並使用線性插值進行採樣
我們可以用glTexParameter*函數將過濾方式設置
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
5.加載與創建紋理
在使用紋理之前,我們必須把紋理加載進應用中,正常有兩種解決方式:
①選擇一個需要的文件格式,比如.png或者.jpeg,然後針對單個格式的圖像,寫一個加載器,把圖像轉化成字節序列,但是圖像文件格式這麼多,如果每個格式都需要寫一個加載器,那就太累了
②使用一個支持多種流行格式的圖像加載庫來解決,可以使用stb_image.h庫,可以從這下載:https://github.com/nothings/stb/blob/master/stb_image.h 當然,還有另一種方法,在我們的工程頭文件裏,添加一個新的C++的頭文件,命名爲,stb_image,將stb_image.h的代碼複製到其中, #include “stb_image.h”
通過定義STB_IMAGE_IMPLEMENTATION,預處理器會修改頭文件,會讓其只包含相關的函數定義源碼
#define STB_IMAGE_IMPLEMENTATION
#include"stb_image.h"
③用stbi_load函數加載圖像
GLint width, height, nrChannels;
unsigned char *data = stbi_load("resource/6789.jpg", &width, &height, &nrChannels, STBI_rgb);
加載一張表情的紋理
第一個參數:圖像文件的位置
第二個參數:寬度
第三個參數:高度
第四個參數:顏色通道的個數
第五個參數:加載模式,STBI_rgb爲RGB,STBI_rgb_alpha爲RGBA,也可以爲0
④生成紋理,跟之前OpenGL創建對象一樣,使用ID引用
unsigned int texture;
glGenTextures(1, &texture);
glGenTextures函數首先需要輸入生成紋理的數量,然後把它們儲存在第二個參數的unsigned int數組中
⑤綁定紋理
glBindTexture(GL_TEXTURE_2D, texture);
⑥glTexImage2D用生成紋理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
第一個參數:紋理目標,設置GL_TEXTURE_2D意味着會生成與當前綁定的紋理對象在同一個目標上的紋理
第二個參數:紋理指定多級漸遠紋理的級別,我們填0,就是基本級別 第三個參數:告訴OpEnGLish,把紋理存儲爲哪種格式
第四,第五個參數:紋理的寬度和高度 第六個參數:一直都是0
第七個,第八個參數:源圖的格式和數據類型,我們使用RGB加載圖像,存儲爲char(byte)數組 第九個參數:真正的圖像數據
當調用glTexImage2D,當前綁定的紋理對象就會被附加上紋理圖像,如果我們要使用多級漸遠紋理,我們必須手動設置所有不同的圖像,就是不斷增加第二個參數,或者,我們直接調用glGenerateMipmap,會爲當前綁定的紋理自動生成所有需要的多級漸遠紋理
⑦釋放圖像
stbi_image_free(data);
注:我們剛開始加載圖像的時候,會出現紋理倒立的情況,這是因爲OpenGL要求y軸0.0座標是在圖片的底部的,但是圖片的y軸0.0座標通常在頂部
我們可以在加載圖像之前,加入語句:
stbi_set_flip_vertically_on_load(true);
效果如下:
片元着色器計算最終顏色:
FragColor = texture(ourTexture, TexCoord);
擴展:
我們在片元着色器中對最終顏色進行紋理和圖像進行混合:
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
代碼如下:
#include <GL/glew.h>
#include<gl\glut.h>
#include<iostream>
#define STB_IMAGE_IMPLEMENTATION
#include"stb_image.h"
void MyInit();
void reshape(int w, int h);
void display();
GLuint vboId;
GLuint vaoId;
unsigned int texture;
int main(int argc, char **argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
glutInitWindowPosition(100, 100);
glutInitWindowSize(512, 512);
glutCreateWindow("SHADER_RECTANGLE");
glewInit();
MyInit();
glutReshapeFunc(&reshape);
glutDisplayFunc(&display);
glutMainLoop();
return 0;
}
void MyInit()
{
glClearColor(0.0,0.0, 0.0,0.0);
const GLfloat vertices[] = {
// ---- 位置 ---- ---- 顏色 ---- - 紋理座標 -
0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 1.0f, // 左上
-0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, // 左下
0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, // 右上
};
//創建VAO對象
glGenVertexArrays(1, &vaoId);
glBindVertexArray(vaoId);
//創建VBO對象
glGenBuffers(1, &vboId);
glBindBuffer(GL_ARRAY_BUFFER, vboId);
//創建紋理
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
//傳入VBO數據
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//解除VBO綁定
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
void reshape(int w, int h)
{
glViewport(0, 0,(GLsizei)w, (GLsizei)h);
}
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
const char* vertex_shader =
"#version 330\n"
"layout (location = 0) in vec3 aPos;"// 位置變量的屬性位置值爲 0
"layout (location = 1) in vec3 aColor; "// 顏色變量的屬性位置值爲 1
"layout (location = 2) in vec2 aTexCoord; "// 紋理變量的屬性位置值爲2
"out vec3 OurColor;"//向片段着色器輸出一個顏色
"out vec2 TexCoord;"//向片段着色器輸出
"void main() {"
" gl_Position = vec4(aPos, 1.0);"
"OurColor = aColor;"
"TexCoord = aTexCoord;"
"}";
const char* fragment_shader =
"#version 330\n"
"out vec4 FragColor;"
"in vec3 OurColor;"
"in vec2 TexCoord;"
"uniform sampler2D ourTexture;"
"void main() {"
" FragColor = texture(ourTexture, TexCoord);" //片元着色器最終顏色
"}";
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertex_shader, NULL);
glCompileShader(vertexShader);
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragment_shader, NULL);
glCompileShader(fragmentShader);
GLuint shader_programme = glCreateProgram();
glAttachShader(shader_programme, fragmentShader);
glAttachShader(shader_programme, vertexShader);
glLinkProgram(shader_programme);
glUseProgram(shader_programme);
glDeleteShader(fragmentShader);
glDeleteShader(vertexShader);
//綁定VBO
glBindBuffer(GL_ARRAY_BUFFER, vboId);
//解釋頂點數據方式
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8* sizeof(float), 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);
//倒立圖像處理
stbi_set_flip_vertically_on_load(true);
//加載圖像
GLint width, height, nrChannels;
unsigned char *data = stbi_load("resource/112233.jpg", &width, &height, &nrChannels, STBI_rgb);
//生成紋理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
//釋放紋理
stbi_image_free(data);
//繪製模型
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glDisableVertexAttribArray(2);
glutSwapBuffers();
}
6.紋理單元
在着色器中,sampler2D變量是一個uniform,我們可以用glUniform賦值,給紋理採樣器分配一個位置。一個紋理的位置值,稱爲紋理單元,一個紋理的默認紋理單元爲0,是默認的激活紋理單元,所以我們之前我們沒有分配一個位置值
紋理單元的主要目的是讓我們在着色器中可以使用多於一個的紋理。通過把紋理單元賦值給採樣器,我們可以一次綁定多個紋理,只要我們首先激活對應的紋理單元。就像glBindTexture一樣,我們可以使用glActiveTexture激活紋理單元,傳入我們需要使用的紋理單元。。
glActiveTexture(GL_TEXTURE0); // 在綁定紋理之前先激活紋理單元
glBindTexture(GL_TEXTURE_2D, texture);
注:
OpenGL至少保證有16個紋理單元供我們使用,也就是說我們可以激活從GL_TEXTURE0到GL_TEXTRUE15。它們都是按順序定義的,所以我們也可以通過GL_TEXTURE0 + 8的方式獲得GL_TEXTURE8,這在當我們需要循環一些紋理單元的時候會很有用。