最新大型开源项目-云游戏,云桌面系统,欢迎关注
本项目代码仓库
1.为什么使用纹理
在之前的章节中,我们绘制了一个矩形,给了它特定的颜色,也通过矩阵操作了它平移旋转。但无论是炫酷的3A大作,还是简单的益智游戏,都是很漂亮,很有设计感的。单纯通过给顶点颜色,几乎很难做到,那么我们可以通过给这个矩形贴上一张图片,让他变得华丽起来。比如我的一个场景(可看这里的视频):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gfyjvkX3-1623148877864)(oscimg.oschina.net/oscnet/up-f…)]
上图中,背景的图片,圆形的图片,小雨滴,甚至81192这几个字也是图片,也就是纹理。它几乎无处不在,极大的丰富我们的画面。
2.纹理采样与纹理环绕
纹理坐标在几何上是连续的,而片段或者像素是离散的,当我们要将坐标映射到纹理上,从纹理上去除颜色值给OpenGL,就是采样的过程。
- 3.1 采样/纹理过滤
OpenGL中的过滤方式由多种,常用的两种:GL_NEAREST和GL_LINEAR。
GL_NEAREST(最近邻):
从图上可以看出,要采样这个坐标的颜色,在GL_NEAREST模式下,会找一个离他最近的一个中心点的颜色,来作为最后的结果。
GL_LINEAR(线性插值):
线性插值的情况下,会找周围相邻的几个点的颜色,做一个平均。当然也不是绝对的平均,会根据距离做加权平均。也不一定是选取周围4个,这些值不固定,与算法实现有关系,但道理是相通的。
两种过滤方式,会产生明显的差别,可以参看如下图示:
在OpenGL中可以很简单的设置:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- 3.2 纹理环绕
OpenGL规定纹理坐标的范围是[0,1],如果超过了怎么办呢?
实际中我们见过很多这种情况,铺地板。假如每块地板都是一个纹理,不停的铺同一种地板就是一种纹理环绕方式:重复。参看如下图示:
OpenGL提供多种环绕方式:
GL_REPEAT 重复纹理图像。 这是OpenGL的默认选项。
GL_MIRRORED_REPEAT 也是重复,但是图片被镜像了。
GL_CLAMP_TO_EDGE 超出的部分全都用边缘的颜色代替。
GL_CLAMP_TO_BORDER 超出的部分用户自己指定颜色。
看下图:
在OpenGL中我们可以这样设置:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
如果自己设置颜色,则可以通过这样设置:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
- 3.3 mipmap贴图
思考一个问题,如果一个物体离我们很远,即便它很大,我们也只是看它是一点点的。就像拍照一样,雷峰塔再高,我们与它合影,拍在画面里也仅仅是一些屏幕的像素。那么问题来了,当一个说很高清的物体离我们很远的时候,通常只需要很少的像素就能表示它,但是我们却因为它的分辨率很大,很难产生理想的片段输出。因此发明了一个叫多级渐远纹理(mipmap)的技术。注意:既然叫多级渐远,那么通常就用来表示远处的物体,也就是缩小时用的。放大的情况并不适合它。
mipmap本质上说就是一堆图片,以原始分辨率的图开始,以后每一张是上一张尺寸的一半,看下面的图:
我们按照距离来划分,在不同的距离范围内,从不同的尺寸上采样,这样就不会出现离得非常远,还要从原始分辨率上采样了,直接在一个小尺寸的图像上采样即可。
需要声明的一个问题是,这些图片也不是连续变化的,是以除以2的尺寸递减,那么必然会出现断档的情况,比如要用0.3倍的图像时,怎么办呢?
答案是让不同尺寸下的图片也进行所谓的纹理过滤,也就是OpenGL通过前后两张图帮我们生成一个中间的过渡图,然后再采样。
生成mipmap非常简单,OpenGL已经提供了一个方法,只需要调用一下即可:
glGenerateMipmap(GL_TEXTURE_2D);
可以用如下代码设置缩小时的纹理过滤:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
3.如何使用纹理
- 2.1 现实中,如何给一个矩形贴一张画呢?
如果我们的画跟矩形一样大,那么只要画的四个顶点跟矩形的四个顶点都对齐就能保证,画严丝合缝的被贴上。 - 2.2 OpenGL中如何做呢?
跟现实中类似,只要我的纹理四个点的坐标跟矩形的四个顶点对应上,就能保证被贴上了。更方便的是,纹理是可以被拉伸缩放的,我们总能把纹理贴在任意大小的矩形上。
现在看一下纹理的坐标,区间是[0,1]:
与颜色一样,纹理坐标也是顶点的一个属性,所以我们把纹理坐标追加到颜色属性的后面,代码如下:
float vertices[] = {
0.5f, 0.5f, 0.0f, 1.0, 0.0, 0.0, 1.0f, 1.0f,
0.5f, -0.5f, 0.0f, 0.0, 1.0, 0.0, 1.0f, 0.0f,
-0.5f, -0.5f, 0.0f, 0.0, 0.0, 1.0, 0.0f, 0.0f,
-0.5f, 0.5f, 0.0f, 0.9, 0.6, 0.8, 0.0f, 1.0f
};
最后的两列就是我们的纹理坐标。内存分布如下图所示:
以一个点为例:(0.5, 0.5) 是顶点的位置坐标,代表矩形右上角的位置,0.5是它在屏幕的坐标系下的位置。对应到纹理上就是 (1,1),因为纹理的右上角要贴在这个矩形的右上角,而不关心矩形到底有多大。参考下图:
- 2.3 修改我们的顶点着色器:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTex;
uniform mat4 model;
out vec3 outColor;
out vec2 outTex;
void main()
{
gl_Position = model * vec4(aPos, 1.0);
outColor = aColor;
outTex = aTex;
}
在我们的顶点着色器中,添加了一个新的属性纹理坐标 layout (location = 2) in vec2 aTex;
, 并用相同的类型将这个属性的值传递给片段着色器 outTex = aTex;
。
此时还要在C++代码中,告诉OpenGL如何用刚才添加的数据,这与之前颜色的做法相同:
注意此时stride要修改为8。
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, false, stride, (void*)(6*sizeof(float)));
紧接着在片段着色器中:
#version 330 core
in vec3 outColor;
in vec2 outTex;
uniform sampler2D image;
void main()
{
vec4 color = texture(image, outTex);
gl_FragColor = color;
}
我们接收到了顶点坐标,并声明了一个uniform的2D的纹理对象。因为纹理对于所有的片段都是一致的,所以是uniform变量。然后用texture函数从纹理上在outTex的位置进行采样,得到最终的颜色值并输出给下一个环节,最终通过各种测试后,显示到屏幕上。
4.加载纹理并使用
万事俱备,只差把图片数据传给OpenGL使用了。
- 4.1 首先,我们需要将图片加载到内存中来
因为图片大都是压缩格式保存到硬盘的,jpg,png等格式都是压缩的,需要把他们解压缩,变成RGB或者RGBA或者BGR等排列方式的原始数据,我们这里使用一个库(stb image loader)来做,它已经做了大量格式的兼容,并且使用简单:
int width, height, channels;
unsigned char* data = stbi_load("../resources/images/person.jpg", &width, &height, &channels, 0 );
- 4.2 像OpenGL申请一块内存,并把数据传过去,完整使用代码如下:
// Gen Texture
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTextureParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTextureParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTextureParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTextureParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// Load Image
int width, height, channels;
unsigned char* data = stbi_load("../resources/images/person.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);
}
stbi_image_free(data);
glBindTexture(GL_TEXTURE_2D, 0);
最后记得要释放内存,因为已经传递给OpenGL了,显存上已经存在,那么主内存的数据就不需要了。
- 4.3 直接运行绘制
从运行效果看,我们的确将图片显示了出来,但却是上下翻转的。这是因为OpenGL的Y轴 0 从底部开始,而图像的 Y轴 0 基本从 上面开始。这也很常见,比如Android的屏幕坐标就是左上角开始的,向下Y轴增加。
直接使用stb_image.h这个头文件里的一个方法翻转图片就可以了:
stbi_set_flip_vertically_on_load(true);
以上是使用单张图片的全部内容,如何使用多张图片,我们下一章再看。