OpenGL学习笔记《四》纹理
之前绘制的图形,只加上简单的颜色,现在尝试绘制出图片。在opengl中可以理解为一个存储了多种多样颜色信息的对象,称之为纹理。
在使用着色器处理图形的颜色信息的时候,我们需要使着色器程序知道哪个顶点用哪种颜色。同理,在使用纹理的时候,我们也需要使着色器程序知道哪个顶点用哪部分的纹理信息。在这里就引入了纹理坐标的概念,纹理坐标在x、y轴上的值区间范围是[0, 1],原点坐标在左下角。着色器程序根据纹理坐标获取纹理信息的过程称之为采样(sampling)。
上面我们提到纹理对象可以看做是存储了非常多颜色信息的对象,但是我们在使用纹理的时候,并不是将每个纹理像素点数据传给着色器程序,我们可能只会传某些顶点坐标,如绘制三角形的时候我们会传左下角、右下角、上中点三个纹理坐标,那么着色器程序又是如何根据这简单的信息,正确的进行纹理采样呢?在这里又会引出另外的几个概念:Texture Wrapping(纹理包围?)、Texture Filtering(纹理过滤?)、Mipmaps(贴图?)
Texture Wrapping
我们知道纹理坐标的取值范围是[0,1],但是我们并不能保证我们传给着色器程序的纹理坐标一定会在这个范围内,假如我们超出这个范围应该怎么处理?opengl提供了四种模式才处理这种情况
- GL_REPEAT:重复,即超出的部分重复表现原本图像;
- GL_MIRRORED_REPEAT:镜像重复,图像不断重复,但是每次重复的时候对图像进行镜像或者反转;
- GL_CLAMP_TO_EDGE:边缘截取,超出取值范围的,显示为边缘拉伸的部分;
- GL_CLAMP_TO_BORDER:边缘截断,超出取值范围的,直接显示用户给定的一种颜色。
上述四种模式,可以参考一下下面的图效果:
opengl提供设置这四种模式的API接口是:glTexParameter*,该函数接受三个参数。参数1表示我们设置的纹理对象,参数2表示在哪个纹理坐标轴,参数3表示设置的模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
上述代码表示,我们设置的是2D纹理对象,在WRAP模式下设置X轴REPEAT,Y轴也是REPEAT 。我们提到如果是选择CLAMP_TO_BORDER,超出纹理坐标范围的会显示成用户自定义的颜色,所以如果我们需要使用这种模式,那么就需要调整一下调用方式:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f }; glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
不需要指定X/Y轴,而是只需要指定一下用户自定义颜色既可。
Texture Filtering
很多时候我们的纹理大小和图形大小并不能匹配,可能纹理小了图形大了,或者反之。此时,opengl也提供了相应的处理方式,称之为Texture Filtering。使用的多的主要是两种模式
- GL_NEAREST:采样的时候,选最靠近纹理坐标的像素点信息,如下图
- GL_LINEAR:采样的时候,对靠近纹理坐标周围的像素点信息进行线性插值计算,得到最终的颜色,如下图
用两张图来看下实际的效果:
从上图中可以看到,GL_NEAREST模式锯齿效果比较明显,而GL_LINEAR模式则更平滑一些。所以一般在我们放大操作的时候,会选择GL_LINEAR模式,缩小操作的时候,会选择GL_NEAREST,opengl提供的api调用接口也是glTexParameter*。参数1也是指定纹理对象,参数2表示操作类型,参数3表示使用的模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
上述代码表示在缩小操作时使用GL_NEAREST模式,在放大操作时使用GL_LINEAR模式。
Mipmaps
当我们把图像缩小到一个很小的比例,此时对于复杂的图像,我们看到的细节其实已经很少了。如果对一个大的纹理只是简单的进行缩小操作,到一定比例之后opengl对纹理的采样也会很困难,并且此时也会造成一定的内存资源浪费(要存储很多不能表现出来的纹理信息)。opengl对于这个问题引入了一个Mipmaps概念,opengl会生成一系列的纹理,每一个纹理是前面一个纹理的二分之一,并且会对每一个纹理进行优化使能拥有更多的显示细节。在程序运行的过程中opengl会使用不用的纹理,以提升效率及显示效果。
opengl提供了api来生成mipmaps,glGenerateMipmaps,在我们创建好纹理之后,再调用这个接口,就能自动生成mipmaps。前面提到这一系列纹理,当前纹理是前面纹理的二分之一,程序渲染过程中切换这两种纹理,如果没有经过一定的处理,就会表现的很生硬的边界。在此opengl也提供了类似于Texture Filtering中用到的模式,来优化切换的效果
- GL_NEAREST_MIPMAP_NEAREST:接收最近的mipmap来匹配像素大小,并使用最临近采样;
- GL_LINEAR_MIPMAP_NEAREST:接收最近的mipmap并使用线性插值进行纹理采样;
- GL_NEAREST_MIPMAP_LINEAR:在量mipmap之间进行线性插值,通过最临近进行纹理采样;
- GL_LINEAR_MIPMAP_LINEAR:在两个mipmap之间进行线性插值,并通过线性插值进行纹理采样;
所以在引入了mipmaps概念之后,我们进行缩小操作不仅可以简单的使用 GL_NEAREST 模式,还可以引入mipmap中提供的四种模式,来达到比较好的缩小效果。
加载纹理
上述提到了几种纹理采样的方式,现在我们需要知道如果加载纹理。我们知道图片有多种格式,如png、jpg、tpg,每一种格式对于纹理信息的存储都有不同,所以需要用不同的加载方式来加载格式的图片,来获取纹理信息。游戏引擎可能会针对不同格式图片采用不同的库来加载,以提升效率或者最优的兼容性。在这我们使用一个简单的加载库,能加载多种格式的图片:stb_image.h ,只有一个头文件,很方便。具体的使用就不描述了,直接看一段代码吧:
// 创建、绑定纹理id glGenTextures(1, &m_imgId); glBindTexture(GL_TEXTURE_2D, m_imgId);
// 设置纹理采样模式 glTexParameteri(GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 加载图片 int width, heigth, nrChannels; unsigned char* data = stbi_load(path, &width, &heigth, &nrChannels, 0);
// 如果是jpg格式图,因为没有透明通道,就是用GL_RGB,如果是png则需要使用GL_RGBA模式。 glTexImage2D(GL_TEXTURE_2D, 0, mode, width, heigth, 0, GL_RGB, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); // 释放 stbi_image_free(data)
这样我们就加载到了纹理数据,再把这传给着色器程序。我们先调整一下顶点数据:
float vertices[] = { // positions // colors // texture coords 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left };
加入了纹理坐标信息,再调整一下顶点属性的设置:
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2);
然后在顶点着色器中获取纹理坐标,再传递给片段着色器,在这里主要看一下片段着色器的调整:
#version 330 core out vec4 FragColor; in vec2 oCoord; uniform sampler2D u_texture; void main(){ FragColor = texture(u_texture, oCoord);
}
texture()函数为GLSL提供的采样函数,u_texture就是我们加载纹理时存储的纹理id索引,oCoord是顶点着色器中传递过来的纹理坐标。当然我们也可以同时加载两张图片,且同时显示出来,这个时候片段着色器就需要做一定的调整:
#version 330 core out vec4 FragColor; in vec2 oCoord; uniform sampler2D u_texture; void main(){ FragColor = mix(texture(u_texture, oCoord), texture(u_texture2, vec2(oCoord.x, oCoord.y)), 0.2); }
mix()函数为GLSE提供的混合函数,将参数1和参数2的值,按照参数3的比例进行混合。最终我们能得到类似这样的图像
对应的代码在这里。