QtOpeng之为三角形绘制纹理Qt5

前几天翻译了国外的一篇绘制三角形的文章,经过几天消化吸收,写篇自己绘制纹理的,之后顺便给出两种对纹理混合颜色的实现方法,关键都在于着色器的语言上。在开始之前呢,还是要从opengl的概念说起,明白纹理绘制原理的可以跳过了。

纹理

纹理顾名思义,就是贴在图元上的图片而已。就如下图的关系一样,首先我们有一张纹理图片,其次我们利用前面的知识画了一个三角形,然后在三角形上贴上了这张纹理图。

QtOpeng之为三角形绘制纹理Qt5_第1张图片

其实贴纹理的思路很简单,我们在绘制三角形的时候,利用到了顶点属性,三角形每个顶点都对应了一个坐标。而同样,我们的纹理也具有同样的坐标,叫纹理坐标。对于我们而言,添加纹理,只要依照我们绘制三角形的顶点,依次添加相应的纹理坐标就好了。我们下面给了例子就好理解了。

 

项目创建

按照前面的教程,我们自行创建一个项目,我们定义我们的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是必要的操作。同时我们的三件套,之前提到过,这里简单介绍。

  • initializeGL() override; //用来初始化
  • paintGL() override; //用来绘制
  • resizeGL(); //用来适应窗体大小绘制

 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的纹理坐标的原点在左下角,而实际的图片在电脑中的坐标为左上角,所以这里镜像操作了下,如果不这样做,你的纹理图片出来后是反着的。这样的话纹理被我们添加好了。之后 绘图的时候绑定就行了。

 

VAO与VBO

#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)

顺便再这里解释一下,顶点坐标和纹理坐标的数据,看图就很简单了。

QtOpeng之为三角形绘制纹理Qt5_第2张图片

按照顶点坐标绘图的顺序,依次设定纹理坐标,因为顶点的第一个坐标为左下角,因此把纹理图片的左下角设定为第一个纹理坐标,依次类推很容易理解。这样配置好之后,就开始绘图了。

paintGL()

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函数就好了。

QtOpeng之为三角形绘制纹理Qt5_第3张图片

 整个项目源码。

你可能感兴趣的:(Qt,opengl)