纹理
一、什么是纹理?
引用百度百科的定义:
计算机图形学中的纹理既包括通常意义上物体表面的纹理即使物体表面呈现凹凸不平的沟纹,同时也包括在物体的光滑表面上的彩色图案,通常我们更多地称之为花纹。对于花纹而言,就是在物体表面绘出彩色花纹或图案,产生了纹理后的物体表面依然光滑如故。对于沟纹而言,实际上也是要在表面绘出彩色花纹或图案,同时要求视觉上给人以凹凸不平感即可。 凹凸不平的图案一般是不规则的。在计算机图形学中,这两种类型的纹理的生成方法完全一致, 这也是计算机图形学中把他们统称为纹理的原因所在。
在上节中我们给每个顶点都添加了一个颜色值,从而使每个顶点都有对应的颜色,纹理的其实就是指定了每一个顶点对应的颜色,存储在一张2D或者3D的图片当中,然后把某个顶点对应的颜色在图片中的位置定义为纹理坐标,也就是说,知道了一个顶点的纹理坐标,我们就可以在图片中查找得到该顶点对应的颜色。有了纹理,我们就能够给物体添加更多的细节。
二、纹理映射
纹理映射 (Texture Mapping) 是一种将物体空间坐标点转化为纹理坐标,进而从纹理上获取对应点的值,以增强着色细节的方法。
以2D纹理举例说明,2D的纹理一般对应的纹理坐标从左下角到右上角分别为(0,0)和(1,1),例如下面这张图片:
如果我指定三角形的三个顶点对应的纹理坐标如下:
float vertices[3 * 8] =
{
//pos //Color //texcoord
-0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.5f, 1.0f
};
正确渲染得到的结果是这样的:
为了便于观察,我把两张图放到一起对比一下:
左边这张是我们的纹理图片,右边这张是给三角形指定纹理坐标后输出的结果,可以看到,根据我们设定的纹理坐标,三角形左面的顶点对应(0 , 0),所以对应纹理中的左下角,右面的顶点对应(1 ,0),对应纹理中的右下角,上面顶点同理对应纹理中上边缘的中间,因此三角形被绘制成了右图所示的样子。此外,还记得上节中指定了三角形三个顶点的颜色后输出的图形是彩色的吗,是因为顶点属性被进行了插值处理,这节中纹理坐标也是相同的原理,虽然我们仅指定了三个顶点的纹理坐标,但是经过插值后可以得到三角形区域对应的所有纹理坐标,从而在纹理中找到对应的颜色值输出,我们看到的输出结果就正好是纹理本身的样子了,输出图像也是纹理映射的结果。
三、创建纹理
想要使用纹理,首先需要创建并绑定纹理对象,和之前的顶点数组以及缓冲区一样:
unsigned int texture;
glGenTexture(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glBindTexture
函数的第一个参数指定了要绑定的纹理格式,是2D还是3D纹理,第二个参数是我们的纹理对象。
四、纹理的加载
使用纹理之前,首先要做的事情就是将准备好的图片文件加载到我们的应用中,这里使用图像加载库stb_image
来实现,有关stb_image
的配置这里不多做介绍,可以参阅: stb_image库及使用
将图片加载到应用中:
//load image
int width, height, channels;
stbi_set_flip_vertically_on_load(1);
unsigned char* data = stbi_load("Asset/texture/leidian.jpg", &width, &height, &channels, 0);
这里创建了三个变量,其中width用于保存图像的宽度,也就是在水平方向的像素数量,height用于保存高度,channels保存图像拥有的通道数,一般为 rgb 三个或是 rgba 四个,然后我们需要使用函数stbi_set_flip_vertically_on_load
来实现翻转图像的 y 坐标,如果不这么做,你会得到下面的结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M6thHPWN181407b444868684eb741ea97c54.png#pic_center)null#pic_center)]
具体原因和图像存储和读取的方式有关,OpenGL要求y轴0.0
坐标是在图片的底部的,但是图片的y轴0.0
坐标通常在顶部,这里不再做展开。
然后创建了一个无符号字符数组用于保存读取到的图像数据,使用函数stbi_load
传入的参数分别为:图像文件所在位置,用于存放宽度高度和通道数变量的地址,最后一个是期望通道数对应的变量地址,这里我们直接填0。
如果加载成功,那么就创建纹理,否则输出读取失败:
if (data)
{
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 image!" << std::endl;
stbi_image_free(data);
glTexImage2D
函数的参数:
- 第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。
- 第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
- 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有
RGB
值,因此我们也把纹理储存为RGB
值。- 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
- 下个参数应该总是被设为
0
(历史遗留的问题)。- 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为
char
(byte)数组,我们将会传入对应值。- 最后一个参数是真正的图像数据。
使用glTexImage2D
后,当前绑定的纹理对象会被附加上我们添加的纹理图像,如果没有指定多级渐远纹理(mipmap),会默认只有基本级别的纹理图像,如果要添加 mipmap,可以通过指定上述第二个参数,也可以通过调用glGenerateMipmap
,会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
注意生成纹理之后一定要释放图像内存:stbi_image_free(data)
。
五、纹理的应用
这次我们使用之前使用过的矩形来应用纹理,首先需要指定矩形对应的顶点数组:
//rectangle vertices
float rectangle_vertices[] =
{
//pos //Color //texcoord
-0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.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, 1.0f,
0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f
};
添加了顶点坐标之后,顶点数据对应的布局如下:
所以添加新的顶点布局:
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
注意此时的步长由于添加了2个float数据变为了8,对应的偏移量也变成了6个浮点数的长度,因为前面有3个顶点坐标和三个颜色对应的float值。
接着在vertex shader中指定输入顶点属性对应的布局:
#version 330 core
layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec3 a_Color;
layout(location = 2) in vec2 a_texCoord;
out vec3 v_Position;
out vec3 v_Color;
out vec2 v_texCoord;
//uniform float u_Offset;
void main()
{
v_Position = a_Position;
v_Color = a_Color;
v_texCoord = a_texCoord;
//v_Position.x += u_Offset;
gl_Position = vec4(v_Position, 1.0);
}
fragment shader:
#version 330 core
layout(location = 0) out vec4 FragColor;
in vec3 v_Position;
in vec3 v_Color;
in vec2 v_texCoord;
uniform vec4 u_Color;
uniform float u_time_factor;
uniform sampler2D Texture;
void main()
{
//FragColor = u_Color;
//FragColor = vec4(v_Color * u_time_factor, 1);
FragColor = texture(Texture, v_texCoord);
}
在片元着色器中,我们声明了纹理采样器:sampler2D,用于访问创建的纹理,使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。texture函数会使用之前设置的纹理参数对相应的颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。
在调用绘制函数之前首先先对纹理进行绑定,绑定之后就会把纹理赋值给片元着色器中的纹理采样器。
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(rectangle_vertex_array);
//glDrawArrays(GL_TRIANGLES, 0, 3);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
运行结果:
可以通过混合输出颜色得到下面的结果:
FragColor = texture(Texture, v_texCoord) * vec4(v_Color, 1);
六、 OpenGL 中的纹理单元
上面的 采样器 sampler2D 定义为了 uniform 变量,我们可以使用 glUniform1i 来指定纹理采样器对应的纹理单元,从而在一个片元着色器中设置多个纹理。
纹理单元,又称之为纹理映射单元(Texture mapping unit,TMU)是现代图形处理器(GPU)的部件。从历史上看,它是一个独立的物理处理器 TMU能够旋转,调整大小和扭曲位图图像(执行纹理采样),以作为纹理放置在给定3D模型的任意平面上。此过程称为纹理制图映射。
如果想要使用不同的纹理,就要激活不同的纹理单元,在进行纹理绑定之前,首先先激活想要使用的纹理单元:
glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture);
这样,我们的texture对象就绑定到了 0 号纹理单元,纹理单元GL_TEXTURE0
是默认激活的,因此之前只有一张纹理使用glBindTexture
的时候,无需激活任何纹理单元。
OpenGL至少保证有16个纹理单元供你使用,也就是说你可以激活从
GL_TEXTURE0
到GL_TEXTRUE15
。它们都是按顺序定义的,所以我们也可以通过GL_TEXTURE0 + 8
的方式获得GL_TEXTURE8
,这在当我们需要循环一些纹理单元的时候会很有用。
下面我们添加另外一个采样器:
uniform sampler2D texture1;
uniform sampler2D texture2;
然后我们再加载一张箱子的纹理:
unsigned int texture2;
glGenTextures(1, &texture2);
glBindTexture(GL_TEXTURE_2D, texture2);
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);
//load image2
data = stbi_load("Asset/texture/container.jpg", &width, &height, &channels, 0);
if (data)
{
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 image!" << std::endl;
stbi_image_free(data);
分别给两个采样器指定对应的纹理单元,这里我们使用 shader 类中的 set_int()来设置:
triangle_shader.bind();
triangle_shader.set_int("texture1", 0);
triangle_shader.set_int("texture2", 1);
指定好纹理单元之后,在Render Loop 中使用两个纹理时分别激活对应的纹理单元:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
然后在 fragment shader中,我们将两个纹理采样得到的颜色进行混合:
FragColor = mix(texture(texture1, v_texCoord), texture(texture2, v_texCoord), 0.5);
运行查看结果:
七、纹理环绕和过滤
创建纹理对象好之后,还可以指定纹理环绕和滤波的格式,来指定当纹理坐标的设置超过(0,1)范围之外,纹理以什么样的形式输出以及以什么样的方式进行滤波处理。
7.1 纹理环绕
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
均通过函数glTexParameter
来实现,其中前两行用于指定在水平和垂直方向的环绕方式,第二个参数如果是3D纹理还对应一个GL_TEXTURE_WRAP_R
,然后第三个参数可以选择的内容有:
环绕方式 | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
四种效果如下:
另外,如果选择GL_CLAMP_TO_BORDER
模式,你还可以指定边缘颜色:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
7.2 纹理过滤
纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以OpenGL需要知道怎样将纹素(Texel)映射到纹理坐标。当你有一个很大的物体但是纹理的分辨率很低的时候,需要指定一个纹理坐标对应的纹素。通过下面两行来指定 filter 的模式。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
GL_NEAREST
(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST
的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:
两种纹理过滤方式的视觉效果
GL_NEAREST产生了颗粒状的图案,我们能够清晰看到组成纹理的像素,而GL_LINEAR能够产生更平滑的图案,很难看出单个的纹理像素。GL_LINEAR可以产生更真实的输出。
八、OpenGL中的 MipMap
OpenGL使用一种叫做多级渐远纹理(Mipmap)来解决纹理缩小的问题,试想这样一个情况,假设有两个像素,一个像素对应了近处的物体,另外一个对应了远处的物体,那么这两个像素映射到纹理中之后,远处物体对应的像素会投射出一块更大的面积,(近大远小的原理,远处往往多个物体对应一个像素),可以看下面这幅图:
因此这个像素的纹理坐标对应到纹理图片中的一大块区域,因此其颜色很难对应准确,会产生类似条纹状的 artifact。
mipmap简单来说就是一系列的纹理图像,后一个纹理图像是前一个的四分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。
OpenGL通过glGenerateMipmaps
函数来创建对应纹理的 mipmap,那么如何使用?
同样使用函数glTexParameteri
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
过滤方式可以选择:
过滤方式 | 描述 |
---|---|
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样 |
Reference:
LearnOpenGL-纹理