加载纹理
当我们需要给图形赋予复杂的颜色的时候,不大可能使用前面的方法,为每一个顶点指定第一个颜色。通常我们会采用纹理贴图。
每个顶点关联一个纹理坐标。之后在图形的其他片段上进行片段插值。
我们的工作:告诉OpenGL该怎样对纹理采样。
纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。
我们为三角形指定了3个纹理坐标点。我们希望三角形的左下角对应纹理的左下角,因此我们把三角形左下角顶点的纹理坐标设置为(0, 0);三角形的上顶点对应于图片的上中位置所以我们把它的纹理坐标设置为(0.5, 1.0);同理右下方的顶点设置为(1, 0)。我们只要给顶点着色器传递这三个纹理坐标就行了,接下来它们会被传片段着色器中,它会为每个片段进行纹理坐标的插值。
纹理坐标看起来就像这样:
float texCoords[] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f // 上中
};
纹理采样可以采用几种不同的插值方式。所以我们需要自己告诉OpenGL该怎样对纹理采样。
应用纹理
添加纹理图片到资源文件:
修改前缀:
我们必须使用纹理坐标更新顶点数据:
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 // 左上
};
由于我们添加了一个额外的顶点属性,我们必须告诉OpenGL我们新的顶点格式:
// 坐标
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);
// 纹理坐标
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)(6*sizeof(float)));
glEnableVertexAttribArray(2);
注意,我们同样需要调整前面两个顶点属性的步长参数为8 * sizeof(float)
。
接着我们需要调整顶点着色器使其能够接受顶点坐标为一个顶点属性,并把坐标传给片段着色器:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;
out vec3 ourColor;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
TexCoord = aTexCoord;
}
片段着色器应该接下来会把输出变量TexCoord
作为输入变量。
片段着色器也应该能访问纹理对象,但是我们怎样能把纹理对象传给片段着色器呢?GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D
、sampler3D
,或在我们的例子中的sampler2D
。我们可以简单声明一个uniform sampler2D
把一个纹理添加到片段着色器中,稍后我们会把纹理赋值给这个uniform
。
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
我们暂且搁置LearnOpenGL上的调用方法,我们使用QT封装好的纹理对象来加载纹理图片。
首先,设置头文件以及声明一个加载纹理对象:
#include <QOpenGLTexture>
......
class ZjjOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_4_5_Core
{
......
protected:
......
QOpenGLTexture* textureWall;
......
};
在类的函数定义中编辑相关代码:
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 // 左上
};
......
ZjjOpenGLWidget::~ZjjOpenGLWidget()
{
......
delete textureWall;
......
}
void ZjjOpenGLWidget::initializeGL()
{
......
textureWall = new QOpenGLTexture(QImage(":/image/wall.jpg").mirrored());
// 坐标
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);
// 纹理坐标
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8*sizeof(float), (void*)(6*sizeof(float)));
glEnableVertexAttribArray(2);
// 将上下文的GL_ARRAY_BUFFER对象设回默认的ID-0,相当于与所创建的VAO和VBO解绑
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
......
void ZjjOpenGLWidget::paintGL()
{
......
switch (m_shape) {
case Rect:
textureWall->bind(0);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
break;
default:
break;
}
glBindVertexArray(0);
}
}
拓展:纹理还可以与颜色混合
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord)*vec4(ourColor, 0.2);
}
如果直接使用OpenGL的库函数加载纹理对象的过程应该看起来像这样:
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 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 texture" << std::endl;
}
stbi_image_free(data);
使用纹理之前要做的第一件事是把它们加载到我们的应用中。纹理图像可能被储存为各种各样的格式,每种都有自己的数据结构和排列,所以我们如何才能把这些图像加载到应用中呢?一个解决方案是选一个需要的文件格式,比如.PNG
,然后自己写一个图像加载器,把图像转化为字节序列。写自己的图像加载器虽然不难,但仍然挺麻烦的,而且如果要支持更多文件格式呢?你就不得不为每种你希望支持的格式写加载器了。
另一个解决方案也许是一种更好的选择,使用一个支持多种流行格式的图像加载库来为我们解决这个问题。比如说我们要用的stb_image.h
库。
stb_image.h是Sean Barrett的一个非常流行的单头文件图像加载库,它能够加载大部分流行的文件格式,并且能够很简单得整合到你的工程之中。stb_image.h可以在这里下载。下载这一个头文件,将它以stb_image.h的名字加入你的工程,并另创建一个新的C++文件,输入以下代码:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
通过定义STB_IMAGE_IMPLEMENTATION
,预处理器会修改头文件,让其只包含相关的函数定义源码,等于是将这个头文件变为一个.cpp
文件了。现在只需要在你的程序中包含stb_image.h并编译就可以了。
可以看出来,如果按照OpenGL的传统写法的话,还是比较繁琐的,所以直接用QT封装的QOpenGLTexture
类可以很方便地代替上述过程。