Qt中使用OpenGL(三)

纹理

简单理解,纹理就是将 “一层皮” 套在了三维世界中的物体身上,物体身上的每个顶点都对应到了这层皮上的一个位置。

以下通过简单的例子解释纹理,展示一张风景画。

绘制矩形与设定纹理映射坐标

首先要绘制一个矩形。OpenGL 提供了多种绘制多个三角形的方法,其中有一个方法,使得我们只需要 逆时针 定义 4 个顶点,就可以绘制出可以组成矩形的两个三角形。

在屏幕坐标中,左上是 (-1, 1),左下是 (-1, -1),右下是 (1, -1),右上是 (1, 1)。按照这个顺序就可以定义四个顶点了。

1
2
3
4
5
6
float _vertex[] = {
-1, 1, 0, // 左上
-1, -1, 0, // 左下
1, -1, 0, // 右下
1, 1, 0 //右上
};

至此,画两个三角形的准备工作就完成了。然后需要纹理映射坐标,就是将每一个顶点,都对应到纹理上的一个位置。

简单来讲就是用(x,y)这个二维坐标来表示纹理上的一个位置,其中,x 与 y 的值域是 [0, 1]。

其中 [0, 0]表示左下角 ,[1, 1]表示右上角。了解了这些,就可以给顶点附加纹理坐标了,方法很简单,只需要在每个顶点坐标后面,附加两个代表纹理坐标的 float 值就行。

1
2
3
4
5
6
7
// 顶点缓存中前三个是顶点坐标,后两个是纹理坐标
float _vertex[] = {
-1, 1, 0, 0, 1, // 左上
-1, -1, 0, 0, 0, // 左下
1, -1, 0, 1, 0, // 右下
1, 1, 0, 1, 1, //右上
};

此时,每个顶点就有 5 个 float 值了。

使用包含纹理的顶点缓存

很显然,之前使用了 setAttributeBuffer() 这个函数告诉了 OpenGL 每个顶点有 3 个值,现在同样需要使用 setAttributeBuffer() 这个函数告诉 OpenGL,现在每个顶点有 5 个值,其中前三个是顶点坐标,后两个是纹理坐标。

所以,首先要修改shader。

修改 shader

之前的 shader 里面,定义了 1 个 vec3 类型的输入,所以要再加一个 vec2 类型的输入

1
2
3
4
5
6
7
8
9
#version 450 core
in vec3 vPos;
in vec2 vTexture;
out vec2 oTexture;
void main()
{
gl_Position = vec4(vPos, 1.0);
oTexture = vTexture;
}

在 Vertex shader 中,在获取坐标的同时也要获取纹理坐标,然后将纹理坐标发送给其他shader。

这里添加了一个输入变量 vTexture,还定义了一个输出变量 oTexture,因为 shader 是一种链式调用的代码,Vertex shader 会自动从顶点缓存中获取数据,可是 Fargment shader 不会,所以需要从顶点缓存中获取数据,然后将获取到的数据传给 Fragment shader。

1
2
3
4
5
6
7
# version 450 core
in vec2 oTexture;
uniform sampler2D uTexture;
void main()
{
gl_FragColor = texture(uTexture, oTexture);
}

在 Fragment shader 中,要定义一个名字和 Vertex shader 中定义输出变量名字一致的输入变量 oTexture,Vertex shader 中的输出变量就会自动的传递到 Fragment shader 中的输入变量中。然后使用 OpenGL 内置的函数 texture() 来进行纹理映射了。

这里除了 in 和 out 之外还有另一个指示器 uniform,它和 in 一样,也表示一种输入,但是,和 in 不一样的是,它的来源可以是任何地方。而 in 的来源只能是顶点缓存或者其他 shader 的输出。可以在不修改顶点缓存的情况下,在任何合理地方输入 uniform 的值。例如可以根据需要,动态的改变纹理。sampler2D 这个类型表示二维纹理。

然后告诉 OpenGL 如何使用顶点缓存。

绑定坐标信息

1
2
3
4
5
6
7
8
// 绑定顶点坐标,从 0 * sizeof(flaot) 字节开始读取 3 个 float,
// 因为每个顶点有 5 个 float 数据,所以下一个数据需要偏移 5 * sizeof(float) 字节
m_program->setAttributeBuffer("vPos", GL_FLOAT, 0 * sizeof(float), 3, 5 * sizeof(float));
m_program->enableAttributeArray("vPos");
// 绑定纹理坐标,从 3 * sizeof(float) 字节开始读取 2 个 float,
// 因为每个顶点有 5 个 float 数据,所以下一个数据需要偏移 5 * sizeof(float) 字节
m_program->setAttributeBuffer("vTexture", GL_FLOAT, 3 * sizeof(float), 2, 5 * sizeof(float));
m_program->enableAttributeArray("vTexture");

setAttributeBuffer()

  • 第一个参数:表示 shader 中变量的名字
  • 第二个参数:表示数据类型
  • 第三个参数:表示在一个顶点中从哪个位置开始是需要的数据,坐标是 0 * sizeof(float),纹理是 3 * sizeof(float)
  • 第四个参数:表示有几个数据,坐标有 3 个数据,纹理坐标有 2 个数据
  • 第五个参数:表示顶点数据中多少数据表示一个顶点,在这里是 5 个 float 值表示一个顶点

绘制带纹理的矩形

加载纹理

在 Qt 中,OpenGL 的纹理使用 QOpenGLTexture 表示。

1
m_texture = new QOpenGLTexture(QImage("path/to/image.png").mirrored());

只要在类的头文件中定义了 QOpenGLTexture *m_texture = nullptr; ,初始化的时候就可以创建纹理了。

在加载图片的时候,要使用 mirrored() 方法,它默认会将图片沿 y 轴进行镜像翻转。因为我们用 (0, 0)表示左下角,所以坐标和一般的图像坐标发生了镜像翻转,所以加载纹理的时候也要镜像翻转一下。

绑定纹理

加载好纹理后,要绑定一下,告诉 OpenGL 有纹理才可以使用。

在绘制前 bind 一下,绘制完毕 release 就可以了。

1
2
3
4
5
6
7
8
9
10
void OpenGLWidget::paintGL()
{
m_texture.bind();
m_vao.bind();
m_program->bind();
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
m_program->release();
m_vao.release();
m_texture.release();
}

绘制矩形的参数不是之前绘制三角形时的 GL_TRIANGLES,0,3, 而是 GL_TRIANGLE_FAN,0,4。其中 GL_TRIANGLE_FAN 就是另一种绘制三角形的方法,它允许我们逆时针定义四个顶点,然后绘制出来两个三角形组成一个矩形。

此时,虽然定义了 uniform sampler2D uTexture; 用来表示纹理,但是始终没有给这个输入变量赋值,但是 OpenGL 又是如何将加载的纹理传到 shader 中?

因为 OpenGL 的纹理系统有个默认设定,那就是使用纹理的时候(调用 bind 函数的时候),会被自动对应到当前激活的纹理,默认激活的纹理就是纹理 0,而我们第一个定义的纹理变量,就会被当作纹理 0。这样,使用的纹理就自动的和 shader 中定义的纹理对应上了。