[OpenGL]OpenGL纹理

1.纹理

纹理是一个2D图片,甚至有可能是1D 和3D纹理,纹理是用来添加物体的细节,可以想象纹理是一张绘有砖块的纸,贴在一面白色的墙上,这样看起来墙就有砖块外表了。
纹理除了图像以外,纹理可以被用来存储大量的数据,这些数据可以发送到着色器上

纹理坐标在x和y轴上,范围为[0,1](用的2D纹理图像),使用纹理坐标获取纹理颜色叫做采样,纹理坐标起始于(0,0),也就是纹理图片的左下角,终于(1,1),即纹理图片的右上角。

假如,我们现在要把一张砖块的纸,贴到之前画的三角形上。。。
为了能够把纹理映射到三角形上,需要指定三角形的每个顶点各自对应纹理的哪个部分,这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明从纹理图像哪个部分采样,之后再图形的其他片段上进行插值。
[OpenGL]OpenGL纹理_第1张图片
如上图,我们为红色的三角形指定3个纹理坐标,

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

2.纹理环绕方式

纹理坐标的范围是从(0,0)到(0,1),那如果我们把纹理坐标设置在范围之外呢?OpenGL默认的行为是重复这个纹理图像
环绕类型:

(1)GL_REPEAT 对纹理的默认,重复纹理图像
[OpenGL]OpenGL纹理_第2张图片
(2)GL_MIRRORED_REPEAT 和GL_REPEAT一样,但是重复的图片是镜像放置的
[OpenGL]OpenGL纹理_第3张图片
(3)GL_CLAMP_TO_EDGE 纹理坐标会被余数在0和1之间,超出的部分会重复纹理坐标边缘,产生一种被拉伸效果
[OpenGL]OpenGL纹理_第4张图片
(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_NEARESTGL_LINEAR两种纹理过滤

纹理像素:当我们打开一张.jpg格式图片,不断放大我们就会发现图片是由无数像素点组成的,这些点就是纹理像素
纹理坐标是给模型顶点设置的数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色

GL_NEAREST(邻近过滤)是OpenGL默认的纹理过滤方式,当设置为邻近过滤的时候,OpenGL会选择中心点最接近纹理坐标的那个像素,如下图:加号代表纹理坐标,可以看到四个像素,左上角那个纹理像素的中心距离纹理坐标最近,所以会被选择为样本颜色
[OpenGL]OpenGL纹理_第5张图片

GL_LINEAR(线性过滤),线性过滤会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的颜色贡献越大

[OpenGL]OpenGL纹理_第6张图片

当我们进行放大和缩小操作的时候可以设置纹理过滤选项,在缩小的时候使用邻近过滤,被放大的时候使用线性过滤,我们需要使用glTexParameter*函数为放大和缩小指定过滤方式。

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

GL_LINEAR效果
[OpenGL]OpenGL纹理_第7张图片
GL_NEAREST效果
[OpenGL]OpenGL纹理_第8张图片

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);

[OpenGL]OpenGL纹理_第9张图片

扩展:

我们在片元着色器中对最终颜色进行纹理和图像进行混合:
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);

[OpenGL]OpenGL纹理_第10张图片

代码如下:

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

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,这在当我们需要循环一些纹理单元的时候会很有用。

你可能感兴趣的:([OpenGL])