纹理是一个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 // 上中
};
纹理坐标的范围是从(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);
纹理坐标不依赖于分辨率,可以是任意浮点值,所以 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);
当我们处在一个房间中,这个房间包含着上千的物品,每个物品都有纹理,有些物体会很远,但是纹理拥有与近处物品同样高的分辨率,远处的物体可能只产生很少的片段,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);
在使用纹理之前,我们必须把纹理加载进应用中,正常有两种解决方式:
通过定义STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,会让其只包含相关的函数定义源码
#define STB_IMAGE_IMPLEMENTATION
#include"stb_image.h"
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
unsigned int texture;
glGenTextures(1, &texture);
glGenTextures函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的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);
第一个参数:纹理目标,设置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
#include
#include
#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();
}
在着色器中,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,这在当我们需要循环一些纹理单元的时候会很有用。