一、纹理
之前为每个顶点添加颜色来增加图形的细节,从而创建出有趣的图像,但是,如果想让图形看起来更真实,我们必须有足够多的顶点,从而指定足够多的颜色,这将会产生很多额外开销,为了节省开销,使用纹理(Texture)来实现功能。纹理是一个2D图片(也有1D和3D的纹理),它可以用来添加物体的细节,可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D房子上,这样你的房子看起来就像有砖墙外表了。我们可以在一张图片上插入非常多的细节,这样可以让物体非常精细而不用指定额外的顶点。
下面是之前的教程的三角形贴上了一张砖墙的图片:
为了能够把纹理映射(Map)到三角形上,需要指定三角形的每个顶点各自对应纹理的哪个部分,这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样,之后在图形的其它片段上进行片段插值。
纹理坐标在x和y轴上(我们使用的是2D纹理图像),范围从0到1之间。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0,0),也就是纹理图片的左下角,终止于(1,1),即纹理图片的右上角。下面的图片展示了如何把纹理坐标映射到三角形上:
如上图所示,为三角形指定了3个纹理坐标点,希望三角形的左下角对应纹理的左下角,因此把三角形左下角顶点的纹理坐标设置为(0,0);三角形的上顶点对应图片的上中位置,所以把它的纹理坐标设置为(0.5,1);同理右下方的顶点的纹理坐标设置为(1,0)。给顶点着色器传递这三个纹理坐标,之后把这个三个纹理坐标传给片段着色器,在片段着色器上进行纹理坐标的插值。
二、使用QOpenGLTexture类绘制纹理
在项目目录下创建一个文件夹,并放入三张图片
右击资源文件res.qrc,选择“添加现有文件”
选择刚才的三张图片,点击“打开”
三张图片被添加到项目文件树上
在myopenglwidget.h文件中包含QOpenGLTexture头文件,并创建其类对象指针:
#ifndef MYOPENGLWIDGET_H
#define MYOPENGLWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
class MyOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_3_3_Core
{
Q_OBJECT
public:
explicit MyOpenGLWidget(QWidget *parent = nullptr);
~MyOpenGLWidget();
protected:
virtual void initializeGL();
virtual void resizeGL(int w, int h);
virtual void paintGL();
private:
QOpenGLShaderProgram m_shaderProgram;
QOpenGLTexture *m_textureWall;
};
#endif // MYOPENGLWIDGET_H
更改myopenglwidget.cpp代码如下:
#include "myopenglwidget.h"
#include <QDebug>
unsigned int VBO; //顶点缓冲对象
unsigned int VAO; //顶点数组对象
MyOpenGLWidget::MyOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
}
MyOpenGLWidget::~MyOpenGLWidget()
{
makeCurrent();
glDeleteBuffers(1, &VBO);
glDeleteVertexArrays(1, &VAO);
doneCurrent();
}
void MyOpenGLWidget::initializeGL()
{
//初始化OpenGL函数
initializeOpenGLFunctions();
//创建VBO,并赋予ID
glGenBuffers(1, &VBO);
//绑定VBO对象
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//顶点数据
float vertices[] = {
//位置 //颜色 //纹理
-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, //上中
};
//把顶点数据复制到显存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//创建VAO对象,并赋予ID
glGenVertexArrays(1, &VAO);
//绑定VAO对象
glBindVertexArray(VAO);
//位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); //开启VAO管理的第一个属性值
//颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1); //开启VAO管理的第二个属性值
//纹理属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2); //开启VAO管理的第三个属性值
//解绑VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
//解绑VAO
glBindVertexArray(0);
//创建一个程序对象
m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/shaders/shapes.vert");
m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/shaders/shapes.frag");
bool success = m_shaderProgram.link();
if(!success)
qDebug()<<"ERR:" << m_shaderProgram.log();
m_shaderProgram.bind();
m_shaderProgram.setUniformValue("vertexColor", 0.0, 1.0, 0.0, 1.0);
m_textureWall = new QOpenGLTexture(QImage(":/images/wall.jpg").mirrored()); //mirrored消除镜像
m_shaderProgram.setUniformValue("textureWall", 0); //把纹理单元传给片段着色器中的采样器
}
void MyOpenGLWidget::resizeGL(int w, int h)
{
Q_UNUSED(w);
Q_UNUSED(h);
//glViewport(0, 0, w, h);
}
void MyOpenGLWidget::paintGL()
{
//设置墨绿色背景
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置
glClear(GL_COLOR_BUFFER_BIT); //状态使用
//绘制
m_shaderProgram.bind(); //激活程序对象
glBindVertexArray(VAO); //绑定VAO
m_textureWall->bind(0); //绑定激活纹理单元0
glDrawArrays(GL_TRIANGLES, 0, 3); //绘图
}
上面的代码中,在顶点数据vertices数组中多添加了三组纹理坐标数据:
float vertices[] = {
//位置 //颜色 //纹理
-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, //上中
};
告诉OpenGL新的顶点格式,同时我们需要调整前两个顶点属性的步长参数为:8 * sizeof(float)
//位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); //开启VAO管理的第一个属性值
//颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1); //开启VAO管理的第二个属性值
//纹理属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2); //开启VAO管理的第三个属性值
加载图片生成一个纹理单元
m_textureWall = new QOpenGLTexture(QImage(":/images/wall.jpg").mirrored()); //mirrored消除镜像
使用setUniformValue函数把textureWall设置为第1个纹理单元,编号为0
m_shaderProgram.setUniformValue("textureWall", 0); //把纹理单元传给片段着色器中的采样器
在paintGL()函数绘图之前需要绑定激活纹理单元
m_textureWall->bind(0);
更改shapes.vert文件代码如下:
#version 330 core
layout (location = 0) in vec3 aPos; //位置变量的属性位置值为0
layout (location = 1) in vec3 aColor; //颜色变量的属性位置值为1
layout (location = 2) in vec2 aTexture; //纹理变量的属性位置值为2
out vec3 ourColor; //向片段着色器输出一个颜色坐标
out vec2 ourTexture; //向片段着色器输出一个纹理坐标
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
ourTexture = aTexture;
}
在顶点着色器中定义了新的顶点纹理属性(aTexture),并把纹理坐标传给片段着色器(ourTexture)
更改shapes.frag文件代码如下:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 ourTexture; //纹理坐标
uniform sampler2D textureWall; //第0个采样器
void main()
{
FragColor = texture(textureWall, ourTexture);
}
在片段着色器中接收纹理坐标(ourTexture);sampler2D是GLSL中一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),texture()函数是从采样器(textureWall)中的ourTexture位置处取点的颜色。
运行程序,界面显示如下:
三、纹理单元
一个纹理的位置值通常被称为纹理单元(Texture Unit)。纹理单元的主要目的是让我们在着色器中可以使用多个纹理。通过把纹理单元赋值给采样器,实现一次绑定多个纹理。OpenGL保证至少有16个纹理单元。
更改代码,在一个矩形中使用两个纹理单元来绘图。在shapes.frag文件中再创建一个采样器textureSmile,并将两个采样器结合使用
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 ourTexture; //纹理坐标
uniform sampler2D textureWall; //第0个采样器
uniform sampler2D textureSmile; //第1个采样器
void main()
{
//texture()函数是从纹理单元textureWall中的ourTexture位置处取一个点的颜色
FragColor = mix(texture(textureWall, ourTexture), texture(textureSmile, ourTexture), 0.2);
}
mix函数需要接受两个值作为参数,并对它们根据第三个参数进行线性插值,最终输出的颜色是两个纹理的混合;第三个值是0.2会返回80%的第一个输入颜色和20%的第二个输入颜色,即返回两个纹理的混合色。
在myopenglwidget.h文件中再创建一个QOpenGLTexture类对象指针m_textureSmile
#ifndef MYOPENGLWIDGET_H
#define MYOPENGLWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
class MyOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_3_3_Core
{
Q_OBJECT
public:
explicit MyOpenGLWidget(QWidget *parent = nullptr);
~MyOpenGLWidget();
protected:
virtual void initializeGL();
virtual void resizeGL(int w, int h);
virtual void paintGL();
private:
QOpenGLShaderProgram m_shaderProgram;
QOpenGLTexture *m_textureWall;
QOpenGLTexture *m_textureSmile;
};
#endif // MYOPENGLWIDGET_H
更改myopenglwidget.cpp文件代码如下:
更改顶点数据:
//顶点数据
float vertices[] = {
//位置 //颜色 //纹理
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.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, 0.0f, //左下角
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f //左上角
};
在initializeGL()函数中使用m_textureSmile指针
m_textureSmile = new QOpenGLTexture(QImage(":/images/awesomeface.png").mirrored()); //mirrored消除镜像
m_shaderProgram.setUniformValue("textureSmile", 1); //把纹理单元传给片段着色器中的采样器
更改paintGL()函数代码:
void MyOpenGLWidget::paintGL()
{
//设置墨绿色背景
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置
glClear(GL_COLOR_BUFFER_BIT); //状态使用
//绘制
m_shaderProgram.bind(); //激活程序对象
glBindVertexArray(VAO); //绑定VAO
m_textureWall->bind(0); //绑定激活纹理单元0
m_textureSmile->bind(1); //绑定激活纹理单元1
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL); //绘图
}
运行结果如下:
四、纹理环绕方式
纹理坐标的范围通常是从(0,0)到(1,1),当我们把纹理坐标设置在范围之外,OpenGL提供了如下的纹理环绕方式:
环绕方式 | 描述 |
GL_REPEAT | 重复纹理图像,默认环绕方式 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但是每次重复的图片都是镜像放置的 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果 |
GL_CLAMP_TO_BORDER | 超出的部分为用户指定的边缘颜色 |
纹理环绕方式的样式如下如所示:
float borderColor[] = {1.0f, 1.0f, 0.0f, 1.0f};
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
使用如下代码进行纹理环绕方式的设置:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
第一个参数指定纹理目标,我们使用的是2D纹理,所以目标是GL_TEXTURE_2D;第二个参数指定设置的选项与应用的纹理轴,我们打算配置的是WRAP选项,并且指定s和t轴(如果是3D纹理那么还有一个r,它们和x、y、z是等价的);第三个参数指定环绕方式。
如果设置的环绕方式是GL_CLAMP_TO_BORDER,还需要指定一个边缘的颜色:
float borderColor[] = {1.0f, 1.0f, 0.0f, 1.0f};
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
在shapes.frag文件中创建第2个采样器:textureSmall
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 ourTexture; //纹理坐标
uniform sampler2D textureWall; //第0个采样器
uniform sampler2D textureSmile; //第1个采样器
uniform sampler2D textureSmall; //第2个采样器
void main()
{
FragColor = texture(textureSmall, ourTexture);
}
在myopenglwidget.h文件中再创建一个QOpenGLTexture类对象指针m_textureSmall;
QOpenGLTexture *m_textureSmall;
更改顶点数据,为了显示效果更明显位置坐标由0.5改成了0.9,绘图的范围变大;同时使纹理坐标超过范围(0,0)到(1,1),这样OpenGL会使用纹理环绕方式
//顶点数据
float vertices[] = {
//位置 //颜色 //纹理
0.9f, 0.9f, 0.0f, 1.0f, 0.0f, 0.0f, 3.0f, 3.0f, //右上角
0.9f, -0.9f, 0.0f, 0.0f, 1.0f, 0.0f, 3.0f, 0.0f, //右下角
-0.9f, -0.9f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, //左下角
-0.9f, 0.9f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 3.0f //左上角
};
更改myopenglwidget.cpp文件中的initializeGL()函数中使用m_textureSmall指针,并设置纹理环绕方式:
m_textureSmall = new QOpenGLTexture(QImage(":/images/small.png").mirrored()); //mirrored消除镜像
m_shaderProgram.setUniformValue("textureSmall", 2); //把纹理单元传给片段着色器中的采样器
m_textureSmall->bind(2);
//纹理环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
更改paintGL()函数代码:
void MyOpenGLWidget::paintGL()
{
//设置墨绿色背景
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置
glClear(GL_COLOR_BUFFER_BIT); //状态使用
//绘制
m_shaderProgram.bind(); //激活程序对象
glBindVertexArray(VAO); //绑定VAO
m_textureSmall->bind(2); //绑定激活纹理单元2
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL); //绘图
}
运行结果如下:
可以更改glTexParameteri函数第三个参数,查看其他的纹理环绕方式显示效果。
五、纹理过滤
纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以OpenGL需要知道怎么将纹理像素(Texture Pixel)映射到纹理坐标上。纹理像素(Texture Pixel)也叫Texel,你可以想象打开一张.jpg格式图片,不断放大你会发现它是由无数像素点组成的,这个点就是纹理像素;注意不要和纹理坐标搞混,纹理坐标是你给模型顶点设置的那个数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色。
- 纹理坐标的精度是无限的,可以是任意的浮点值
- 纹理像素是有限的(图片分辨率)
- 一个像素需要一个颜色
- 采样:通过纹理坐标向图片要纹理像素的颜色值
当你有一个很大的物体但是分辨率很低的时候,就需要用的纹理过滤(Texture Filtering),纹理过滤有很多个选项,但是现在只讨论最重要的两种:GL_NEAREST和GL_LINEAR。
GL_NEAREST(叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中可以看到四个像素,加号代表纹理坐标,左上角那个纹理像素的中心离纹理坐标最近,所以它会被选择为样本颜色:
GL_LINEAR(叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中可以看到返回的颜色是邻近像素的混合色:
在一个很大的物体上应用一个低分辨率的纹理样式如下所示(纹理被放大,每个纹理像素都能看到):
GL_NEAREST产生了颗粒感的图案,我们能够清晰看到组成纹理的像素,而GL_LINEAR能够产生更平滑的图案,很难看出单个的纹理像素,GL_LINEAR可以产生更真实的输出。
当进行放大或缩小操作的时候可以设置纹理过滤的选项,比如可以在纹理被缩小时使用邻近过滤(大纹理贴小面时,纹理的精度高,相邻纹理像素往往色差不大,无需融合,直接就近选取即可),被放大时使用线性过滤。使用下面为放大和缩小时指定过滤方式:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
更改上面代码中顶点数据,是纹理图像放大显示:
//顶点数据
float vertices[] = {
//位置 //颜色 //纹理
0.9f, 0.9f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, //右上角
0.9f, -0.9f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, //右下角
-0.9f, -0.9f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, //左下角
-0.9f, 0.9f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f //左上角
};
设置纹理过滤方式:
m_textureSmall->bind(2);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
运行结果如下:
更改glTexParameteri函数代码如下
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
运行结果如下:
六、多级渐远纹理
多级渐远纹理(Mipmap)简单来说就是一系列的纹理图像,后一个纹理图像时前一个的二分之一。多级渐远纹理背后的理念:一系列的纹理图像,根据观察者与物体的距离,参考临界值,选择最适合物体的距离的那个纹理。如下图所示,第一个枕木与最后一个枕木所需要的纹理像素点数是不同的
OpenGL使用如下代码设置多级渐远纹理:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
就像普通的纹理过滤一样,切换多级渐远纹理级别时可以在两个不同多级渐远纹理级别之间使用GL_NEAREST和GL_LINEAR过滤,还可以使用下面四个选项中的一个代替原有的过滤方式:
过滤方式 | 描述 |
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样 |
一个常用的错误是:将放大过滤的选项设置为多级渐远纹理过滤选项之一,这样没有任何效果,因为多级渐远纹理主要是使用在纹理被缩小的情况下;为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。
多级渐远纹理的效果目前代码还无法演示,之后的章节中会演示其效果。
注:观看OpenGL中文官网(https://learnopengl-cn.github.io/)和阿西拜的现代OpenGL入门(https://ke.qq.com/course/3999604#term_id=104150693)学习OpenGL