OpenGL入门——纹理
已知,我们是对每个顶点去添加对应颜色,如果想要图像更加细节真实,就必须有足够多的点,点的密度要很大,而每个点都有个颜色属性,这样很浪费资源。
这时候我们就可以引入纹理,它可以用来添加图像的细节,类似于皮肤。每个顶点对应一个纹理坐标(表明从纹理图像的哪个位置采样,即获得颜色),其他片段进行插值采样(非顶点位置的颜色也是从纹理图像中采样获得),让物体非常精细又不需要指定额外的顶点。
纹理环绕方式
纹理坐标的范围0到1,如果是2D纹理图像,纹理坐标起始于左下角(0,0),终止于右上角(1,1)。如果超出这个范围,openGL使用纹理环绕方式进行填充。环绕方式分为以下四种:
1)GL_REPEAT:重复纹理图像,默认行为;
2)GL_MIRRORED_REPEAT:和GL_REPEAT一样,但是重复的图片是镜像放置的;
3)GL_CLAMP_TO_EDGE:重复纹理坐标的边缘,产生一种边缘被拉伸的效果;
4)GL_CLAMP_TO_BORDER:用户指定的颜色填充超出部分
使用glTexParameteri函数对单个坐标轴设置环绕方式(s,t,r对应x,y,z)
/*第一个参数指定了纹理目标;我们使用的是2D纹理,因此纹理目标是GL_TEXTURE_2D; 第二个参数需要我们指定设置的选项与应用的纹理轴。我们打算配置的是WRAP选项,并且指定S和T轴; 最后一个参数需要我们传递一个环绕方式 */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT); //如果选择GL_CLAMP_TO_BORDER选项,还需要使用glTexParameterfv指定一个边缘的颜色 //float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f }; //glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
纹理过滤
因为纹理坐标范围是0到1的浮点值,不依赖于分辨率,所有怎么将纹理像素映射到纹理坐标呢?OpenGL使用纹理过滤,其中最重要的两种是GL_NEAREST和GL_LINEAR:
1)GL_NEAREST:邻近过滤,默认的过滤方式,其选择中心点最接近纹理坐标的那个像素。产生的是颗粒状的图案,能够清晰看到组成纹理的像素,一般在纹理被缩小时使用。
2)GL_LINEAR:线性过滤,基于纹理坐标附近的纹理像素,计算一个插值,相当于临近像素的混合色,距离越近的纹理像素影响越大。产生的是平滑的图案,很难看出单个的纹理像素,一般在纹理被放大时使用。
使用glTexParameteri函数对放大和缩小两种情况设置过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);//被缩小时的选项 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);//被放大时的选项
多级渐远纹理
物体有远近之分,远处的物体看起来小,产生的片段少。为了节约内存且让小物体更真实,OpenGL使用不同的多级渐远纹理(就是一系列的纹理图像,后一个纹理是前一个纹理的二分之一),根据不同的距离选择不同的纹理图像取样。
在使用多级渐远纹理的时候,不同级别的纹理层之间会产生不真实的生硬边界,这时候可以使用纹理过滤方式,指定不同多级渐远纹理级别之间的过滤方式有4种:
1)GL_NEAREST_MIPMAP_NEAREST:使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样;
2)GL_LINEAR_MIPMAP_NEAREST:使用最邻近的多级渐远纹理级别,并使用线性插值进行采样;
3)GL_NEAREST_MIPMAP_LINEAR:在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样;
4)GL_LINEAR_MIPMAP_LINEAR:在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样。
使用方式与纹理过滤一样:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
注意:对放大的设置选项不能是多级渐远纹理的选项之一,否则会产生GL_INVALID_ENUM错误代码,因为多级渐远纹理是使用在纹理被缩小的情况下。
使用纹理
使用stb_image.h库(下载地址stb/stb_image.h at master · nothings/stb · GitHub),下载这个头文件,将它以stb_image.h
的名字加入工程,并另创建一个新的C++文件,输入以下代码:
#define STB_IMAGE_IMPLEMENTATION #include "stb_image.h"
通过定义STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,让其只包含相关的函数定义源码,等于是将这个头文件变为一个 .cpp
文件了。现在只需要在程序中包含stb_image.h
并编译就可以了。
使用纹理的方式也是需要先创建一个纹理对象,然后绑定它进行配置,如下所示:
//创建纹理,和之前生成的OpenGL对象一样,纹理也是使用ID引用的 unsigned int texture; glGenTextures(1, &texture);//生成纹理的数量1,然后把它们储存在第二个参数的unsigned int数组中 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); //加载纹理图像 int width, height, nrChannels; unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannels, 0); if (data) { //生成纹理,当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像 /* 第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。 第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。 下个参数应该总是被设为0(历史遗留的问题)。 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。 最后一个参数是真正的图像数据。 */ 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);
OpenGL如何采样纹理呢?
首先在顶点着色器传入顶点的纹理坐标
texture_triangle.vs
//vertex shader source #version 330 core layout(location = 0) in vec3 position; //位置X,Y,Z layout(location = 1) in vec3 color; //颜色R,G,B layout(location = 2) in vec2 texture; //纹理S,T out vec3 vertexColor; //顶点颜色 out vec2 textureCoord; //顶点对应纹理坐标 void main() { gl_Position = vec4(position, 1.0); //顶点坐标 vertexColor = color; //从顶点数据那里得到的输入颜色 textureCoord = texture; //从顶点数据那里得到的对应纹理坐标 }
然后片段着色器使用采样器(Sampler)进行采样,这个采样器是uniform类型,因为它是用于程序把纹理对象传给片段着色器的。最后使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。
texture_triangle.fs
//fragment shader source #version 330 core in vec3 vertexColor; //顶点颜色 in vec2 textureCoord; //顶点对应纹理坐标 out vec4 fragColor; //像素的最终颜色 uniform sampler2D ourTexture; //纹理采样器,通过源码中绑定纹理glBindTexture赋值 void main() { fragColor = texture(ourTexture, textureCoord); }
完整示例
//GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h),所以需要在其它依赖于OpenGL的头文件之前包含GLAD #include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> #include "shader.h" #include "stb_image.h" //改变窗口大小 void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } //输入 void processInput(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//点击ESC键退出绘制 glfwSetWindowShouldClose(window, true); } GLFWwindow* init_window() { ///窗口初始化 glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号,当API以不兼容的方式更改时,该值会增加。 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号,当特性被添加到API中时,它会增加,但是它保持向后兼容。 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//使用核心模式,不兼容已废弃函数 //创建glfw窗口 GLFWwindow* window = glfwCreateWindow(800, 600, "ping-window", NULL, NULL); if (window == NULL) { std::cout << "failed to create GLFW window" << std::endl; glfwTerminate();//释放/删除之前的分配的所有资源 return nullptr; } glfwMakeContextCurrent(window);//将窗口的上下文设置为当前线程的主上下文 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);//注册为调整窗口回调函数 //GLAD是用来管理OpenGL的函数指针的,在调用任何OpenGL的函数之前初始化GLAD if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))//给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数 { std::cout << "failed to intialize GLAD" << std::endl; return nullptr; } glViewport(0, 0, 800, 600);//处理过的OpenGL坐标范围只为-1到1,因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600) return window; } int texture_triangle() { ///初始化窗口 GLFWwindow* window = init_window(); ///定义着色器 CShader shader("texture_triangle.vs", "texture_triangle.fs"); ///定义顶点对象 float vertices[] = { // 顶点坐标X,Y,Z // 顶点颜色R,G,B // 纹理坐标S,T 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 // 左上 }; unsigned int indices[] = { 0, 1, 3, // 第一个三角形 1, 2, 3 // 第二个三角形 }; unsigned int VBO, VAO, EBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 顶点坐标属性 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)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); //创建纹理,和之前生成的OpenGL对象一样,纹理也是使用ID引用的 unsigned int texture; glGenTextures(1, &texture);//生成纹理的数量1,然后把它们储存在第二个参数的unsigned int数组中 glBindTexture(GL_TEXTURE_2D, texture);//绑定纹理 //为当前绑定的纹理对象设置环绕、过滤方式 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);//X轴环绕方式 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);//Y轴环绕方式 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);//被缩小时的过滤选项 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);//被放大时的过滤选项 //加载纹理图像 int width, height, nrChannels; unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannels, 0); if (data) { //生成纹理,当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像 /* 第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。 第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。 下个参数应该总是被设为0(历史遗留的问题)。 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。 最后一个参数是真正的图像数据。 */ 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)) { processInput(window); //清空屏幕 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); ///绘制物体 shader.run(); glBindVertexArray(VAO); //使用VEO绘制 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);//绘制图元为三角形,绘制顶点数量6,索引类型uint,偏移量0 glfwSwapBuffers(window);//交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲) glfwPollEvents();//检查有没有触发什么事件 } //释放资源 glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); glfwTerminate();//释放/删除之前的分配的所有资源 return 0; } int main() { texture_triangle(); return 0; }
运行效果
附上纹理图像下载地址container.jpg (512×512) (learnopengl-cn.github.io)