OpenGL 纹理详解
1. 纹理
在OpenGL中,纹理是一种常用的技术,用于将图像或图案映射到3D模型的表面上,以增加图形的细节和真实感
2. 纹理坐标
纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。下面的图片展示了我们是如何把纹理坐标映射到三角形上的。
当图片绘制大小与图片实际大小不一致时,势必会涉及到缩放。此时而缩放时我们要采取什么插值方式,是需要我们指定给GL的,也就是说,我们要指定采样方式。
3. 采样方式
环绕方式
之前我们有说过,纹理的坐标区域是[0,1],且通常左下角为纹理坐标的(0,0)点。现在我们设想一下下面的情况:
当我们给出顶点坐标是横纵左边均是0.5的四个象限的点时,我们的渲染区域即是第一个图中的粉色区域。
此时我们有一张图像,即是第四个图样子的图片。
我们知道,纹理坐标系是[0,1]的,如果我们与顶点坐标系中我们指定的四个顶点一一对应起来的时候,那就应该是渲染出第二幅图的样子。
但是如果我们并不是想图像A充满我们的粉色区域怎么办?我们想让图像只有粉色区域的1/2大小,并且居中平铺,这时候怎么办呢?
试想如果大小1/2且居中对齐,那么我们纹理坐标系的(1,1)点应该对应顶点坐标系的(0.25,0.25)。这点没有问题对吧。但实际我们应用只应用顶点坐标,那么我们要换算一下顶点坐标系中(0.5,0.5)对应的是纹理坐标系中的那个点呢?换算完成后应该是(1.5,1.5)。这里如何换算可以结合第三幅图的样子考虑下。这样我们换算完四个顶点坐标分别对应的纹理坐标值后传个顶点着色器就好。
另外我们只会渲染出我们顶点数据所渲染的图形,超出边届的将会被剪裁掉。
事实上,我们只是想绘制一个比顶点区域要小的图片,至于平铺是我们选择的一种环绕方式而已。
然而GL实际为我们提供了四种环绕方式:
那么知道了这几种环绕方式,在GL中我们要如何设置环绕方式呢?
当然,如果我们指定边缘颜色的环绕模式,我们还要指定边缘颜色。
4. 纹理过滤
渲染一个图像,我们不可能保证绘制的实际大小即是图片的实际大小,事实上一般情况下,我们都需要进行缩放。我们知道,GL中我们只指定顶点数据,而中间点都是GL内部自己采用插值器进行计算的。那么当进行缩放时,我们就要告诉GL应该采用的插值方式。指定插值方式,又叫做纹理过滤
。
那么缩放就涉及到图像的放大和缩小。我们先想一下放大图像应该采取什么纹理滤镜。
这里我们先讨论两个较为重要的纹理滤镜:GL_NEAREST
和GL_LINEAR
。
GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:
那么这两种纹理过滤方式有怎样的视觉效果呢?让我们看看在一个很大的物体上应用一张低分辨率的纹理会发生什么吧(纹理被放大了,每个纹理像素都能看到):
5. 多级渐远纹理
上述中,我们叙述了放大的纹理滤镜,但是如果是缩小呢?我们当然也可以采取之前提到的两种纹理滤镜。但当我们缩小的倍数足够小时,计算插值将会是一个耗时过程,此外缩小本身就会丢失很多细节,这时如果我们仍使用原分辨率的纹理进行缩放并绘制,无疑在内存上也是浪费。
如何创建多级渐远纹理呢?我们可以使用glGenerateMipmaps
函数。
那么多级渐远纹理有几种模式呢:
像放大时使用的纹理滤镜一样,我们应该像下面这样设置缩小的纹理滤镜:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
6. 加载与创建纹理
1 #include <glad/glad.h> 2 #include <GLFW/glfw3.h> 3 #include <math.h> 4 #include <iostream> 5 #define STB_IMAGE_IMPLEMENTATION 6 #include "stb_image.h" 7 8 void framebuffer_size_callback(GLFWwindow* window, int width, int height); 9 void processInput(GLFWwindow *window); 10 GLFWwindow * configOpenGL(); 11 void loadImg(const char * path,unsigned int * texture,unsigned int uniteLoc); 12 void configVAO(unsigned int * VAO,unsigned int * VBO,unsigned int * EBO); 13 void finishiRenderLoop(); 14 // settings 15 const unsigned int SCR_WIDTH = 800; 16 const unsigned int SCR_HEIGHT = 600; 17 18 19 const char *vertexShaderSource = "#version 330 core\n" 20 "layout (location = 0) in vec2 aPos;\n" 21 "layout (location = 1) in vec3 aColor;\n" 22 "layout (location = 2) in float show;\n" 23 "layout (location = 3) in vec2 aTexCoord;\n" 24 "out vec3 ourColor;\n" 25 "out float Img;\n" 26 "out vec2 TexCoord;\n" 27 "void main()\n" 28 "{\n" 29 " gl_Position = vec4(aPos,0.0, 1.0);\n" 30 " ourColor = aColor;\n" 31 " Img = show;\n" 32 " TexCoord = aTexCoord;\n" 33 "}\0"; 34 const char *fragmentShaderSource = "#version 330 core\n" 35 "out vec4 FragColor;\n" 36 "in vec3 ourColor;\n" 37 "in float Img;\n" 38 "in vec2 TexCoord;\n" 39 "uniform sampler2D ourTexture;\n" 40 "uniform sampler2D avatarTexture;\n" 41 "uniform float factor;\n" 42 "void main()\n" 43 "{\n" 44 "if (Img == 1.0f) {\n" 45 "FragColor = mix(texture(ourTexture, TexCoord),texture(avatarTexture, TexCoord),factor) * vec4(ourColor, 1.0);\n" 46 "} else {\n" 47 "FragColor = vec4(ourColor, 1.0);\n" 48 "}\n" 49 "}\n\0"; 50 51 int main() 52 { 53 GLFWwindow * window = configOpenGL(); 54 55 ///创建一个顶点着色器 56 int vertexShader = glCreateShader(GL_VERTEX_SHADER); 57 58 ///附着源码并编译 59 glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); 60 glCompileShader(vertexShader); 61 62 ///检查编译是否成功 63 int success; 64 char infoLog[512]; 65 glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); 66 if (!success) 67 { 68 glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); 69 std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; 70 } 71 72 ///创建一个片段着色器 73 int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); 74 75 ///附着源码并编译 76 glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); 77 glCompileShader(fragmentShader); 78 79 ///检查编译是否成功 80 glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); 81 if (!success) 82 { 83 glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); 84 std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl; 85 } 86 87 ///创建着色器程序 88 int shaderProgram = glCreateProgram(); 89 90 ///链接着色器 91 glAttachShader(shaderProgram, vertexShader); 92 glAttachShader(shaderProgram, fragmentShader); 93 glLinkProgram(shaderProgram); 94 95 ///检查链接是否成功 96 glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); 97 if (!success) { 98 glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); 99 std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl; 100 } 101 102 ///释放着色器 103 glDeleteShader(vertexShader); 104 glDeleteShader(fragmentShader); 105 106 unsigned int VAO,VBO,EBO; 107 108 ///配置VAO 109 configVAO(&VAO,&VBO,&EBO); 110 111 ///设置纹理单元的位置(想要设置着色器程序的值,必先激活着色器程序) 112 glUseProgram(shaderProgram); 113 glUniform1i(glGetUniformLocation(shaderProgram,"ourTexture"),0); 114 glUniform1i(glGetUniformLocation(shaderProgram,"avatarTexture"),1); 115 116 while (!glfwWindowShouldClose(window)) 117 { 118 processInput(window); 119 120 ///设置清屏颜色 121 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); 122 ///清屏 123 glClear(GL_COLOR_BUFFER_BIT); 124 125 ///使用指定着色器程序(由于上面已经激活过着色器程序,所以此处不用再次激活) 126 // glUseProgram(shaderProgram); 127 128 ///改变 129 float timeValue = glfwGetTime(); 130 float factor = sin(timeValue) / 2.0f + 0.5f; 131 glad_glUniform1f(glGetUniformLocation(shaderProgram,"factor"),factor); 132 133 ///绑定定点数组对象 134 glBindVertexArray(VAO); 135 ///以索引绘制顶点数据 136 // glDrawArrays(GL_TRIANGLES, 0, 3); 137 glDrawElements(GL_TRIANGLES,30,GL_UNSIGNED_INT,0); 138 139 ///交换颜色缓冲 140 glfwSwapBuffers(window); 141 ///拉取用户事件 142 glfwPollEvents(); 143 } 144 145 ///释放对象 146 glDeleteVertexArrays(1, &VAO); 147 glDeleteBuffers(1, &VBO); 148 glDeleteBuffers(1, &EBO); 149 150 finishiRenderLoop(); 151 152 return 0; 153 } 154 155 GLFWwindow* configOpenGL() { 156 ///初始化glfw 157 glfwInit(); 158 159 ///设置版本号 160 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 161 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); 162 163 ///设置核心模式 164 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); 165 166 #ifdef __APPLE__ 167 ///设置对Mac OS X的兼容 168 glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); 169 #endif 170 171 ///创建window 172 GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL); 173 if (window == NULL) { 174 std::cout << "Failed to create GLFW window" << std::endl; 175 glfwTerminate(); 176 return NULL; 177 } 178 ///将window设置成当前上下文 179 glfwMakeContextCurrent(window); 180 ///设置窗口事件更新触发的回调 181 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); 182 183 ///初始化GLAD 184 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { 185 std::cout << "Failed to initialize GLAD" << std::endl; 186 return NULL; 187 } 188 return window; 189 } 190 191 void configVAO(unsigned int * VAO,unsigned int * VBO,unsigned int * EBO) { 192 ///顶点数据 193 float vertices[] = { 194 //顶点坐标-2 //颜色-3 //是否绘制图片-1 //纹理坐标-2 195 0.5f, 0.5f,1.0f,1.0f,0.0f,1.0f,1.5f,1.5f, // 右上角 196 0.5f, -0.5f,0.0f,1.0f,1.0f,1.0f,1.5f,-0.5f, // 右下角 197 -0.5f, -0.5f,1.0f,0.0f,1.0f,1.0f,-0.5f,-0.5f, // 左下角 198 -0.5f, 0.5f,1.0f,1.0f,1.0f,1.0f,-0.5f,1.5f, // 左上角 199 1.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,0.0f, 200 1.0f,-1.0f,1.0f,0.0f,0.0f,0.0f,0.0f,0.0f, 201 -1.0f,-1.0f,0.0f,1.0f,0.0f,0.0f,0.0f,0.0f, 202 -1.0f,1.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f, 203 }; 204 205 ///索引数据 206 unsigned int indices[] = { 207 0,1,3, 208 1,2,3, 209 0,4,5, 210 0,1,5, 211 1,5,6, 212 1,2,6, 213 2,6,7, 214 2,3,7, 215 3,7,4, 216 3,0,4, 217 }; 218 219 ///创建顶点数组对象 220 glGenVertexArrays(1, VAO); 221 222 ///创建顶点缓冲对象 223 glGenBuffers(1, VBO); 224 ///创建索引缓冲对象 225 glGenBuffers(1, EBO); 226 227 ///绑定定点数组对象至上下文 228 glBindVertexArray(*VAO); 229 230 ///绑定定点缓冲对象至上下文 231 glBindBuffer(GL_ARRAY_BUFFER, *VBO); 232 233 ///把顶点数组复制到顶点缓冲对象中 234 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); 235 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); 236 glEnableVertexAttribArray(0); 237 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(2 * sizeof(float))); 238 glEnableVertexAttribArray(1); 239 glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(5 * sizeof(float))); 240 glEnableVertexAttribArray(2); 241 glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); 242 glEnableVertexAttribArray(3); 243 ///绑定索引缓冲对象至上下文 244 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, *EBO); 245 ///把索引数据复制到索引缓冲对象中 246 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); 247 248 ///加载图片 249 unsigned int texture,avatar; 250 loadImg("/Users/momo/Desktop/Wicky/Learn\ OpenGL/入门/Demos/6.纹理/OpenGL_Template/container.jpg", &texture,0); 251 loadImg("/Users/momo/Desktop/Wicky/Learn\ OpenGL/入门/Demos/6.纹理/OpenGL_Template/avatar.jpeg", &avatar, 1); 252 253 ///解除顶点数组对象的绑定 254 glBindVertexArray(0); 255 ///解除顶点缓冲对象的绑定 256 glBindBuffer(GL_ARRAY_BUFFER, 0); 257 ///解除索引缓冲对象的绑定 258 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0); 259 } 260 261 void loadImg(const char * path,unsigned int * texture,unsigned int uniteLoc) { 262 ///设置图片加载时上下翻转 263 stbi_set_flip_vertically_on_load(true); 264 265 ///加载图片 266 int width, height, nrChannels; 267 unsigned char *data = stbi_load(path, &width, &height, &nrChannels, 0); 268 269 ///生成纹理对象并绑定至上下文中的2D纹理 270 glGenTextures(1, texture); 271 glActiveTexture(GL_TEXTURE0 + uniteLoc); 272 glBindTexture(GL_TEXTURE_2D, *texture); 273 274 ///设置纹理环绕及过滤模式 275 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 276 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 277 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 278 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 279 280 ///加载纹理数据并设置多级渐远纹理 281 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); 282 glGenerateMipmap(GL_TEXTURE_2D); 283 284 ///释放图像数据 285 stbi_image_free(data); 286 } 287 288 void finishiRenderLoop() { 289 ///释放窗口资源 290 glfwTerminate(); 291 } 292 293 ///处理输入 294 void processInput(GLFWwindow *window) 295 { 296 if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) 297 glfwSetWindowShouldClose(window, true); 298 } 299 300 ///窗口事件更新回调 301 void framebuffer_size_callback(GLFWwindow* window, int width, int height) 302 { 303 ///设置视口大小 304 glViewport(0, 0, width, height); 305 }
7. 加载图像
那么,我们先考虑如何加载图片数据。这里我们使用std_image.h
进行图像加载。
接下来我们用std_image
为我们提供的函数加载图像数据:
8. 生成纹理
接下来我们基本就是用GL统一的模式去创建对象了:
这里我们单独讲一下glTexImage2D
这个函数。
- 第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。
- 第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
- 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。
- 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
- 下个参数应该总是被设为0(历史遗留的问题)。
- 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。
- 最后一个参数是真正的图像数据。
有了图像数据,我们还要指定纹理坐标到顶点坐标数据中。同时我们要修改顶点着色器和片段着色器。并设置顶点属性。与前文中绘制三角形时设置的基本相同。这里我们只介绍如何在片段着色器中使用我们的纹理。
9. 纹理单元
GL中,一个纹理的位置值被称为一个纹理单元
。而GL中默认的纹理单元是0,且这个纹理单元是GL中默认激活的。所以上述代码中,我们绑定纹理的时候,并没有指定纹理单元,就是使用的默认的0这个单元。所以在片段着色器中声明的采样器,默认也是对应的纹理单元0。所以我们取到的也就是这个默认的纹理单元。
当使用多个纹理时,首先我们要激活对应的纹理单元,然后在纹理单元中绑定纹理。在真正开始渲染之前,即进入渲染循环之前,我们还要告诉片段着色器每一个采样器对应的是哪个纹理单元。
所以我们使用纹理的代码大概是这个样子的:
那么我们在片段着色器中,如果想要进行混合的话应该使用mix函数。
FragColor = mix(texture(ourTexture, TexCoord),texture(avatarTexture, TexCoord),factor) * vec4(ourColor, 1.0);