前几天翻译了国外的一篇绘制三角形的文章,经过几天消化吸收,写篇自己绘制纹理的,之后顺便给出两种对纹理混合颜色的实现方法,关键都在于着色器的语言上。在开始之前呢,还是要从opengl的概念说起,明白纹理绘制原理的可以跳过了。
纹理顾名思义,就是贴在图元上的图片而已。就如下图的关系一样,首先我们有一张纹理图片,其次我们利用前面的知识画了一个三角形,然后在三角形上贴上了这张纹理图。
其实贴纹理的思路很简单,我们在绘制三角形的时候,利用到了顶点属性,三角形每个顶点都对应了一个坐标。而同样,我们的纹理也具有同样的坐标,叫纹理坐标。对于我们而言,添加纹理,只要依照我们绘制三角形的顶点,依次添加相应的纹理坐标就好了。我们下面给了例子就好理解了。
按照前面的教程,我们自行创建一个项目,我们定义我们的opengl页面类名为widget,记得.pro文件要更改下。
QT += core gui widgets
CONFIG += c++11 console //可不要
CONFIG -= app_bundle
DEFINES += QT_DEPRECATED_WARNINGS
SOURCES += \
main.cpp \
widget.cpp
HEADERS += \
widget.h
RESOURCES += \
texture.qrc //这里的项目文件是之后用来添加纹理图片的,创建时无此项
这里没有什么好说的,我们看看我们的头文件。
#ifndef WIDGET_H
#define WIDGET_H
#include
#include
#include
#include
#include
#include
#include
#include
class Widget : public QOpenGLWidget,protected QOpenGLFunctions
{
Q_OBJECT
public:
Widget(QOpenGLWidget *parent = nullptr);
void makeObject();
~Widget() override;
protected:
virtual void initializeGL() override;
virtual void paintGL() override;
virtual void resizeGL();
private:
void creataData( int count , int number);
private:
QOpenGLShader * vshader;
QOpenGLShader * fshader;
QOpenGLShaderProgram* shaderprogram;
QOpenGLTexture* texture;
QOpenGLBuffer vbo;
QOpenGLVertexArrayObject vao;
QImage * image;
};
#endif // WIDGET_H
这里跟我们之前创建的头文件有些区别,但实际上是换汤不换药,之前的创建方式将着色器语言放入到了resource中,这里我们会演示一种在程序中编写着色器的方式,两种方式并无优劣,之前提到的会少写几行代码。
继承两个类QOpenGLWidget, QOpenGLFunctions是必要的操作。同时我们的三件套,之前提到过,这里简单介绍。
cpp代码:
#include "widget.h"
#include
#include
#define PROGRAM_VERTEX_ATTRIBUTE 0
#define PROGRAM_TEXCOORD_ATTRIBUTE 1
Widget::Widget(QOpenGLWidget *parent)
: QOpenGLWidget(parent)
{
}
Widget::~Widget()
{
makeCurrent();
vbo.destroy();
vao.destroy();
delete shaderprogram;
delete texture;
doneCurrent();
}
void Widget::initializeGL()
{
initializeOpenGLFunctions();
glClearColor(0.0, 0.0, 0.0, 0.0);
static float verties[] =
{
-0.5f , -0.5f, 0.0f, 0.0f, 0.0f,
0.5f , -0.5f, 0.0f, 1.0f, 0.0f,
-0.5f , 0.5f, 0.0f, 0.0f, 1.0f,
};
vshader = new QOpenGLShader(QOpenGLShader::Vertex,this);
const char * vsrc =
"attribute highp vec3 vertex;\n"
"attribute mediump vec2 texCoord;\n"
"varying mediump vec2 texc;\n"
"void main(void)\n"
"{\n"
" gl_Position = vec4(vertex,1.0);\n"
" texc = texCoord;\n"
"}\n";
vshader->compileSourceCode( vsrc);
fshader = new QOpenGLShader(QOpenGLShader::Fragment,this);
const char * fsrc =
"uniform sampler2D texture;\n"
"varying mediump vec2 texc;\n"
"void main(void)\n"
"{\n"
" gl_FragColor = texture2D(texture, texc.st);\n"
"}\n";
fshader->compileSourceCode( fsrc);
shaderprogram = new QOpenGLShaderProgram();
shaderprogram->addShader( vshader);
shaderprogram->addShader( fshader);
shaderprogram->link();
shaderprogram->bind();
texture = new QOpenGLTexture(QImage(QString(":./wall.jpg")).mirrored());
vbo.create();
vbo.bind();
vbo.allocate(verties,sizeof(verties));
vao.create();
vao.bind();
shaderprogram->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE);
shaderprogram->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE);
shaderprogram->setAttributeBuffer(PROGRAM_VERTEX_ATTRIBUTE, GL_FLOAT, 0, 3, 5 * sizeof(GLfloat));
shaderprogram->setAttributeBuffer(PROGRAM_TEXCOORD_ATTRIBUTE, GL_FLOAT, 3 * sizeof(GLfloat), 2, 5 * sizeof(GLfloat));
}
void Widget::paintGL()
{
glClear(GL_COLOR_BUFFER_BIT);
glEnable(GL_BLEND);
shaderprogram->bind();
vao.bind();
texture->bind();
glDrawArrays(GL_TRIANGLES,0,3);
shaderprogram->release();
vao.release();
texture->release();
}
void Widget::resizeGL()
{
glViewport(0, 0, width(), height());
}
这里我们重点留意着色器语言的编写,注意与绘制三角形的区别。这里的着色器语言与我们之前的语言有所区别,但本质是一样的,我解释一下,很快就能理解。
attribute 表示只读的顶点数据,只用在顶点着色器中。数据来自当前的顶点状态或者顶点数组。一个attribute可以是浮点数类型的标量,向量,或者矩阵。
varing 顶点着色器的输出,用来向片段着色器传递数据。例如颜色或者纹理坐标,(插值后的数据)作为片段着色器的只读输入数据。必须是全局范围声明的全局变量。可以是浮点数类型的标量,向量,矩阵。不能是数组或者结构体。
uniform 一致变量。在着色器执行期间一致变量的值是不变的。与const常量不同的是,这个值在编译时期是未知的是由着色器外部初始化的。一致变量在顶点着色器和片段着色器之间是共享的。它也只能在全局范围进行声明。
const char * vsrc =
"attribute highp vec3 vertex;\n" //顶点坐标
"attribute mediump vec2 texCoord;\n" //纹理坐标
"varying mediump vec2 texc;\n"
"void main(void)\n"
"{\n"
" gl_Position = vec4(vertex,1.0);\n"
" texc = texCoord;\n"
"}\n";
const char * fsrc =
"uniform sampler2D texture;\n" //纹理图片
"varying mediump vec2 texc;\n"
"void main(void)\n"
"{\n"
" gl_FragColor = texture2D(texture, texc.st);\n"
"}\n";
这段着色器程序了,最开始创建了顶点属性,然后创建纹理属性,并申明了一个varing变量,把纹理坐标从顶点着色器传入片段着色器。片段着色器中uniform变量用来从外部传入纹理图片。最后,将纹理坐标与纹理图片渲染后一起输出。
着色器的代码这里我们写好了,然后我们还是老规矩,编译-链接-绑定。
vshader = new QOpenGLShader(QOpenGLShader::Vertex,this);
const char * vsrc =
"attribute highp vec3 vertex;\n"
"attribute mediump vec2 texCoord;\n"
"varying mediump vec2 texc;\n"
"void main(void)\n"
"{\n"
" gl_Position = vec4(vertex,1.0);\n"
" texc = texCoord;\n"
"}\n";
vshader->compileSourceCode( vsrc);
fshader = new QOpenGLShader(QOpenGLShader::Fragment,this);
const char * fsrc =
"uniform sampler2D texture;\n"
"varying mediump vec2 texc;\n"
"void main(void)\n"
"{\n"
" gl_FragColor = texture2D(texture, texc.st);\n"
"}\n";
fshader->compileSourceCode( fsrc);
shaderprogram = new QOpenGLShaderProgram();
shaderprogram->addShader( vshader);
shaderprogram->addShader( fshader);
shaderprogram->link();
shaderprogram->bind();
这个代码很容易就看懂了,没有什么难点。创建了两个着色器,一个顶点,一个片段。
然后分别编译 compileSourceCode() ,将两者代码合入ShaderProgram,链接绑定。
上文已经提到过我们需要纹理图片,而在qt中添加这个图片也很简单。
texture = new QOpenGLTexture(QImage(QString(":./wall.jpg")).mirrored());
这里的照片是从资源文件里导入的,小伙伴们就自行添加吧。这里需要注意一下这镜像操作。我们 opengl的纹理坐标的原点在左下角,而实际的图片在电脑中的坐标为左上角,所以这里镜像操作了下,如果不这样做,你的纹理图片出来后是反着的。这样的话纹理被我们添加好了。之后 绘图的时候绑定就行了。
#define PROGRAM_VERTEX_ATTRIBUTE 0
#define PROGRAM_TEXCOORD_ATTRIBUTE 1
vbo.create();
vbo.bind();
vbo.allocate(verties,sizeof(verties));
vao.create();
vao.bind();
shaderprogram->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE);
shaderprogram->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE);
shaderprogram->setAttributeBuffer(PROGRAM_VERTEX_ATTRIBUTE, GL_FLOAT, 0, 3, 5 * sizeof(GLfloat));
shaderprogram->setAttributeBuffer(PROGRAM_TEXCOORD_ATTRIBUTE, GL_FLOAT, 3 * sizeof(GLfloat), 2, 5 * sizeof(GLfloat));
vbo用来向我们的gpu传送数据,而gpu如何按照规定的数据格式读入,就是shaderprogram->setAttributeBuffer()函数需要的操作。我们在文中提前定义两个属性位置,方便理解。那么接下来看下这个函数里的参数吧。
void setAttributeBuffer
(int location, GLenum type, int offset, int tupleSize, int stride = 0);
这里参数的设定与我们着色器程序是分不开的。我们最开始定义了两个属性一个顶点坐标vec3,一个是纹理坐标vec2。由此,我们定义出的数据如下:
static float verties[] =
{
//顶点坐标 //纹理坐标
-0.5f , -0.5f, 0.0f, 0.0f, 0.0f,
0.5f , -0.5f, 0.0f, 1.0f, 0.0f,
-0.5f , 0.5f, 0.0f, 0.0f, 1.0f,};
- 第一个参数指定我们要配置的顶点属性。enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE)这里配置了属性为0,因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入PROGRAM_VERTEX_ATTRIBUTE即0。
- 第二个参数指定数据的类型,这里是GL_FLOAT(GLSL中
vec*
都是由浮点数值组成的)。- 第三个参数它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。
- 第四个参数指定顶点属性的大小。顶点属性是一个
vec3
,它由3个值组成,所以大小是3。- 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在5个
float
之后,我们把步长设置为5 * sizeof(float)
。
static float verties[] =
{
//顶点坐标 //纹理坐标
-0.5f , -0.5f, 0.0f, 0.0f, 0.0f,
0.5f , -0.5f, 0.0f, 1.0f, 0.0f,
-0.5f , 0.5f, 0.0f, 0.0f, 1.0f,
};
而对应的函数参数就很明显了,因为顶点属性从0开始,所以没有偏移(offset =0),该属性大小为3( tuplesize =3 ),下一个顶点属性需要跨越的步长为5(stride = 5)。而纹理坐标属性需要这么配置,因为纹理坐标是从数据位置偏移3开始( offset =3),大小为2, (tuplesize =2 ),步长跨越为5(stride = 5)。
顺便再这里解释一下,顶点坐标和纹理坐标的数据,看图就很简单了。
按照顶点坐标绘图的顺序,依次设定纹理坐标,因为顶点的第一个坐标为左下角,因此把纹理图片的左下角设定为第一个纹理坐标,依次类推很容易理解。这样配置好之后,就开始绘图了。
void Widget::paintGL()
{
glClear(GL_COLOR_BUFFER_BIT);
glEnable(GL_BLEND);
shaderprogram->bind(); //着色器绑定
vao.bind(); //buffer对象绑定
texture->bind();//纹理绑定
glDrawArrays(GL_TRIANGLES,0,3);
shaderprogram->release();
vao.release();
texture->release();
}
注意paintGL()中要记得释放资源。因为我们只需要画一个三角形,因此设定数据起始位置为0,总共为3个点。
好了到这里我们的关键代码都结束了。自己更改下main函数,实例化widget,对象调用show函数就好了。
整个项目源码。